diff --git a/binding-sandbox/binding-sandbox.cpp b/binding-sandbox/binding-sandbox.cpp index 5de1ea5d..2971666a 100644 --- a/binding-sandbox/binding-sandbox.cpp +++ b/binding-sandbox/binding-sandbox.cpp @@ -20,7 +20,8 @@ */ #include "binding-sandbox.h" -#include +#include "mkxp-polyfill.h" // std::strtol +#include #include #include #include "sharedstate.h" @@ -535,6 +536,26 @@ void sandbox_binding_init::operator()() { static VALUE system_module; static VALUE cfg_module; + struct register_ruby_revision : boost::asio::coroutine { + typedef decl_slots slots; + + void operator()() { + BOOST_ASIO_CORO_REENTER (this) { + SANDBOX_AWAIT_S(2, rb_intern, "RUBY_REVISION"); + SANDBOX_AWAIT_S(1, rb_const_get, sb()->rb_cObject(), SANDBOX_SLOT(2)); + SANDBOX_AWAIT_S(0, rb_string_value_cstr, &SANDBOX_SLOT(1)); + + struct sandbox_str_guard str = sb()->str(SANDBOX_SLOT(0)); + if (std::strlen(str) != 2 * sizeof mkxp_retro::ruby_revision) { + std::abort(); + } + for (size_t i = 0; i < sizeof mkxp_retro::ruby_revision; ++i) { + mkxp_retro::ruby_revision[i] = std::strtol(std::string(str + 2 * i, 2).c_str(), nullptr, 16); + } + } + } + }; + struct register_utf8_encoding : boost::asio::coroutine { typedef decl_slots slots; @@ -547,6 +568,7 @@ void sandbox_binding_init::operator()() { }; BOOST_ASIO_CORO_REENTER (this) { + SANDBOX_AWAIT(register_ruby_revision); SANDBOX_AWAIT(register_utf8_encoding); SANDBOX_AWAIT(exception_binding_init); diff --git a/binding-sandbox/hasher.cpp b/binding-sandbox/hasher.cpp new file mode 100644 index 00000000..cc314c9b --- /dev/null +++ b/binding-sandbox/hasher.cpp @@ -0,0 +1,70 @@ +/* +** hasher.cpp +** +** This file is part of mkxp. +** +** Copyright (C) 2013 - 2021 Amaryllis Kulla +** +** mkxp is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 2 of the License, or +** (at your option) any later version. +** +** mkxp is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with mkxp. If not, see . +*/ + +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + std::cerr << "[hasher] error: at least one argument must be passed to this program" << std::endl; + return 1; + } + + picosha2::hash256_one_by_one digest; + + for (int i = 2; i < argc; ++i) { + std::ifstream file(argv[i], std::ios::binary); + if (!file.is_open()) { + std::cerr << "[hasher] error: could not open input file " << argv[i] << std::endl; + return 2; + } + std::vector buffer; + for (;;) { + uint8_t byte = file.get(); + if (file.eof()) { + break; + } + buffer.push_back(byte); + } + digest.process(buffer.begin(), buffer.end()); + } + + digest.finish(); + std::vector bytes(picosha2::k_digest_size); + digest.get_hash_bytes(bytes.begin(), bytes.end()); + + std::ofstream file(argv[1]); + if (!file.is_open()) { + std::cerr << "[hasher] error: could not open output file " << argv[1] << std::endl; + return 3; + } + file << "#ifndef MKXPZ_BINDING_SANDBOX_HASH" << std::endl; + file << "# define MKXPZ_BINDING_SANDBOX_HASH \"" << std::hex; + for (uint8_t byte : bytes) { + file << "\\x" << (unsigned int)byte; + } + file << '"' << std::endl << "#endif" << std::endl; + + return 0; +} diff --git a/binding-sandbox/meson.build b/binding-sandbox/meson.build index f3e7d368..09bed76b 100644 --- a/binding-sandbox/meson.build +++ b/binding-sandbox/meson.build @@ -1,4 +1,4 @@ -global_sources += files( +binding_sandbox_sources = files( 'audio-binding.cpp', 'binding-base.cpp', 'binding-sandbox.cpp', @@ -22,3 +22,32 @@ global_sources += files( 'window-binding.cpp', 'windowvx-binding.cpp', ) + +global_sources += binding_sandbox_sources + +binding_sandbox_hasher = executable( + 'binding-sandbox-hasher', + dependencies: subproject('picosha2').get_variable('picosha2'), + sources: 'hasher.cpp', + native: true, + override_options: [ + 'buildtype=release', + 'b_coverage=false', + 'b_lto=false', + 'b_ndebug=false', + 'b_pgo=off', + 'b_sanitize=none', + 'cpp_std=c++11', + ], +) + +global_sources += custom_target( + 'binding-sandbox-hash', + input: binding_sandbox_sources, + output: 'binding-sandbox-hash.h', + command: [ + binding_sandbox_hasher, + '@OUTPUT@', + '@INPUT@', + ], +) diff --git a/src/core.cpp b/src/core.cpp index 69d10568..f7178c41 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -40,6 +40,7 @@ #include "mkxp-polyfill.h" // std::mutex, std::strtoul #include "git-hash.h" +#include "binding-sandbox-hash.h" #include "al-util.h" #include "audio.h" @@ -636,6 +637,8 @@ namespace mkxp_retro { uint8_t midi_chorus_override; uint8_t midi_reverb_override; + uint8_t ruby_revision[20]; + uint64_t get_ticks_ms() noexcept { return frame_time / 1000; } @@ -1622,6 +1625,19 @@ extern "C" RETRO_API bool retro_serialize(void *data, size_t len) { // Write 4-byte version: 1 if (!sandbox_serialize((uint32_t)1, data, max_size)) return false; + // Write mkxp-z version + if (!sandbox_serialize(MKXPZ_VERSION "/" MKXPZ_GIT_HASH, data, max_size)) return false; + + // Write 20-byte Ruby revision + RESERVE(sizeof ruby_revision); + std::memcpy(data, ruby_revision, sizeof ruby_revision); + ADVANCE(sizeof ruby_revision); + + // Write 32-byte hash of binding-sandbox source files + RESERVE(sizeof MKXPZ_BINDING_SANDBOX_HASH - 1); + std::memcpy(data, MKXPZ_BINDING_SANDBOX_HASH, sizeof MKXPZ_BINDING_SANDBOX_HASH - 1); + ADVANCE(sizeof MKXPZ_BINDING_SANDBOX_HASH - 1); + // Write the capacity of the VM memory if (!sandbox_serialize(sb()->memory_capacity(), data, max_size)) return false; @@ -1770,6 +1786,26 @@ extern "C" RETRO_API bool retro_unserialize(const void *data, size_t len) { if (version != 1) return false; } + // Read mkxp-z version that the save state was created by + std::string mkxpz_version; + if (!sandbox_deserialize(mkxpz_version, data, max_size)) return false; + + // Make sure the Ruby revision matches that of that version of mkxp-z, since save state compatibility breaks when the Ruby version changes + RESERVE(sizeof ruby_revision); + if (std::memcmp(data, ruby_revision, sizeof ruby_revision)) { + log_printf(RETRO_LOG_ERROR, "Failed to load save state because it uses a different Ruby version than the current version of mkxp-z; try using mkxp-z version %s to load this save state instead\n", mkxpz_version.c_str()); + return false; + } + ADVANCE(sizeof ruby_revision); + + // Make sure the hash of the binding-sandbox source files matches that of that version of mkxp-z, since save state compatibility breaks when the sandbox bindings are modified + RESERVE(sizeof MKXPZ_BINDING_SANDBOX_HASH - 1); + if (std::memcmp(data, MKXPZ_BINDING_SANDBOX_HASH, sizeof MKXPZ_BINDING_SANDBOX_HASH - 1)) { + log_printf(RETRO_LOG_ERROR, "Failed to load save state because the sandbox bindings used are incompatible with the current version of mkxp-z; try using mkxp-z version %s to load this save state instead\n", mkxpz_version.c_str()); + return false; + } + ADVANCE(sizeof MKXPZ_BINDING_SANDBOX_HASH - 1); + // Read the VM memory { wasm_size_t memory_capacity; diff --git a/src/core.h b/src/core.h index 6c5b81dc..f8deb975 100644 --- a/src/core.h +++ b/src/core.h @@ -50,6 +50,8 @@ namespace mkxp_retro { extern uint8_t midi_chorus_override; extern uint8_t midi_reverb_override; + extern uint8_t ruby_revision[20]; + uint64_t get_ticks_ms() noexcept; uint64_t get_ticks_us() noexcept; double get_refresh_rate() noexcept; diff --git a/subprojects/packagefiles/picosha2/meson.build b/subprojects/packagefiles/picosha2/meson.build new file mode 100644 index 00000000..9eb1de6b --- /dev/null +++ b/subprojects/packagefiles/picosha2/meson.build @@ -0,0 +1,2 @@ +project('picosha2', 'cpp', meson_version: '>=1.3.0') +picosha2 = declare_dependency(include_directories: '.') diff --git a/subprojects/picosha2.wrap b/subprojects/picosha2.wrap new file mode 100644 index 00000000..91ce8711 --- /dev/null +++ b/subprojects/picosha2.wrap @@ -0,0 +1,5 @@ +[wrap-git] +url = https://github.com/okdshin/PicoSHA2 +revision = v1.0.1 +depth = 1 +patch_directory = picosha2