// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" #include "BaseScriptStoreImpl.h" #include "Hasher.h" #include "MemoryMappedBuffer.h" #include // C++/WinRT #include // Standard Library #include namespace facebook { namespace react { namespace { class ByteArrayBuffer : public facebook::jsi::Buffer { public: size_t size() const override { return size_; } const uint8_t *data() const override { return byteArray_.get(); } uint8_t *data() { return byteArray_.get(); } ByteArrayBuffer(size_t size) : size_(size), byteArray_(std::make_unique(size)) {} ByteArrayBuffer(ByteArrayBuffer &&) = default; ByteArrayBuffer &operator=(ByteArrayBuffer &&) = default; private: ByteArrayBuffer(const ByteArrayBuffer &) = delete; ByteArrayBuffer &operator=(const ByteArrayBuffer &) = delete; size_t size_; std::unique_ptr byteArray_; }; class BufferViewBuffer : public facebook::jsi::Buffer { public: size_t size() const override { return size_; } const uint8_t *data() const override { return buffer_->data() + offset_; } uint8_t *data() { return const_cast(buffer_->data()) + offset_; } BufferViewBuffer(std::unique_ptr buffer, size_t offset, size_t size) : buffer_(std::move(buffer)), offset_(offset), size_(size) { if (size_ > buffer_->size() - offset) std::terminate(); } BufferViewBuffer(BufferViewBuffer &&) = default; BufferViewBuffer &operator=(BufferViewBuffer &&) = default; private: BufferViewBuffer(const BufferViewBuffer &) = delete; BufferViewBuffer &operator=(const BufferViewBuffer &) = delete; std::unique_ptr buffer_; size_t offset_; size_t size_; }; constexpr const char *PERSIST_MAGIC = "RNWPREP"; constexpr const char *PERSIST_EOF = "EOF"; int constexpr length__(const char *str) { return *str ? 1 + length__(str + 1) : 0; } // TODO : Enforce any packing policy ? struct PreparedScriptPrefix { // TODO :: constexpr initialize the array. char magic[length__(PERSIST_MAGIC)]; jsi::ScriptVersion_t scriptVersion; jsi::JSRuntimeVersion_t runtimeVersion; uint64_t sizeInBytes; std::uint8_t hash[32]; }; struct PreparedScriptSuffix { char eof[length__(PERSIST_EOF)]; }; } // namespace jsi::VersionedBuffer BaseScriptStoreImpl::getVersionedScript(const std::string &url) noexcept { std::ifstream file(url, std::ios::binary | std::ios::ate); if (!file) { return {nullptr, 0}; } std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); auto buffer = std::make_unique(static_cast(size)); if (!file.read(reinterpret_cast(buffer->data()), size)) { return {nullptr, 0}; } file.close(); return {std::move(buffer), versionProvider_ ? versionProvider_->getVersion(url) : static_cast(size)}; } jsi::ScriptVersion_t BaseScriptStoreImpl::getScriptVersion(const std::string &url) noexcept { if (versionProvider_) { return versionProvider_->getVersion(url); } else { std::ifstream file(url, std::ios::binary | std::ios::ate); if (!file) { return 0; } std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); return static_cast(size); } } std::unique_ptr LocalFileSimpleBufferStore::getBuffer(const std::string &bufferId) noexcept { // 1. Store path must be set // 2. It must be a directory that exists. TODO :: Figure out a cross platform // way to ensure this. // 3. Directory must end with the path delimiter. TODO :: Verify this. if (storeDirectory_.empty()) { std::terminate(); } if (Microsoft::React::GetRuntimeOptionBool("JSI.MemoryMappedScriptStore")) { try { return Microsoft::JSI::MakeMemoryMappedBuffer(winrt::to_hstring(storeDirectory_ + bufferId).c_str()); } catch (const facebook::jsi::JSINativeException &) { return nullptr; } } else { // Treat buffer id as the relative path fragment. std::ifstream file(winrt::to_hstring(storeDirectory_ + bufferId).c_str(), std::ios::binary | std::ios::ate); if (!file) { return nullptr; } std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); auto buffer = std::make_unique(static_cast(size)); if (!file.read(reinterpret_cast(buffer->data()), size)) { return nullptr; } return buffer; } } bool LocalFileSimpleBufferStore::persistBuffer( const std::string &relativeUrl, std::unique_ptr buffer) noexcept { // Assumptions on storeDirectory_ same as in getRawBuffer if (storeDirectory_.empty()) std::terminate(); std::ofstream file; file.open(winrt::to_hstring(storeDirectory_ + relativeUrl).c_str(), std::ios::binary | std::ios::trunc); if (!file) return false; file.write(reinterpret_cast(buffer->data()), buffer->size()); file.close(); return true; } std::string BasePreparedScriptStoreImpl::getPreparedScriptFileName( const jsi::ScriptSignature &scriptSignature, const jsi::JSRuntimeSignature &runtimeSignature, const char *prepareTag) { // Essentially, we are trying to construct, // prep___.cache std::string preparedScriptFileName("prep_"); const std::string &scriptUrl = scriptSignature.url; // As a crude heuristic we choose the last 64 characters of the source url. constexpr int MAXLENGTH = 64; preparedScriptFileName.append( scriptUrl.begin() + ((scriptUrl.size() < MAXLENGTH) ? 0 : (scriptUrl.size() - MAXLENGTH)), scriptUrl.end()); // Make a valid file name. std::replace(preparedScriptFileName.begin(), preparedScriptFileName.end(), '\\', '_'); std::replace(preparedScriptFileName.begin(), preparedScriptFileName.end(), '/', '_'); std::replace(preparedScriptFileName.begin(), preparedScriptFileName.end(), ':', '_'); std::replace(preparedScriptFileName.begin(), preparedScriptFileName.end(), '.', '_'); if (runtimeSignature.runtimeName.empty()) { std::terminate(); } preparedScriptFileName.append("_"); preparedScriptFileName.append(runtimeSignature.runtimeName); if (prepareTag) { preparedScriptFileName.append("_"); preparedScriptFileName.append(prepareTag); } // TODO :: Need to construct a hash. ref: // https://en.wikipedia.org/wiki/Base64#Filenames // extension preparedScriptFileName.append(".cache"); return preparedScriptFileName; } std::shared_ptr BasePreparedScriptStoreImpl::tryGetPreparedScript( const jsi::ScriptSignature &scriptSignature, const jsi::JSRuntimeSignature &runtimeSignature, const char *prepareTag) noexcept { std::string preparedScriptFilePath = getPreparedScriptFileName(scriptSignature, runtimeSignature, prepareTag); auto buffer = bufferStore_->getBuffer(preparedScriptFilePath); if (!buffer) { return nullptr; } if (buffer->size() < sizeof(PreparedScriptPrefix) + sizeof(PreparedScriptSuffix)) { // Too small to contain the prefix and suffix. The file is truncated or corrupted. return nullptr; } const PreparedScriptPrefix *prefix = reinterpret_cast(buffer->data()); if (strncmp(prefix->magic, PERSIST_MAGIC, sizeof(prefix->magic)) != 0) { // magic value doesn't match!! The store is very likely corrupted or belongs // to old version. return nullptr; } if (prefix->scriptVersion != scriptSignature.version) { // script version don't match!! Need to regenerate cache. return nullptr; } if (prefix->runtimeVersion != runtimeSignature.version) { // Runtime changed after the cache generation. return nullptr; } if (prefix->sizeInBytes != buffer->size() - sizeof(PreparedScriptPrefix) - sizeof(PreparedScriptSuffix)) { // Size is not as expected. Store is possibly corrupted .. It is safer to // bail out. return nullptr; } std::optional> hashBuffer = Microsoft::ReactNative::GetSHA256Hash( reinterpret_cast(buffer->data()) + sizeof(PreparedScriptPrefix), static_cast(prefix->sizeInBytes)); if (!hashBuffer) { // Hashing failed. return nullptr; } if (hashBuffer.value().size() < sizeof(prefix->hash)) { // Unexpected hash size. return nullptr; } if (memcmp(hashBuffer.value().data(), prefix->hash, sizeof(prefix->hash)) != 0) { // Hash doesn't match. Store is possibly corrupted. It is safer to bail out. return nullptr; } const PreparedScriptSuffix *suffix = reinterpret_cast( buffer->data() + sizeof(PreparedScriptPrefix) + prefix->sizeInBytes); if (strncmp(suffix->eof, PERSIST_EOF, sizeof(suffix->eof)) != 0) { // magic value doesn't match!! The store is very likely corrupted or belongs // to old version. return nullptr; } return std::make_shared( std::move(buffer), sizeof(PreparedScriptPrefix), static_cast(prefix->sizeInBytes)); } void BasePreparedScriptStoreImpl::persistPreparedScript( std::shared_ptr preparedScript, const jsi::ScriptSignature &scriptMetadata, const jsi::JSRuntimeSignature &runtimeMetadata, const char *prepareTag) noexcept { // TODO :: Unfortunately, The current abstraction is forcing us to make a // copy. Need to re-evaluate. auto newBuffer = std::make_unique( sizeof(PreparedScriptPrefix) + preparedScript->size() + sizeof(PreparedScriptSuffix)); PreparedScriptPrefix *prefix = reinterpret_cast(newBuffer->data()); memcpy_s(prefix->magic, sizeof(prefix->magic), PERSIST_MAGIC, sizeof(prefix->magic)); prefix->scriptVersion = scriptMetadata.version; prefix->runtimeVersion = runtimeMetadata.version; prefix->sizeInBytes = preparedScript->size(); std::optional> hashBuffer = Microsoft::ReactNative::GetSHA256Hash(preparedScript->data(), preparedScript->size()); if (!hashBuffer) { // Hashing failed. std::terminate(); } memcpy_s(prefix->hash, sizeof(prefix->hash), hashBuffer.value().data(), hashBuffer.value().size()); memcpy_s( newBuffer->data() + sizeof(PreparedScriptPrefix), newBuffer->size() - sizeof(PreparedScriptPrefix), preparedScript->data(), preparedScript->size()); PreparedScriptSuffix *suffix = reinterpret_cast( newBuffer->data() + sizeof(PreparedScriptPrefix) + preparedScript->size()); memcpy_s(suffix->eof, sizeof(suffix->eof), PERSIST_EOF, sizeof(suffix->eof)); std::string preparedScriptFilePath = getPreparedScriptFileName(scriptMetadata, runtimeMetadata, prepareTag); bufferStore_->persistBuffer(preparedScriptFilePath, std::move(newBuffer)); } } // namespace react } // namespace facebook