OXYGENHUB
◆ main openmsx / src / memory / Rom.cc
12.6 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "Rom.hh"
2
3
#include "ConfigException.hh"
4
#include "Debuggable.hh"
5
#include "Debugger.hh"
6
#include "DeviceConfig.hh"
7
#include "EmptyPatch.hh"
8
#include "File.hh"
9
#include "FileContext.hh"
10
#include "FileException.hh"
11
#include "FilePool.hh"
12
#include "Filename.hh"
13
#include "HardwareConfig.hh"
14
#include "IPSPatch.hh"
15
#include "MSXCliComm.hh"
16
#include "MSXMotherBoard.hh"
17
#include "MemBuffer.hh"
18
#include "PanasonicMemory.hh"
19
#include "Reactor.hh"
20
#include "RomDatabase.hh"
21
#include "RomInfo.hh"
22
#include "XMLElement.hh"
23
24
#include "narrow.hh"
25
#include "ranges.hh"
26
#include "sha1.hh"
27
#include "stl.hh"
28
29
#include <algorithm>
30
#include <memory>
31
32
namespace openmsx {
33
34
class RomDebuggable final : public Debuggable
35
{
36
public:
37
	RomDebuggable(Debugger& debugger, Rom& rom);
38
	RomDebuggable(const RomDebuggable&) = delete;
39
	RomDebuggable(RomDebuggable&&) = delete;
40
	RomDebuggable& operator=(const RomDebuggable&) = delete;
41
	RomDebuggable& operator=(RomDebuggable&&) = delete;
42
	~RomDebuggable();
43
44
	[[nodiscard]] unsigned getSize() const override;
45
	[[nodiscard]] std::string_view getDescription() const override;
46
	[[nodiscard]] uint8_t read(unsigned address) override;
47
	void readBlock(unsigned start, std::span<uint8_t> output) override;
48
	void write(unsigned address, uint8_t value) override;
49
	void moved(Rom& r);
50
private:
51
	Debugger& debugger;
52
	Rom* rom;
53
};
54
55
56
Rom::Rom(std::string name_, static_string_view description_,
57
         DeviceConfig& config, std::string_view id /*= {}*/)
58
	: name(std::move(name_)), description(description_)
59
{
60
	// Try all <rom> tags with matching "id" attribute.
61
	std::string errors;
62
	for (auto* c : config.getXML()->getChildren("rom")) {
63
		if (c->getAttributeValue("id", {}) == id) {
64
			try {
65
				init(config.getMotherBoard(), *c, config.getFileContext());
66
				return;
67
			} catch (MSXException& e) {
68
				// remember error message, and try next
69
				if (!errors.empty() && (errors.back() != '\n')) {
70
					errors += '\n';
71
				}
72
				errors += e.getMessage();
73
			}
74
		}
75
	}
76
	if (errors.empty()) {
77
		// No matching <rom> tag.
78
		auto err = strCat("Missing <rom> tag", strCat_if(!id.empty(), " with id=\"", id, '"'));
79
		throw ConfigException(std::move(err));
80
	} else {
81
		// We got at least one matching <rom>, but it failed to load.
82
		// Report error messages of all failed attempts.
83
		throw ConfigException(errors);
84
	}
85
}
86
87
void Rom::init(MSXMotherBoard& motherBoard, XMLElement& config,
88
               const FileContext& context)
89
{
90
	// (Only) if the content of this ROM depends on state that is not part
91
	// of a savestate, we want to compare the sha1sum of the ROM from the
92
	// time the savestate was created with the one from the loaded
93
	// savestate. External state can be a .rom file or a patch file.
94
	File file;
95
	bool checkResolvedSha1 = false;
96
97
	auto sums      = config.getChildren("sha1");
98
	auto filenames = config.getChildren("filename");
99
	const auto* resolvedFilenameElem = config.findChild("resolvedFilename");
100
	const auto* resolvedSha1Elem     = config.findChild("resolvedSha1");
101
	if (config.findChild("firstblock")) {
102
		// part of the TurboR main ROM
103
		//  If there is a firstblock/lastblock tag, (only) use these to
104
		//  locate the rom. In the past we would also write add a
105
		//  resolvedSha1 tag for this type of ROM (before we used
106
		//  'checkResolvedSha1'). So there are old savestates that have
107
		//  both type of tags. For such a savestate it's important to
108
		//  first check firstblock, otherwise it will only load when
109
		//  there is a file that matches the sha1sums of the
110
		//  firstblock-lastblock portion of the containing file.
111
		unsigned first = config.getChildDataAsInt("firstblock", 0);
112
		unsigned last  = config.getChildDataAsInt("lastblock", 0);
113
		rom = motherBoard.getPanasonicMemory().getRomRange(first, last);
114
		assert(rom.data());
115
116
		// Part of a bigger (already checked) rom, no need to check.
117
		checkResolvedSha1 = false;
118
119
	} else if (resolvedFilenameElem || resolvedSha1Elem ||
120
	           !std::ranges::empty(sums) || !std::ranges::empty(filenames)) {
121
		auto& filePool = motherBoard.getReactor().getFilePool();
122
		// first try already resolved filename ..
123
		if (resolvedFilenameElem) {
124
			try {
125
				auto fname = resolvedFilenameElem->getData();
126
				file = File(fname);
127
				filename = fname;
128
			} catch (FileException&) {
129
				// ignore
130
			}
131
		}
132
		// .. then try the actual sha1sum ..
133
		auto fileType = context.isUserContext()
134
			? FileType::ROM : FileType::SYSTEM_ROM;
135
		if (!file.is_open() && resolvedSha1Elem) {
136
			Sha1Sum sha1(resolvedSha1Elem->getData());
137
			auto [f, fname] = filePool.getFile(fileType, sha1);
138
			file = std::move(f);
139
			if (file.is_open()) {
140
				// avoid recalculating same sha1 later
141
				originalSha1 = sha1;
142
				filename = std::move(fname);
143
			}
144
		}
145
		// .. and then try filename as originally given by user ..
146
		if (!file.is_open()) {
147
			for (const auto& f : filenames) {
148
				try {
149
					auto fname = context.resolve(f->getData());
150
					file = File(fname);
151
					filename = std::move(fname);
152
					break;
153
				} catch (FileException&) {
154
					// ignore
155
				}
156
			}
157
		}
158
		// .. then try all alternative sha1sums ..
159
		// (this might retry the actual sha1sum)
160
		if (!file.is_open()) {
161
			for (const auto& s : sums) {
162
				Sha1Sum sha1(s->getData());
163
				auto [f, fname] = filePool.getFile(fileType, sha1);
164
				file = std::move(f);
165
				if (file.is_open()) {
166
					// avoid recalculating same sha1 later
167
					originalSha1 = sha1;
168
					filename = std::move(fname);
169
					break;
170
				}
171
			}
172
		}
173
		// .. still no file, then error
174
		if (!file.is_open()) {
175
			auto error = strCat(
176
				"Couldn't find ROM file for \"", name, '"',
177
				strCat_if(!std::ranges::empty(filenames),
178
					' ', STRCAT_LAZY((*std::ranges::begin(filenames))->getData())),
179
				strCat_if(resolvedSha1Elem,
180
					" (sha1: ", STRCAT_LAZY(resolvedSha1Elem->getData()), ')')
181
				.else_if(!std::ranges::empty(sums),
182
					" (sha1: ", STRCAT_LAZY((*std::ranges::begin(sums))->getData()), ')'),
183
				'.');
184
			throw MSXException(std::move(error));
185
		}
186
187
		// actually read file content
188
		if (config.findChild("filesize") ||
189
		    config.findChild("skip_headerbytes")) {
190
			throw MSXException(
191
				"The <filesize> and <skip_headerbytes> tags "
192
				"inside a <rom> section are no longer "
193
				"supported.");
194
		}
195
		try {
196
			mmap = file.mmap<uint8_t>();
197
			rom = *mmap;
198
		} catch (FileException&) {
199
			throw MSXException("Error reading ROM image: ", filename);
200
		}
201
202
		// For file-based roms, calc sha1 via File::getSha1Sum(). It can
203
		// possibly use the FilePool cache to avoid the calculation.
204
		if (originalSha1.empty()) {
205
			originalSha1 = filePool.getSha1Sum(file, filename);
206
		}
207
208
		// verify SHA1
209
		if (!checkSHA1(config)) {
210
			motherBoard.getMSXCliComm().printWarning(
211
				"SHA1 sum for '", name,
212
				"' does not match with sum of '",
213
				filename, "'.");
214
		}
215
216
		// We loaded an external file, so check.
217
		checkResolvedSha1 = true;
218
219
	} else {
220
		// for an empty SCC the <size> tag is missing, so take 0
221
		// for MegaFlashRomSCC the <size> tag is used to specify
222
		// the size of the mapper (and you don't care about initial
223
		// content)
224
		unsigned size = config.getChildDataAsInt("size", 0) * 1024; // in kb
225
		extendedRom.resize(size);
226
		std::ranges::fill(std::span{extendedRom}, 0xff);
227
		rom = std::span{extendedRom};
228
229
		// Content does not depend on external files. No need to check
230
		checkResolvedSha1 = false;
231
	}
232
233
	if (!rom.empty()) {
234
		if (const auto* patchesElem = config.findChild("patches")) {
235
			// calculate before content is altered
236
			(void)getOriginalSHA1(); // fills cache
237
238
			std::unique_ptr<PatchInterface> patch =
239
				std::make_unique<EmptyPatch>(rom);
240
241
			for (const auto* p : patchesElem->getChildren("ips")) {
242
				patch = std::make_unique<IPSPatch>(
243
					Filename(p->getData(), context),
244
					std::move(patch));
245
			}
246
			auto patchSize = patch->getSize();
247
			if (patchSize <= rom.size()) {
248
				patch->copyBlock(0, std::span{const_cast<uint8_t*>(rom.data()), rom.size()});
249
			} else {
250
				MemBuffer<uint8_t> extendedRom2(patchSize);
251
				patch->copyBlock(0, std::span{extendedRom2});
252
				extendedRom = std::move(extendedRom2);
253
				rom = std::span{extendedRom};
254
			}
255
256
			// calculated because it's different from original
257
			actualSha1 = SHA1::calc(rom);
258
259
			// Content altered by external patch file -> check.
260
			checkResolvedSha1 = true;
261
		}
262
	}
263
264
	// TODO fix this, this is a hack that depends heavily on
265
	//      HardwareConfig::createRomConfig
266
	if (name.starts_with("MSXRom")) {
267
		const auto& db = motherBoard.getReactor().getSoftwareDatabase();
268
		std::string_view title;
269
		if (const auto* romInfo = db.fetchRomInfo(getOriginalSHA1())) {
270
			title = romInfo->getTitle(db.getBufferStart());
271
		}
272
		if (!title.empty()) {
273
			name = title;
274
		} else {
275
			// unknown ROM, use file name
276
			name = file.getOriginalName();
277
			if (name.empty()) name = filename;
278
		}
279
	}
280
281
	// Make name unique wrt all registered debuggables.
282
	auto& debugger = motherBoard.getDebugger();
283
	if (!rom.empty() && debugger.findDebuggable(name)) {
284
		unsigned n = 0;
285
		std::string tmp;
286
		do {
287
			tmp = strCat(name, " (", ++n, ')');
288
		} while (debugger.findDebuggable(tmp));
289
		name = std::move(tmp);
290
	}
291
292
	if (checkResolvedSha1) {
293
		auto& doc = motherBoard.getMachineConfig()->getXMLDocument();
294
		std::array<char, 40> buf;
295
		auto patchedSha1Str = getSHA1().toString(buf);
296
		const auto* actualSha1Elem = doc.getOrCreateChild(
297
			config, "resolvedSha1", doc.allocateString(patchedSha1Str));
298
		if (actualSha1Elem->getData() != patchedSha1Str) {
299
			const auto& tmp = file.is_open() ? filename : name;
300
			// can only happen in case of loadstate
301
			motherBoard.getMSXCliComm().printWarning(
302
				"The content of the rom ", tmp, " has "
303
				"changed since the time this savestate was "
304
				"created. This might result in emulation "
305
				"problems.");
306
		}
307
	}
308
309
	// This must come after we store the 'resolvedSha1', because on
310
	// loadstate we use that tag to search the complete rom in a filePool.
311
	if (const auto* windowElem = config.findChild("window")) {
312
		unsigned windowBase = windowElem->getAttributeValueAsInt("base", 0);
313
		unsigned windowSize = windowElem->getAttributeValueAsInt("size", narrow_cast<int>(rom.size()));
314
		if ((windowBase + windowSize) > rom.size()) {
315
			throw MSXException(
316
				"The specified window [", windowBase, ',',
317
				windowBase + windowSize, ") falls outside "
318
				"the rom (with size ", rom.size(), ").");
319
		}
320
		rom = rom.subspan(windowBase, windowSize);
321
	}
322
323
	// Only create the debuggable once all checks succeeded.
324
	if (!rom.empty()) {
325
		romDebuggable = std::make_unique<RomDebuggable>(debugger, *this);
326
	}
327
}
328
329
bool Rom::checkSHA1(const XMLElement& config) const
330
{
331
	auto sums = config.getChildren("sha1");
332
	return std::ranges::empty(sums) ||
333
	       contains(sums, getOriginalSHA1(),
334
	                [](const auto* s) { return Sha1Sum(s->getData()); });
335
}
336
337
Rom::Rom(Rom&& r) noexcept
338
	: rom          (r.rom)
339
	, extendedRom  (std::move(r.extendedRom))
340
	, filename     (std::move(r.filename))
341
	, mmap         (std::move(r.mmap))
342
	, originalSha1 (r.originalSha1)
343
	, actualSha1   (r.actualSha1)
344
	, name         (std::move(r.name))
345
	, description  (r.description)
346
	, romDebuggable(std::move(r.romDebuggable))
347
{
348
	if (romDebuggable) romDebuggable->moved(*this);
349
}
350
351
Rom::~Rom() = default;
352
353
const Sha1Sum& Rom::getOriginalSHA1() const
354
{
355
	if (originalSha1.empty()) {
356
		originalSha1 = SHA1::calc(rom);
357
	}
358
	return originalSha1;
359
}
360
361
const Sha1Sum& Rom::getSHA1() const
362
{
363
	if (actualSha1.empty())
364
	{
365
		actualSha1 = getOriginalSHA1();
366
	}
367
	return actualSha1;
368
}
369
370
void Rom::addPadding(size_t newSize, uint8_t filler)
371
{
372
	assert(newSize >= rom.size());
373
	if (newSize == rom.size()) return;
374
375
	MemBuffer<uint8_t> tmp(newSize);
376
	copy_to_range(rom, std::span{tmp});
377
	std::ranges::fill(tmp.subspan(rom.size()), filler);
378
379
	rom = std::span{tmp};
380
	extendedRom = std::move(tmp);
381
}
382
383
void Rom::getInfo(TclObject& result) const
384
{
385
	std::array<char, 40> buf1;
386
	std::array<char, 40> buf2;
387
	result.addDictKeyValues("actualSHA1", getSHA1().toString(buf1),
388
	                        "originalSHA1", getOriginalSHA1().toString(buf2),
389
	                        "filename", getFilename());
390
}
391
392
393
RomDebuggable::RomDebuggable(Debugger& debugger_, Rom& rom_)
394
	: debugger(debugger_), rom(&rom_)
395
{
396
	debugger.registerDebuggable(rom->getName(), *this);
397
}
398
399
RomDebuggable::~RomDebuggable()
400
{
401
	debugger.unregisterDebuggable(rom->getName(), *this);
402
}
403
404
unsigned RomDebuggable::getSize() const
405
{
406
	return narrow<unsigned>(rom->size());
407
}
408
409
std::string_view RomDebuggable::getDescription() const
410
{
411
	return rom->getDescription();
412
}
413
414
uint8_t RomDebuggable::read(unsigned address)
415
{
416
	assert(address < getSize());
417
	return (*rom)[address];
418
}
419
420
void RomDebuggable::readBlock(unsigned start, std::span<uint8_t> output)
421
{
422
	copy_to_range(std::span{*rom}.subspan(start, output.size()), output);
423
}
424
425
void RomDebuggable::write(unsigned /*address*/, uint8_t /*value*/)
426
{
427
	// ignore
428
}
429
430
void RomDebuggable::moved(Rom& r)
431
{
432
	rom = &r;
433
}
434
435
} // namespace openmsx