// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdlib.h>
#include <string.h>

#include "src/api-inl.h"
#include "src/objects-inl.h"
#include "src/snapshot/code-serializer.h"
#include "src/version.h"
#include "src/wasm/module-decoder.h"
#include "src/wasm/wasm-engine.h"
#include "src/wasm/wasm-memory.h"
#include "src/wasm/wasm-module-builder.h"
#include "src/wasm/wasm-module.h"
#include "src/wasm/wasm-objects-inl.h"
#include "src/wasm/wasm-opcodes.h"

#include "test/cctest/cctest.h"
#include "test/common/wasm/flag-utils.h"
#include "test/common/wasm/test-signatures.h"
#include "test/common/wasm/wasm-macro-gen.h"
#include "test/common/wasm/wasm-module-runner.h"

namespace v8 {
namespace internal {
namespace wasm {
namespace test_wasm_serialization {

namespace {
void Cleanup(Isolate* isolate = CcTest::InitIsolateOnce()) {
  // By sending a low memory notifications, we will try hard to collect all
  // garbage and will therefore also invoke all weak callbacks of actually
  // unreachable persistent handles.
  reinterpret_cast<v8::Isolate*>(isolate)->LowMemoryNotification();
}

#define EMIT_CODE_WITH_END(f, code)  \
  do {                               \
    f->EmitCode(code, sizeof(code)); \
    f->Emit(kExprEnd);               \
  } while (false)

}  // namespace

// Approximate gtest TEST_F style, in case we adopt gtest.
class WasmSerializationTest {
 public:
  WasmSerializationTest() : zone_(&allocator_, ZONE_NAME) {
    // Don't call here if we move to gtest.
    SetUp();
  }

  static void BuildWireBytes(Zone* zone, ZoneBuffer* buffer) {
    WasmModuleBuilder* builder = new (zone) WasmModuleBuilder(zone);
    TestSignatures sigs;

    WasmFunctionBuilder* f = builder->AddFunction(sigs.i_i());
    byte code[] = {WASM_GET_LOCAL(0), kExprI32Const, 1, kExprI32Add};
    EMIT_CODE_WITH_END(f, code);
    builder->AddExport(CStrVector(kFunctionName), f);

    builder->WriteTo(*buffer);
  }

  void ClearSerializedData() { serialized_bytes_ = {nullptr, 0}; }

  void InvalidateVersion() {
    uint32_t* slot = reinterpret_cast<uint32_t*>(
        const_cast<uint8_t*>(serialized_bytes_.start) +
        SerializedCodeData::kVersionHashOffset);
    *slot = Version::Hash() + 1;
  }

  void InvalidateWireBytes() {
    memset(const_cast<uint8_t*>(wire_bytes_.start), 0, wire_bytes_.size / 2);
  }

  void InvalidateLength() {
    uint32_t* slot = reinterpret_cast<uint32_t*>(
        const_cast<uint8_t*>(serialized_bytes_.start) +
        SerializedCodeData::kPayloadLengthOffset);
    *slot = 0u;
  }

  v8::MaybeLocal<v8::WasmCompiledModule> Deserialize() {
    ErrorThrower thrower(current_isolate(), "");
    v8::MaybeLocal<v8::WasmCompiledModule> deserialized =
        v8::WasmCompiledModule::DeserializeOrCompile(
            current_isolate_v8(), serialized_bytes_, wire_bytes_);
    return deserialized;
  }

  void DeserializeAndRun() {
    ErrorThrower thrower(current_isolate(), "");
    v8::Local<v8::WasmCompiledModule> deserialized_module;
    CHECK(Deserialize().ToLocal(&deserialized_module));
    Handle<WasmModuleObject> module_object = Handle<WasmModuleObject>::cast(
        v8::Utils::OpenHandle(*deserialized_module));
    {
      DisallowHeapAllocation assume_no_gc;
      Vector<const byte> deserialized_module_wire_bytes =
          module_object->native_module()->wire_bytes();
      CHECK_EQ(deserialized_module_wire_bytes.size(), wire_bytes_.size);
      CHECK_EQ(memcmp(deserialized_module_wire_bytes.start(), wire_bytes_.start,
                      wire_bytes_.size),
               0);
    }
    Handle<WasmInstanceObject> instance =
        current_isolate()
            ->wasm_engine()
            ->SyncInstantiate(current_isolate(), &thrower, module_object,
                              Handle<JSReceiver>::null(),
                              MaybeHandle<JSArrayBuffer>())
            .ToHandleChecked();
    Handle<Object> params[1] = {
        Handle<Object>(Smi::FromInt(41), current_isolate())};
    int32_t result = testing::CallWasmFunctionForTesting(
        current_isolate(), instance, &thrower, kFunctionName, 1, params);
    CHECK_EQ(42, result);
  }

  Isolate* current_isolate() {
    return reinterpret_cast<Isolate*>(current_isolate_v8_);
  }

  ~WasmSerializationTest() {
    // Don't call from here if we move to gtest
    TearDown();
  }

  v8::Isolate* current_isolate_v8() { return current_isolate_v8_; }

 private:
  static const char* kFunctionName;

  Zone* zone() { return &zone_; }

  void SetUp() {
    ZoneBuffer buffer(&zone_);
    WasmSerializationTest::BuildWireBytes(zone(), &buffer);

    Isolate* serialization_isolate = CcTest::InitIsolateOnce();
    ErrorThrower thrower(serialization_isolate, "");
    {
      HandleScope scope(serialization_isolate);
      testing::SetupIsolateForWasmModule(serialization_isolate);

      auto enabled_features = WasmFeaturesFromIsolate(serialization_isolate);
      MaybeHandle<WasmModuleObject> maybe_module_object =
          serialization_isolate->wasm_engine()->SyncCompile(
              serialization_isolate, enabled_features, &thrower,
              ModuleWireBytes(buffer.begin(), buffer.end()));
      Handle<WasmModuleObject> module_object =
          maybe_module_object.ToHandleChecked();

      v8::Local<v8::Object> v8_module_obj =
          v8::Utils::ToLocal(Handle<JSObject>::cast(module_object));
      CHECK(v8_module_obj->IsWebAssemblyCompiledModule());

      v8::Local<v8::WasmCompiledModule> v8_compiled_module =
          v8_module_obj.As<v8::WasmCompiledModule>();
      v8::WasmCompiledModule::BufferReference uncompiled_bytes =
          v8_compiled_module->GetWasmWireBytesRef();
      uint8_t* bytes_copy = zone()->NewArray<uint8_t>(uncompiled_bytes.size);
      memcpy(bytes_copy, uncompiled_bytes.start, uncompiled_bytes.size);
      wire_bytes_ = {bytes_copy, uncompiled_bytes.size};
      // keep alive data_ until the end
      data_ = v8_compiled_module->Serialize();
    }

    serialized_bytes_ = {data_.first.get(), data_.second};

    v8::Isolate::CreateParams create_params;
    create_params.array_buffer_allocator =
        serialization_isolate->array_buffer_allocator();

    current_isolate_v8_ = v8::Isolate::New(create_params);
    v8::HandleScope new_scope(current_isolate_v8());
    v8::Local<v8::Context> deserialization_context =
        v8::Context::New(current_isolate_v8());
    deserialization_context->Enter();
    testing::SetupIsolateForWasmModule(current_isolate());
  }

  void TearDown() {
    current_isolate_v8()->Dispose();
    current_isolate_v8_ = nullptr;
  }

  v8::internal::AccountingAllocator allocator_;
  Zone zone_;
  v8::WasmCompiledModule::SerializedModule data_;
  v8::WasmCompiledModule::BufferReference wire_bytes_ = {nullptr, 0};
  v8::WasmCompiledModule::BufferReference serialized_bytes_ = {nullptr, 0};
  v8::Isolate* current_isolate_v8_;
};

const char* WasmSerializationTest::kFunctionName = "increment";

TEST(DeserializeValidModule) {
  WasmSerializationTest test;
  {
    HandleScope scope(test.current_isolate());
    test.DeserializeAndRun();
  }
  Cleanup(test.current_isolate());
  Cleanup();
}

TEST(DeserializeMismatchingVersion) {
  WasmSerializationTest test;
  {
    HandleScope scope(test.current_isolate());
    test.InvalidateVersion();
    test.DeserializeAndRun();
  }
  Cleanup(test.current_isolate());
  Cleanup();
}

TEST(DeserializeNoSerializedData) {
  WasmSerializationTest test;
  {
    HandleScope scope(test.current_isolate());
    test.ClearSerializedData();
    test.DeserializeAndRun();
  }
  Cleanup(test.current_isolate());
  Cleanup();
}

TEST(DeserializeInvalidLength) {
  WasmSerializationTest test;
  {
    HandleScope scope(test.current_isolate());
    test.InvalidateLength();
    test.DeserializeAndRun();
  }
  Cleanup(test.current_isolate());
  Cleanup();
}

TEST(DeserializeWireBytesAndSerializedDataInvalid) {
  WasmSerializationTest test;
  {
    HandleScope scope(test.current_isolate());
    test.InvalidateVersion();
    test.InvalidateWireBytes();
    test.Deserialize();
  }
  Cleanup(test.current_isolate());
  Cleanup();
}

bool False(v8::Local<v8::Context> context, v8::Local<v8::String> source) {
  return false;
}

TEST(BlockWasmCodeGenAtDeserialization) {
  WasmSerializationTest test;
  {
    HandleScope scope(test.current_isolate());
    test.current_isolate_v8()->SetAllowCodeGenerationFromStringsCallback(False);
    v8::MaybeLocal<v8::WasmCompiledModule> nothing = test.Deserialize();
    CHECK(nothing.IsEmpty());
  }
  Cleanup(test.current_isolate());
  Cleanup();
}

namespace {

void TestTransferrableWasmModules(bool should_share) {
  i::wasm::WasmEngine::InitializeOncePerProcess();
  v8::internal::AccountingAllocator allocator;
  Zone zone(&allocator, ZONE_NAME);

  ZoneBuffer buffer(&zone);
  WasmSerializationTest::BuildWireBytes(&zone, &buffer);

  v8::Isolate::CreateParams create_params;
  create_params.array_buffer_allocator = CcTest::array_buffer_allocator();
  v8::Isolate* from_isolate = v8::Isolate::New(create_params);
  std::vector<v8::WasmCompiledModule::TransferrableModule> store;
  std::shared_ptr<NativeModule> original_native_module;
  {
    v8::HandleScope scope(from_isolate);
    LocalContext env(from_isolate);

    Isolate* from_i_isolate = reinterpret_cast<Isolate*>(from_isolate);
    testing::SetupIsolateForWasmModule(from_i_isolate);
    ErrorThrower thrower(from_i_isolate, "TestTransferrableWasmModules");
    auto enabled_features = WasmFeaturesFromIsolate(from_i_isolate);
    MaybeHandle<WasmModuleObject> maybe_module_object =
        from_i_isolate->wasm_engine()->SyncCompile(
            from_i_isolate, enabled_features, &thrower,
            ModuleWireBytes(buffer.begin(), buffer.end()));
    Handle<WasmModuleObject> module_object =
        maybe_module_object.ToHandleChecked();
    v8::Local<v8::WasmCompiledModule> v8_module =
        v8::Local<v8::WasmCompiledModule>::Cast(
            v8::Utils::ToLocal(Handle<JSObject>::cast(module_object)));
    store.push_back(v8_module->GetTransferrableModule());
    original_native_module = module_object->managed_native_module()->get();
  }

  {
    v8::Isolate* to_isolate = v8::Isolate::New(create_params);
    {
      v8::HandleScope scope(to_isolate);
      LocalContext env(to_isolate);

      v8::MaybeLocal<v8::WasmCompiledModule> transferred_module =
          v8::WasmCompiledModule::FromTransferrableModule(to_isolate, store[0]);
      CHECK(!transferred_module.IsEmpty());
      Handle<WasmModuleObject> module_object = Handle<WasmModuleObject>::cast(
          v8::Utils::OpenHandle(*transferred_module.ToLocalChecked()));
      std::shared_ptr<NativeModule> transferred_native_module =
          module_object->managed_native_module()->get();
      bool is_sharing = (original_native_module == transferred_native_module);
      CHECK_EQ(should_share, is_sharing);
    }
    to_isolate->Dispose();
  }
  original_native_module.reset();
  from_isolate->Dispose();
}

}  // namespace

UNINITIALIZED_TEST(TransferrableWasmModulesCloned) {
  FlagScope<bool> flag_scope_code(&FLAG_wasm_shared_code, false);
  TestTransferrableWasmModules(false);
}

UNINITIALIZED_TEST(TransferrableWasmModulesShared) {
  FlagScope<bool> flag_scope_engine(&FLAG_wasm_shared_engine, true);
  FlagScope<bool> flag_scope_code(&FLAG_wasm_shared_code, true);
  TestTransferrableWasmModules(true);
}

#undef EMIT_CODE_WITH_END

}  // namespace test_wasm_serialization
}  // namespace wasm
}  // namespace internal
}  // namespace v8
