// license:LGPL-2.1+ // copyright-holders:Michael Zapf /*************************************************************************** rpk.cpp RPK format support A RPK file ("rompack") contains a collection of dump files and a layout file that defines the kind of circuit board (PCB) used in the cartridge and the mapping of dumps to sockets on the board. Example: DTD: ***************************************************************************/ #include "rpk.h" #include "xmlfile.h" namespace { /*************************************************************************** TYPE DEFINITIONS ***************************************************************************/ class rpk_category_impl : public std::error_category { public: virtual char const *name() const noexcept override { return "rpk"; } virtual std::string message(int condition) const override; }; /*************************************************************************** GLOBAL VARIABLES ***************************************************************************/ rpk_category_impl const f_rpk_category_instance; }; /*************************************************************************** RPK READER ***************************************************************************/ //------------------------------------------------- // ctor //------------------------------------------------- rpk_reader::rpk_reader(char const *const *pcb_types, bool supports_ram) : m_pcb_types(pcb_types) , m_supports_ram(supports_ram) { } //------------------------------------------------- // read //------------------------------------------------- std::error_condition rpk_reader::read(std::unique_ptr &&stream, rpk_file::ptr &result) const { // open the RPK (as a zip file) util::archive_file::ptr zipfile; std::error_condition ziperr = util::archive_file::open_zip(std::move(stream), zipfile); if (ziperr) return ziperr; // open the layout XML if (zipfile->search("layout.xml", false) < 0) return error::MISSING_LAYOUT; // determine the uncompressed length uint64_t uncompressed_length_uint64 = zipfile->current_uncompressed_length(); size_t uncompressed_length = (size_t)uncompressed_length_uint64; if (uncompressed_length != uncompressed_length_uint64) return std::errc::not_enough_memory; // prepare a buffer for the layout XML std::unique_ptr layout_xml_text(new (std::nothrow) char[uncompressed_length + 1]); if (!layout_xml_text) return std::errc::not_enough_memory; // and decompress it ziperr = zipfile->decompress(&layout_xml_text[0], uncompressed_length); if (ziperr) return ziperr; layout_xml_text[uncompressed_length] = 0; // parse the layout text util::xml::file::ptr const layout_xml = util::xml::file::string_read(&layout_xml_text[0], nullptr); if (!layout_xml) return error::XML_ERROR; layout_xml_text.reset(); // now we work within the XML tree // romset is the root node util::xml::data_node const *const romset_node = layout_xml->get_child("romset"); if (!romset_node) return error::INVALID_LAYOUT; // document element must be // resources is a child of romset util::xml::data_node const *const resources_node = romset_node->get_child("resources"); if (!resources_node) return error::INVALID_LAYOUT; // must have a child // configuration is a child of romset; we're actually interested in ... util::xml::data_node const *const configuration_node = romset_node->get_child("configuration"); if (!configuration_node) return error::INVALID_LAYOUT; // must have a child // ... pcb, which is a child of configuration util::xml::data_node const *const pcb_node = configuration_node->get_child("pcb"); if (!pcb_node) return error::INVALID_LAYOUT; // must have a child // we'll try to find the PCB type on the provided type list. std::string const *const pcb_type_string = pcb_node->get_attribute_string_ptr("type"); if (!pcb_type_string) return error::INVALID_LAYOUT; // must have a 'type' attribute"; int pcb_type = 0; while (m_pcb_types[pcb_type] && strcmp(m_pcb_types[pcb_type], pcb_type_string->c_str())) pcb_type++; if (!m_pcb_types[pcb_type]) return error::UNKNOWN_PCB_TYPE; // create the rpk_file object rpk_file::ptr file = std::make_unique(std::move(zipfile), pcb_type); // find the sockets and load their respective resource for (util::xml::data_node const *socket_node = pcb_node->get_first_child(); socket_node; socket_node = socket_node->get_next_sibling()) { if (strcmp(socket_node->get_name(), "socket") != 0) return error::INVALID_LAYOUT; // element has only children std::string const *const id = socket_node->get_attribute_string_ptr("id"); if (!id) return error::INVALID_LAYOUT; // must have an 'id' attribute std::string const *const uses_name = socket_node->get_attribute_string_ptr("uses"); if (!uses_name) return error::INVALID_LAYOUT; // must have a 'uses' attribute" // locate the resource node util::xml::data_node const *resource_node = nullptr; for (util::xml::data_node const *this_resource_node = resources_node->get_first_child(); this_resource_node; this_resource_node = this_resource_node->get_next_sibling()) { std::string const *const resource_name = this_resource_node->get_attribute_string_ptr("id"); if (!resource_name) return error::INVALID_LAYOUT; // resource node must have an 'id' attribute if (*resource_name == *uses_name) { resource_node = this_resource_node; break; } } if (!resource_node) return error::INVALID_RESOURCE_REF; // *uses_name // process the resource if (!strcmp(resource_node->get_name(), "rom")) { std::error_condition err = file->add_rom_socket(std::string(*id), *resource_node); if (err) return err; } else if (!strcmp(resource_node->get_name(), "ram")) { if (!m_supports_ram) return error::UNSUPPORTED_RPK_FEATURE; // is not supported by this system std::error_condition err = file->add_ram_socket(std::string(*id), *resource_node); if (err) return err; } else return error::INVALID_LAYOUT; // resource node must be or } // and we're done! result = std::move(file); return std::error_condition(); } /*************************************************************************** RPK FILE ***************************************************************************/ //------------------------------------------------- // ctor //------------------------------------------------- rpk_file::rpk_file(util::archive_file::ptr &&zipfile, int pcb_type) : m_zipfile(std::move(zipfile)) , m_pcb_type(pcb_type) { } //------------------------------------------------- // dtor //------------------------------------------------- rpk_file::~rpk_file() { } //------------------------------------------------- // add_rom_socket //------------------------------------------------- std::error_condition rpk_file::add_rom_socket(std::string &&id, const util::xml::data_node &rom_resource_node) { // find the file attribute (required) std::string const *const file = rom_resource_node.get_attribute_string_ptr("file"); if (!file) return rpk_reader::error::INVALID_LAYOUT; // must have a 'file' attribute // check for crc (optional) std::optional hashes; std::string const *const crcstr = rom_resource_node.get_attribute_string_ptr("crc"); if (crcstr) { if (!hashes) hashes.emplace(); hashes->add_from_string(util::hash_collection::HASH_CRC, *crcstr); } // check for sha1 (optional) std::string const *const sha1 = rom_resource_node.get_attribute_string_ptr("sha1"); if (sha1) { if (!hashes.has_value()) hashes.emplace(); hashes->add_from_string(util::hash_collection::HASH_SHA1, *sha1); } // finally add the socket m_sockets.emplace_back(*this, std::move(id), rpk_socket::socket_type::ROM, std::string(*file), std::move(hashes)); return std::error_condition(); } //------------------------------------------------- // add_ram_socket //------------------------------------------------- std::error_condition rpk_file::add_ram_socket(std::string &&id, const util::xml::data_node &ram_resource_node) { // find the length attribute std::string const *const length_string = ram_resource_node.get_attribute_string_ptr("length"); if (!length_string) return rpk_reader::error::MISSING_RAM_LENGTH; // parse it unsigned int length; char suffix; switch (sscanf(length_string->c_str(), "%u%c", &length, &suffix)) { case 1: // fall through break; case 2: switch (tolower(suffix)) { case 'k': // kilobytes length *= 1024; break; case 'm': // megabytes length *= 1024 * 1024; break; default: // failed return rpk_reader::error::INVALID_RAM_SPEC; } break; default: return rpk_reader::error::INVALID_RAM_SPEC; } // determine the type of RAM rpk_socket::socket_type type; std::string const *const ram_type = ram_resource_node.get_attribute_string_ptr("type"); if (ram_type && *ram_type == "persistent") type = rpk_socket::socket_type::PERSISTENT_RAM; else type = rpk_socket::socket_type::RAM; // persistent RAM needs a file name std::string file; if (type == rpk_socket::socket_type::PERSISTENT_RAM) { std::string const *const ram_filename = ram_resource_node.get_attribute_string_ptr("file"); if (ram_filename == nullptr) return rpk_reader::error::INVALID_RAM_SPEC; // must have a 'file' attribute file = *ram_filename; } // finally add the socket m_sockets.emplace_back(*this, std::move(id), type, std::move(file), std::nullopt, length); return std::error_condition(); } /*************************************************************************** RPK SOCKET ***************************************************************************/ //------------------------------------------------- // ctor //------------------------------------------------- rpk_socket::rpk_socket(rpk_file &rpk, std::string &&id, socket_type type, std::string &&filename, std::optional &&hashes, std::uint32_t length) : m_rpk(rpk) , m_id(id) , m_type(type) , m_filename(filename) , m_hashes(std::move(hashes)) , m_length(length) { } //------------------------------------------------- // dtor //------------------------------------------------- rpk_socket::~rpk_socket() { } //------------------------------------------------- // read_file //------------------------------------------------- std::error_condition rpk_socket::read_file(std::vector &result) const { // find the file if (m_rpk.zipfile().search(m_filename, false) < 0) return rpk_reader::error::INVALID_FILE_REF; // prepare a buffer result.clear(); try { result.resize(m_rpk.zipfile().current_uncompressed_length()); } catch (std::bad_alloc const &) { return std::errc::not_enough_memory; } // read the file std::error_condition const ziperr = m_rpk.zipfile().decompress(&result[0], m_rpk.zipfile().current_uncompressed_length()); if (ziperr) return ziperr; // perform hash checks, if appropriate if (m_hashes.has_value()) { util::hash_collection actual_hashes; actual_hashes.compute(&result[0], result.size(), m_hashes->hash_types().c_str()); if (actual_hashes != m_hashes) return rpk_reader::error::INVALID_FILE_REF; // Hash check failed } // success! return std::error_condition(); } /*************************************************************************** RPK EXCEPTION HANDLING ***************************************************************************/ //------------------------------------------------- // rpk_category - gets the RPK error category instance //------------------------------------------------- std::error_category const &rpk_category() noexcept { return f_rpk_category_instance; } //------------------------------------------------- // error_message //------------------------------------------------- std::string rpk_category_impl::message(int condition) const { using namespace std::literals; std::string result; switch ( condition) { case 0: result = "No error"s; break; case (int)rpk_reader::error::XML_ERROR: result = "XML format error"s; break; case (int)rpk_reader::error::MISSING_RAM_LENGTH: result = "Missing RAM length"s; break; case (int)rpk_reader::error::INVALID_RAM_SPEC: result = "Invalid RAM specification"s; break; case (int)rpk_reader::error::INVALID_RESOURCE_REF: result = "Invalid resource reference"s; break; case (int)rpk_reader::error::INVALID_LAYOUT: result = "layout.xml not valid"s; break; case (int)rpk_reader::error::MISSING_LAYOUT: result = "Missing layout"s; break; case (int)rpk_reader::error::UNKNOWN_PCB_TYPE: result = "Unknown pcb type"s; break; case (int)rpk_reader::error::UNSUPPORTED_RPK_FEATURE: result = "RPK feature not supported"s; break; default: result = "Unknown error"s; break; } return result; }