openMSX
RomDatabase.cc
Go to the documentation of this file.
1 #include "RomDatabase.hh"
2 #include "RomInfo.hh"
3 #include "InfoTopic.hh"
4 #include "CommandException.hh"
5 #include "TclObject.hh"
6 #include "FileContext.hh"
7 #include "File.hh"
8 #include "FileOperations.hh"
9 #include "FileException.hh"
11 #include "CliComm.hh"
12 #include "StringOp.hh"
13 #include "MemBuffer.hh"
14 #include "StringMap.hh"
15 #include "rapidsax.hh"
16 #include "unreachable.hh"
17 #include "memory.hh"
18 #include <set>
19 
20 using std::set;
21 using std::string;
22 using std::vector;
23 
24 namespace openmsx {
25 
27 {
28 public:
29  SoftwareInfoTopic(InfoCommand& openMSXInfoCommand, RomDatabase& romDatabase);
30 
31  virtual void execute(const vector<TclObject>& tokens,
32  TclObject& result) const;
33  virtual string help(const vector<string>& tokens) const;
34  virtual void tabCompletion(vector<string>& tokens) const;
35 
36 private:
37  const RomDatabase& romDatabase;
38 };
39 
40 
41 
43 
45 {
46 public:
47  DBParser(RomDatabase::DBMap& romDBSHA1_, UnknownTypes& unknownTypes_,
48  CliComm& cliComm_)
49  : romDBSHA1(romDBSHA1_)
50  , unknownTypes(unknownTypes_)
51  , cliComm(cliComm_)
52  , state(BEGIN)
53  , unknownLevel(0)
54  {
55  }
56 
57  // rapidsax handler interface
58  void start(string_ref name);
59  void attribute(string_ref name, string_ref value);
60  void text(string_ref text);
61  void stop();
62  void doctype(string_ref text);
63 
64  string_ref getSystemID() const { return systemID; }
65 
66 private:
67  void addEntries();
68 
69  enum State {
70  BEGIN,
71  SOFTWAREDB,
72  SOFTWARE,
73  SYSTEM,
74  TITLE,
75  COMPANY,
76  YEAR,
77  COUNTRY,
78  GENMSXID,
79  SW_REMARK,
80  SW_TEXT,
81  DUMP_REMARK,
82  DUMP_TEXT,
83  DUMP,
84  ORIGINAL,
85  ROM,
86  TYPE,
87  START,
88  HASH,
89  END
90  };
91 
92  struct Dump {
93  string remarks;
94  Sha1Sum hash;
95  string_ref origData;
96  RomType type;
97  bool origValue;
98  };
99 
100  RomDatabase::DBMap& romDBSHA1;
101  UnknownTypes& unknownTypes;
102  CliComm& cliComm;
103  set<Sha1Sum> sums;
104 
105  string_ref systemID;
106  string_ref type;
107  string_ref startVal;
108  string_ref algo;
109 
110  vector<Dump> dumps;
111  string_ref system;
112  string_ref title;
113  string_ref company;
114  string_ref year;
115  string_ref country;
116  string remarks;
117  int genMSXid;
118 
119  State state;
120  unsigned unknownLevel;
121 };
122 
124 {
125  if (unknownLevel) {
126  ++unknownLevel;
127  return;
128  }
129 
130  switch (state) {
131  case BEGIN:
132  if (tag == "softwaredb") {
133  state = SOFTWAREDB;
134  } else {
135  throw MSXException("Expected <softwaredb> as root tag.");
136  }
137  break;
138  case SOFTWAREDB:
139  if (tag == "software") {
140  system.clear();
141  title.clear();
142  company.clear();
143  year.clear();
144  country.clear();
145  remarks.clear();
146  genMSXid = 0;
147  dumps.clear();
148  state = SOFTWARE;
149  } else {
150  ++unknownLevel;
151  }
152  break;
153  case SOFTWARE:
154  if (tag == "system") {
155  state = SYSTEM;
156  } else if (tag == "title") {
157  state = TITLE;
158  } else if (tag == "company") {
159  state = COMPANY;
160  } else if (tag == "year") {
161  state = YEAR;
162  } else if (tag == "country") {
163  state = COUNTRY;
164  } else if (tag == "remark") {
165  state = SW_REMARK;
166  } else if (tag == "genmsxid") {
167  state = GENMSXID;
168  } else if (tag == "dump") {
169  dumps.resize(dumps.size() + 1);
170  dumps.back().type = ROM_UNKNOWN;
171  dumps.back().origValue = false;
172  state = DUMP;
173  } else {
174  ++unknownLevel;
175  }
176  break;
177  case DUMP:
178  if (tag == "original") {
179  dumps.back().origValue = false;
180  state = ORIGINAL;
181  } else if (tag == "megarom") {
182  type.clear();
183  startVal.clear();
184  state = ROM;
185  } else if (tag == "rom") {
186  type = "Mirrored";
187  startVal.clear();
188  state = ROM;
189  } else {
190  ++unknownLevel;
191  }
192  break;
193  case ROM:
194  if (tag == "type") {
195  state = TYPE;
196  } else if (tag == "start") {
197  state = START;
198  } else if (tag == "remark") {
199  state = DUMP_REMARK;
200  } else if (tag == "hash") {
201  algo.clear();
202  state = HASH;
203  } else {
204  ++unknownLevel;
205  }
206  break;
207  case SW_REMARK:
208  if (tag == "text") {
209  state = SW_TEXT;
210  } else {
211  ++unknownLevel;
212  }
213  break;
214  case DUMP_REMARK:
215  if (tag == "text") {
216  state = DUMP_TEXT;
217  } else {
218  ++unknownLevel;
219  }
220  break;
221  case SYSTEM:
222  case TITLE:
223  case COMPANY:
224  case YEAR:
225  case COUNTRY:
226  case GENMSXID:
227  case ORIGINAL:
228  case TYPE:
229  case START:
230  case HASH:
231  case SW_TEXT:
232  case DUMP_TEXT:
233  ++unknownLevel;
234  break;
235 
236  case END:
237  throw MSXException("Unexpected opening tag: " + tag);
238 
239  default:
240  UNREACHABLE;
241  }
242 }
243 
245 {
246  if (unknownLevel) return;
247 
248  switch (state) {
249  case ORIGINAL:
250  if (name == "value") {
251  dumps.back().origValue = StringOp::stringToBool(value);
252  }
253  break;
254  case HASH:
255  if (name == "algo") {
256  algo = value;
257  }
258  break;
259  case BEGIN:
260  case SOFTWAREDB:
261  case SOFTWARE:
262  case SYSTEM:
263  case TITLE:
264  case COMPANY:
265  case YEAR:
266  case COUNTRY:
267  case GENMSXID:
268  case SW_REMARK:
269  case SW_TEXT:
270  case DUMP_REMARK:
271  case DUMP_TEXT:
272  case DUMP:
273  case ROM:
274  case TYPE:
275  case START:
276  case END:
277  break;
278  default:
279  UNREACHABLE;
280  }
281 }
282 
283 static void joinRemarks(string& result, string_ref extra)
284 {
285  if (extra.empty()) return;
286  if (!result.empty()) result += '\n';
287  result.append(extra.data(), extra.size());
288 }
289 
291 {
292  if (unknownLevel) return;
293 
294  switch (state) {
295  case SYSTEM:
296  system = text;
297  break;
298  case TITLE:
299  title = text;
300  break;
301  case COMPANY:
302  company = text;
303  break;
304  case YEAR:
305  year = text;
306  break;
307  case COUNTRY:
308  country = text;
309  break;
310  case GENMSXID:
311  genMSXid = stoi(text);
312  // TODO error checks?
313  // cliComm.printWarning(StringOp::Builder() <<
314  // "Ignoring bad Generation MSX id (genmsxid) "
315  // "in entry with title '" << title <<
316  // ": " << data);
317  break;
318  case ORIGINAL:
319  dumps.back().origData = text;
320  break;
321  case TYPE:
322  type = text;
323  break;
324  case START:
325  startVal = text;
326  break;
327  case HASH:
328  if (algo == "sha1") {
329  dumps.back().hash = Sha1Sum(text);
330  }
331  break;
332  case SW_REMARK:
333  case SW_TEXT:
334  joinRemarks(remarks, text);
335  break;
336  case DUMP_REMARK:
337  case DUMP_TEXT:
338  joinRemarks(dumps.back().remarks, text);
339  break;
340  case BEGIN:
341  case SOFTWAREDB:
342  case SOFTWARE:
343  case DUMP:
344  case ROM:
345  case END:
346  break;
347  default:
348  UNREACHABLE;
349  }
350 }
351 
352 // called on </software>
353 void DBParser::addEntries()
354 {
355  if (!system.empty() && (system != "MSX")) {
356  // skip non-MSX entries
357  return;
358  }
359 
360  for (auto& d : dumps) {
361  if (!sums.insert(d.hash).second) {
362  cliComm.printWarning(
363  "duplicate softwaredb entry SHA1: " +
364  d.hash.toString());
365  continue;
366  }
367 
368  auto& ptr = romDBSHA1[d.hash];
369  if (ptr.get()) {
370  // User database already had this entry, don't overwrite
371  // with the value from the system database.
372  continue;
373  }
374 
375  string r = remarks;
376  joinRemarks(r, d.remarks);
377 
378  ptr = make_unique<RomInfo>(
379  title, year, company, country,
380  d.origValue, d.origData, r, d.type,
381  genMSXid);
382  }
383 }
384 
385 static const char* parseStart(string_ref s)
386 {
387  // we expect "0x0000", "0x4000", "0x8000", "0xc000" or ""
388  return ((s.size() == 6) && s.starts_with("0x")) ? (s.data() + 2) : nullptr;
389 }
390 
392 {
393  if (unknownLevel) {
394  --unknownLevel;
395  return;
396  }
397 
398  switch (state) {
399  case SOFTWAREDB:
400  state = END;
401  break;
402  case SOFTWARE:
403  addEntries();
404  state = SOFTWAREDB;
405  break;
406  case SYSTEM:
407  case TITLE:
408  case COMPANY:
409  case YEAR:
410  case COUNTRY:
411  case GENMSXID:
412  case SW_REMARK:
413  state = SOFTWARE;
414  break;
415  case SW_TEXT:
416  state = SW_REMARK;
417  break;
418  case DUMP:
419  if (dumps.back().hash.empty()) {
420  // no sha1 sum specified, drop this dump
421  dumps.pop_back();
422  }
423  state = SOFTWARE;
424  break;
425  case ORIGINAL:
426  state = DUMP;
427  break;
428  case ROM: {
429  string_ref t = type;
430  char buf[12];
431  if (t == "Mirrored") {
432  if (const char* start = parseStart(startVal)) {
433  memcpy(buf, t.data(), 8);
434  memcpy(buf + 8, start, 4);
435  t = string_ref(buf, 12);
436  }
437  } else if (t == "Normal") {
438  if (const char* start = parseStart(startVal)) {
439  memcpy(buf, t.data(), 6);
440  memcpy(buf + 6, start, 4);
441  t = string_ref(buf, 10);
442  }
443  }
444  RomType romType = RomInfo::nameToRomType(t);
445  if (romType == ROM_UNKNOWN) {
446  unknownTypes[t]++;
447  }
448  dumps.back().type = romType;
449  state = DUMP;
450  break;
451  }
452  case TYPE:
453  case START:
454  case HASH:
455  case DUMP_REMARK:
456  state = ROM;
457  break;
458  case DUMP_TEXT:
459  state = DUMP_REMARK;
460  break;
461  case BEGIN:
462  case END:
463  throw MSXException("Unexpected closing tag");
464 
465  default:
466  UNREACHABLE;
467  }
468 }
469 
471 {
472  auto pos1 = text.find(" SYSTEM \"");
473  if (pos1 == string_ref::npos) return;
474  auto t = text.substr(pos1 + 9);
475  auto pos2 = t.find('"');
476  if (pos2 == string_ref::npos) return;
477  systemID = t.substr(0, pos2);
478 }
479 
480 static void parseDB(CliComm& cliComm, const string& filename,
481  RomDatabase::DBMap& romDBSHA1, UnknownTypes& unknownTypes)
482 {
483  File file(filename);
484  auto size = file.getSize();
485  MemBuffer<char> buf(size + 1);
486  file.read(buf.data(), size);
487  buf[size] = 0;
488 
489  DBParser handler(romDBSHA1, unknownTypes, cliComm);
490  rapidsax::parse<rapidsax::trimWhitespace>(handler, buf.data());
491 
492  if (handler.getSystemID() != "softwaredb1.dtd") {
493  throw rapidsax::ParseError(
494  "Missing or wrong systemID.\n"
495  "You're probably using an old incompatible file format.",
496  nullptr);
497  }
498 }
499 
501  : softwareInfoTopic(make_unique<SoftwareInfoTopic>(
502  commandController.getOpenMSXInfoCommand(), *this))
503 {
504  UnknownTypes unknownTypes;
505  // first user- then system-directory
506  vector<string> paths = SystemFileContext().getPaths();
507  for (auto& p : paths) {
508  string filename = FileOperations::join(p, "softwaredb.xml");
509  try {
510  parseDB(cliComm, filename, romDBSHA1, unknownTypes);
511  } catch (rapidsax::ParseError& e) {
512  cliComm.printWarning(StringOp::Builder() <<
513  "Rom database parsing failed: " << e.what());
514  } catch (MSXException& /*e*/) {
515  // Ignore. It's not unusual the DB in the user
516  // directory is not found. In case there's an error
517  // with both user and system DB, we must give a
518  // warning, but that's done below.
519  }
520  }
521  if (romDBSHA1.empty()) {
522  cliComm.printWarning(
523  "Couldn't load software database.\n"
524  "This may cause incorrect ROM mapper types to be used.");
525  }
526  if (!unknownTypes.empty()) {
527  StringOp::Builder output;
528  output << "Unknown mapper types in software database: ";
529  for (auto& p : unknownTypes) {
530  output << p.first() << " (" << p.second << "x); ";
531  }
532  cliComm.printWarning(output);
533  }
534 }
535 
537 {
538 }
539 
540 const RomInfo* RomDatabase::fetchRomInfo(const Sha1Sum& sha1sum) const
541 {
542  auto it = romDBSHA1.find(sha1sum);
543  return (it == romDBSHA1.end()) ? nullptr : it->second.get();
544 }
545 
546 
547 // SoftwareInfoTopic
548 
550  RomDatabase& romDatabase_)
551  : InfoTopic(openMSXInfoCommand, "software")
552  , romDatabase(romDatabase_)
553 {
554 }
555 
556 void SoftwareInfoTopic::execute(const vector<TclObject>& tokens,
557  TclObject& result) const
558 {
559  if (tokens.size() != 3) {
560  throw CommandException("Wrong number of parameters");
561  }
562 
563  Sha1Sum sha1sum = Sha1Sum(tokens[2].getString());
564  const RomInfo* romInfo = romDatabase.fetchRomInfo(sha1sum);
565  if (!romInfo) {
566  // no match found
567  throw CommandException(
568  "Software with sha1sum " + sha1sum.toString() + " not found");
569  }
570 
571  result.addListElement("title");
572  result.addListElement(romInfo->getTitle());
573  result.addListElement("year");
574  result.addListElement(romInfo->getYear());
575  result.addListElement("company");
576  result.addListElement(romInfo->getCompany());
577  result.addListElement("country");
578  result.addListElement(romInfo->getCountry());
579  result.addListElement("orig_type");
580  result.addListElement(romInfo->getOrigType());
581  result.addListElement("remark");
582  result.addListElement(romInfo->getRemark());
583  result.addListElement("original");
584  result.addListElement(romInfo->getOriginal());
585  result.addListElement("mapper_type_name");
587  result.addListElement("genmsxid");
588  result.addListElement(romInfo->getGenMSXid());
589 }
590 
591 string SoftwareInfoTopic::help(const vector<string>& /*tokens*/) const
592 {
593  return "Returns information about the software "
594  "given its sha1sum, in a paired list.";
595 }
596 
597 void SoftwareInfoTopic::tabCompletion(vector<string>& /*tokens*/) const
598 {
599  // no useful completion possible
600 }
601 
602 } // namespace openmsx