// license:BSD-3-Clause // copyright-holders:Wilbert Pol /*************************************************************************** softlist.cpp Software list construction helpers. ***************************************************************************/ #include "softlist.h" #include "hash.h" #include "expat.h" #include //************************************************************************** // STATIC VARIABLES //************************************************************************** static std::regex s_potenial_softlist_regex("\\w+(\\:\\w+)*"); //************************************************************************** // FEATURE LIST ITEM //************************************************************************** //------------------------------------------------- // feature_list_item - constructor //------------------------------------------------- feature_list_item::feature_list_item(const std::string &name, const std::string &value) : m_next(nullptr), m_name(name), m_value(value) { } //------------------------------------------------- // feature_list_item - constructor //------------------------------------------------- feature_list_item::feature_list_item(std::string &&name, std::string &&value) : m_next(nullptr), m_name(std::move(name)), m_value(std::move(value)) { } //************************************************************************** // SOFTWARE PART //************************************************************************** //------------------------------------------------- // software_part - constructor //------------------------------------------------- software_part::software_part(software_info &info, std::string &&name, std::string &&interface) : m_next(nullptr), m_info(info), m_name(std::move(name)), m_interface(std::move(interface)) { } //------------------------------------------------- // feature - return the value of the given // feature, if specified //------------------------------------------------- const char *software_part::feature(const std::string &feature_name) const { // scan the feature list for an entry matching feature_name and return the value auto iter = std::find_if( m_featurelist.begin(), m_featurelist.end(), [&feature_name](const feature_list_item &feature) { return feature.name() == feature_name; }); return iter != m_featurelist.end() ? iter->value().c_str() : nullptr; } //------------------------------------------------- // matches_interface - determine if we match // an interface in the provided list //------------------------------------------------- bool software_part::matches_interface(const char *interface_list) const { // if we have no interface, then we match by default if (m_interface.empty()) return true; // copy the comma-delimited interface list and ensure it ends with a final comma std::string interfaces = std::string(interface_list).append(","); // then add a comma to the end of our interface and return true if we find it in the list string std::string our_interface = std::string(m_interface).append(","); return (interfaces.find(our_interface) != -1); } //************************************************************************** // SOFTWARE INFO //************************************************************************** //------------------------------------------------- // software_info - constructor //------------------------------------------------- software_info::software_info(std::string &&name, std::string &&parent, const std::string &supported) : m_supported(SOFTWARE_SUPPORTED_YES), m_shortname(std::move(name)), m_parentname(std::move(parent)) { // handle the supported flag if provided if (supported == "partial") m_supported = SOFTWARE_SUPPORTED_PARTIAL; else if (supported == "no") m_supported = SOFTWARE_SUPPORTED_NO; } //------------------------------------------------- // find_part - find a part by name with an // optional interface match //------------------------------------------------- const software_part *software_info::find_part(const std::string &part_name, const char *interface) const { // look for the part by name and match against the interface if provided auto iter = std::find_if( m_partdata.begin(), m_partdata.end(), [&](const software_part &part) { // try to match the part_name (or all parts if part_name is empty), and then try // to match the interface (or all interfaces if interface is nullptr) return (part_name.empty() || part_name == part.name()) && (interface == nullptr || part.matches_interface(interface)); }); return iter != m_partdata.end() ? &*iter : nullptr; } //------------------------------------------------- // has_multiple_parts - return true if we have // more than one part matching the given // interface //------------------------------------------------- bool software_info::has_multiple_parts(const char *interface) const { int count = 0; // increment the count for each match and stop if we hit more than 1 for (const software_part &part : m_partdata) if (part.matches_interface(interface)) if (++count > 1) return true; return false; } //************************************************************************** // SOFTWARE LIST PARSER //************************************************************************** //------------------------------------------------- // softlist_parser - constructor //------------------------------------------------- softlist_parser::softlist_parser(util::core_file &file, const std::string &filename, std::string &description, std::list &infolist, std::ostringstream &errors) : m_file(file), m_filename(filename), m_infolist(infolist), m_errors(errors), m_done(false), m_description(description), m_data_accum_expected(false), m_current_info(nullptr), m_current_part(nullptr), m_pos(POS_ROOT) { // create the parser m_parser = XML_ParserCreate_MM(nullptr, nullptr, nullptr); if (m_parser == nullptr) throw std::bad_alloc(); // set the handlers XML_SetUserData(m_parser, this); XML_SetElementHandler(m_parser, &softlist_parser::start_handler, &softlist_parser::end_handler); XML_SetCharacterDataHandler(m_parser, &softlist_parser::data_handler); // parse the file contents m_file.seek(0, SEEK_SET); char buffer[1024]; while (!m_done) { u32 length = m_file.read(buffer, sizeof(buffer)); m_done = m_file.eof(); if (XML_Parse(m_parser, buffer, length, m_done) == XML_STATUS_ERROR) { parse_error("%s", parser_error()); break; } } // free the parser XML_ParserFree(m_parser); } //------------------------------------------------- // line //------------------------------------------------- int softlist_parser::line() const { return XML_GetCurrentLineNumber(m_parser); } //------------------------------------------------- // column //------------------------------------------------- int softlist_parser::column() const { return XML_GetCurrentColumnNumber(m_parser); } //------------------------------------------------- // parser_error //------------------------------------------------- const char *softlist_parser::parser_error() const { return XML_ErrorString(XML_GetErrorCode(m_parser)); } //------------------------------------------------- // parse_error - append a parsing error with // filename, line and column information //------------------------------------------------- template inline void softlist_parser::parse_error(Format &&fmt, Params &&... args) { // always start with filename(line.column): util::stream_format(m_errors, "%s(%d.%d): ", m_filename, line(), column()); // append the remainder of the string util::stream_format(m_errors, std::forward(fmt), std::forward(args)...); // append a newline at the end m_errors.put('\n'); } //------------------------------------------------- // parse_attributes - helper to parse a set of // attributes into a list of strings //------------------------------------------------- template std::vector softlist_parser::parse_attributes(const char **attributes, const T &attrlist) { std::vector outlist(std::distance(std::begin(attrlist), std::end(attrlist))); // iterate over attribute/value pairs for( ; attributes[0]; attributes += 2) { auto iter = std::begin(attrlist); // look for a match among the attributes provided for (std::size_t index = 0; iter != std::end(attrlist); ++index, ++iter) { if (strcmp(attributes[0], *iter) == 0) { // if found, set the corresponding output entry to the value outlist[index] = attributes[1]; break; } } // if not found, report an unknown attribute if (iter == std::end(attrlist)) unknown_attribute(attributes[0]); } return outlist; } //------------------------------------------------- // parse_name_and_value - helper to parse "name" // and "value" attribute pairs (allowing the // latter to be defined as an empty string) //------------------------------------------------- bool softlist_parser::parse_name_and_value(const char **attributes, std::string &name, std::string &value) { bool found_value = false; // iterate over attribute/value pairs for( ; attributes[0]; attributes += 2) { // if found, set the corresponding output entry to the value if (strcmp(attributes[0], "name") == 0) { name = attributes[1]; } else if (strcmp(attributes[0], "value") == 0) { value = attributes[1]; found_value = true; } // if not found, report an unknown attribute else unknown_attribute(attributes[0]); } return !name.empty() && found_value; } //------------------------------------------------- // add_rom_entry - append a new ROM entry to the // current part's list //------------------------------------------------- void softlist_parser::add_rom_entry(std::string &&name, std::string &&hashdata, u32 offset, u32 length, u32 flags) { // get the current part if (m_current_part == nullptr) { parse_error("ROM entry added in invalid context"); return; } // make sure we don't add duplicate regions if (!name.empty() && (flags & ROMENTRY_TYPEMASK) == ROMENTRYTYPE_REGION) { for (auto &elem : m_current_part->m_romdata) if (elem.name() == name) parse_error("Duplicated dataarea %s in software %s", name, infoname()); } m_current_part->m_romdata.emplace_back(std::move(name), std::move(hashdata), offset, length, flags); } //------------------------------------------------- // start_handler - expat handler for tag start //------------------------------------------------- void softlist_parser::start_handler(void *data, const char *tagname, const char **attributes) { // switch off the current state softlist_parser *state = reinterpret_cast(data); switch (state->m_pos) { case POS_ROOT: state->parse_root_start(tagname, attributes); break; case POS_MAIN: state->parse_main_start(tagname, attributes); break; case POS_SOFT: state->parse_soft_start(tagname, attributes); break; case POS_PART: state->parse_part_start(tagname, attributes); break; case POS_DATA: state->parse_data_start(tagname, attributes); break; } // increment the state since this is a tag start state->m_pos = parse_position(state->m_pos + 1); } //------------------------------------------------- // end_handler - handle end-of-tag post-processing //------------------------------------------------- void softlist_parser::end_handler(void *data, const char *name) { // reset the text destination and bump the position down softlist_parser *state = reinterpret_cast(data); state->m_pos = parse_position(state->m_pos - 1); // switch off of the new position switch (state->m_pos) { case POS_ROOT: break; case POS_MAIN: state->m_current_info = nullptr; break; case POS_SOFT: state->parse_soft_end(name); state->m_current_part = nullptr; break; case POS_PART: break; case POS_DATA: break; } // stop accumulating state->m_data_accum_expected = false; state->m_data_accum.clear(); } //------------------------------------------------- // data_handler - expat data handler //------------------------------------------------- void softlist_parser::data_handler(void *data, const char *s, int len) { softlist_parser *state = reinterpret_cast(data); // if we have an std::string to accumulate data in, do it if (state->m_data_accum_expected) state->m_data_accum.append(s, len); // otherwise, report an error if the data is non-blank else for (int i = 0; i < len; i++) if (!isspace(s[i])) { state->parse_error("Unexpected content"); break; } } //------------------------------------------------- // parse_root_start - handle tag start at the root //------------------------------------------------- void softlist_parser::parse_root_start(const char *tagname, const char **attributes) { // if (strcmp(tagname, "softwarelist") == 0) { static const char *attrnames[] = { "name", "description" }; const auto attrvalues = parse_attributes(attributes, attrnames); if (!attrvalues[1].empty()) m_description = attrvalues[1]; } else unknown_tag(tagname); } //------------------------------------------------- // parse_main_start - handle tag start within // a softwarelist tag //------------------------------------------------- void softlist_parser::parse_main_start(const char *tagname, const char **attributes) { // if (strcmp(tagname, "software") == 0) { static const char *attrnames[] = { "name", "cloneof", "supported" }; auto attrvalues = parse_attributes(attributes, attrnames); if (!attrvalues[0].empty()) { m_infolist.emplace_back(std::move(attrvalues[0]), std::move(attrvalues[1]), attrvalues[2].c_str()); m_current_info = &m_infolist.back(); } else parse_error("No name defined for item"); } else unknown_tag(tagname); } //------------------------------------------------- // parse_main_start - handle tag start within // a software tag //------------------------------------------------- void softlist_parser::parse_soft_start(const char *tagname, const char **attributes) { // get the current info; error if none if (m_current_info == nullptr) { parse_error("Tag %s found outside of software context", tagname); return; } // if (strcmp(tagname, "description") == 0) m_data_accum_expected = true; // else if (strcmp(tagname, "year") == 0) m_data_accum_expected = true; // else if (strcmp(tagname, "publisher") == 0) m_data_accum_expected = true; // else if (strcmp(tagname, "info") == 0) { std::string infoname, infovalue; if (parse_name_and_value(attributes, infoname, infovalue)) m_current_info->m_other_info.emplace_back(std::move(infoname), std::move(infovalue)); else parse_error("Incomplete other_info definition"); } // else if (strcmp(tagname, "sharedfeat") == 0) { std::string featname, featvalue; if (parse_name_and_value(attributes, featname, featvalue)) m_current_info->m_shared_info.emplace_back(std::move(featname), std::move(featvalue)); else parse_error("Incomplete sharedfeat definition"); } // else if (strcmp(tagname, "part" ) == 0) { static const char *attrnames[] = { "name", "interface" }; auto attrvalues = parse_attributes(attributes, attrnames); if (!attrvalues[0].empty() && !attrvalues[1].empty()) { m_current_info->m_partdata.emplace_back(*m_current_info, std::move(attrvalues[0]), std::move(attrvalues[1])); m_current_part = &m_current_info->m_partdata.back(); } else parse_error("Incomplete part definition"); } else unknown_tag(tagname); } //------------------------------------------------- // parse_part_start - handle tag start within // a part tag //------------------------------------------------- void softlist_parser::parse_part_start(const char *tagname, const char **attributes) { // get the current part; error if none if (m_current_part == nullptr) { parse_error("Tag %s found outside of part context", tagname); return; } // if (strcmp(tagname, "dataarea") == 0) { static const char *attrnames[] = { "name", "size", "width", "endianness" }; auto attrvalues = parse_attributes(attributes, attrnames); if (!attrvalues[0].empty() && !attrvalues[1].empty()) { // handle region attributes const std::string &width = attrvalues[2]; const std::string &endianness = attrvalues[3]; u32 regionflags = ROMENTRYTYPE_REGION; if (!width.empty()) { if (width == "8") regionflags |= ROMREGION_8BIT; else if (width == "16") regionflags |= ROMREGION_16BIT; else if (width == "32") regionflags |= ROMREGION_32BIT; else if (width == "64") regionflags |= ROMREGION_64BIT; else parse_error("Invalid dataarea width"); } if (!endianness.empty()) { if (endianness == "little") regionflags |= ROMREGION_LE; else if (endianness == "big") regionflags |= ROMREGION_BE; else parse_error("Invalid dataarea endianness"); } add_rom_entry(std::move(attrvalues[0]), "", 0, strtol(attrvalues[1].c_str(), nullptr, 0), regionflags); } else parse_error("Incomplete dataarea definition"); } // else if (strcmp(tagname, "diskarea") == 0) { static const char *attrnames[] = { "name" }; auto attrvalues = parse_attributes(attributes, attrnames); if (!attrvalues[0].empty()) add_rom_entry(std::move(attrvalues[0]), "", 0, 1, ROMENTRYTYPE_REGION | ROMREGION_DATATYPEDISK); else parse_error("Incomplete diskarea definition"); } // else if (strcmp(tagname, "feature") == 0) { std::string featname, featvalue; if (parse_name_and_value(attributes, featname, featvalue)) m_current_part->m_featurelist.emplace_back(std::move(featname), std::move(featvalue)); else parse_error("Incomplete feature definition"); } // else if (strcmp(tagname, "dipswitch") == 0) ; else unknown_tag(tagname); } //------------------------------------------------- // parse_data_start - handle tag start within a // dataarea or diskarea tag //------------------------------------------------- void softlist_parser::parse_data_start(const char *tagname, const char **attributes) { // get the current part; error if none if (m_current_part == nullptr) { parse_error("Tag %s found outside of part context", tagname); return; } // if (strcmp(tagname, "rom") == 0) { static const char *attrnames[] = { "name", "size", "crc", "sha1", "offset", "value", "status", "loadflag" }; auto attrvalues = parse_attributes(attributes, attrnames); std::string &name = attrvalues[0]; const std::string &sizestr = attrvalues[1]; const std::string &crc = attrvalues[2]; const std::string &sha1 = attrvalues[3]; const std::string &offsetstr = attrvalues[4]; std::string &value = attrvalues[5]; const std::string &status = attrvalues[6]; const std::string &loadflag = attrvalues[7]; if (!sizestr.empty() && !offsetstr.empty()) { u32 length = strtol(sizestr.c_str(), nullptr, 0); u32 offset = strtol(offsetstr.c_str(), nullptr, 0); if (loadflag == "reload") add_rom_entry("", "", offset, length, ROMENTRYTYPE_RELOAD | ROM_INHERITFLAGS); else if (loadflag == "reload_plain") add_rom_entry("", "", offset, length, ROMENTRYTYPE_RELOAD); else if (loadflag == "continue") add_rom_entry("", "", offset, length, ROMENTRYTYPE_CONTINUE | ROM_INHERITFLAGS); else if (loadflag == "fill") add_rom_entry("", std::move(value), offset, length, ROMENTRYTYPE_FILL); else if (!name.empty()) { bool baddump = (status == "baddump"); bool nodump = (status == "nodump"); std::string hashdata; if (nodump) { hashdata = string_format("%s", NO_DUMP); if (!crc.empty() && !sha1.empty()) parse_error("No need for hash definition"); } else { if (!crc.empty() && !sha1.empty()) hashdata = string_format("%c%s%c%s%s", util::hash_collection::HASH_CRC, crc, util::hash_collection::HASH_SHA1, sha1, (baddump ? BAD_DUMP : "")); else parse_error("Incomplete rom hash definition"); } // Handle loadflag attribute int romflags = 0; if (loadflag == "load16_word_swap") romflags = ROM_GROUPWORD | ROM_REVERSE; else if (loadflag == "load16_byte") romflags = ROM_SKIP(1); else if (loadflag == "load32_word_swap") romflags = ROM_GROUPWORD | ROM_REVERSE | ROM_SKIP(2); else if (loadflag == "load32_word") romflags = ROM_GROUPWORD | ROM_SKIP(2); else if (loadflag == "load32_byte") romflags = ROM_SKIP(3); add_rom_entry(std::move(name), std::move(hashdata), offset, length, ROMENTRYTYPE_ROM | romflags); } else parse_error("Rom name missing"); } else if (!sizestr.empty() && !loadflag.empty() && loadflag == "ignore") { u32 length = strtol(sizestr.c_str(), nullptr, 0); add_rom_entry("", "", 0, length, ROMENTRYTYPE_IGNORE | ROM_INHERITFLAGS); } else parse_error("Incomplete rom definition"); } // else if (strcmp(tagname, "disk") == 0) { static const char *attrnames[] = { "name", "sha1", "status", "writeable" }; auto attrvalues = parse_attributes(attributes, attrnames); std::string &name = attrvalues[0]; const std::string &sha1 = attrvalues[1]; const std::string &status = attrvalues[2]; const std::string &writeablestr = attrvalues[3]; if (!name.empty() && !sha1.empty()) { const bool baddump = (status == "baddump"); const bool nodump = (status == "nodump" ); const bool writeable = (writeablestr == "yes"); std::string hashdata = string_format("%c%s%s", util::hash_collection::HASH_SHA1, sha1, (nodump ? NO_DUMP : (baddump ? BAD_DUMP : ""))); add_rom_entry(std::move(name), std::move(hashdata), 0, 0, ROMENTRYTYPE_ROM | (writeable ? DISK_READWRITE : DISK_READONLY)); } else if (status != "nodump") // a no_dump chd is not an incomplete entry parse_error("Incomplete disk definition"); } // else if (strcmp(tagname, "dipvalue") == 0) ; else unknown_tag(tagname); } //------------------------------------------------- // parse_soft_end - handle end-of-tag post- // processing within the tag //------------------------------------------------- void softlist_parser::parse_soft_end(const char *tagname) { assert(m_current_info != nullptr); // if (strcmp(tagname, "description") == 0) m_current_info->m_longname = m_data_accum; // else if (strcmp(tagname, "year") == 0) m_current_info->m_year = m_data_accum; // else if (strcmp(tagname, "publisher") == 0) m_current_info->m_publisher = m_data_accum; // else if (strcmp(tagname, "part") == 0) { // get the last part assert(m_current_part != nullptr); if (m_current_part == nullptr) return; // was any dataarea/rom information encountered? if so, add a terminator if (!m_current_part->m_romdata.empty()) add_rom_entry("", "", 0, 0, ROMENTRYTYPE_END); // get the info; if present, copy shared data (we assume name/value strings live // in the string pool and don't need to be reallocated) if (m_current_info != nullptr) for (const feature_list_item &item : m_current_info->shared_info()) m_current_part->m_featurelist.emplace_back(item.name(), item.value()); } } //------------------------------------------------- // software_name_parse - helper that splits a // software identifier (software_list:software:part) // string into separate software_list, software, and part // strings. // // str1:str2:str3 => swlist_name - str1, swname - str2, swpart - str3 // str1:str2 => swlist_name - nullptr, swname - str1, swpart - str2 // str1 => swlist_name - nullptr, swname - str1, swpart - nullptr // // Notice however that we could also have been // passed a string swlist_name:swname, and thus // some special check has to be performed in this // case. //------------------------------------------------- bool software_name_parse(const std::string &identifier, std::string *list_name, std::string *software_name, std::string *part_name) { // first, sanity check the arguments if (!std::regex_match(identifier, s_potenial_softlist_regex)) return false; // reset all output parameters (if specified of course) if (list_name != nullptr) list_name->clear(); if (software_name != nullptr) software_name->clear(); if (part_name != nullptr) part_name->clear(); // if no colon, this is the swname by itself auto split1 = identifier.find_first_of(':'); if (split1 == std::string::npos) { if (software_name != nullptr) *software_name = identifier; return true; } // if one colon, it is the swname and swpart alone auto split2 = identifier.find_first_of(':', split1 + 1); if (split2 == std::string::npos) { if (software_name != nullptr) *software_name = identifier.substr(0, split1); if (part_name != nullptr) *part_name = identifier.substr(split1 + 1); return true; } // if two colons present, split into 3 parts if (list_name != nullptr) *list_name = identifier.substr(0, split1); if (software_name != nullptr) *software_name = identifier.substr(split1 + 1, split2 - (split1 + 1)); if (part_name != nullptr) *part_name = identifier.substr(split2 + 1); return true; }