/** @file
  This module produce main entry for BDS phase - BdsEntry.
  When this module was dispatched by DxeCore, gEfiBdsArchProtocolGuid will be installed
  which contains interface of BdsEntry.
  After DxeCore finish DXE phase, gEfiBdsArchProtocolGuid->BdsEntry will be invoked
  to enter BDS phase.

Copyright (c) 2004 - 2019, Intel Corporation. All rights reserved.<BR>
(C) Copyright 2016-2019 Hewlett Packard Enterprise Development LP<BR>
(C) Copyright 2015 Hewlett-Packard Development Company, L.P.<BR>
SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include "Bds.h"

#include <Guid/GlobalVariable.h>
#include <Guid/EventGroup.h>
#include <FlashLayout.h>

#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/DevicePathLib.h>
#include <Library/DuetBdsLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PcdLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/UefiLib.h>

#include <Protocol/Capsule.h>
#include <Protocol/BlockIo.h>
#include <Protocol/VariableLock.h>

///
/// BDS arch protocol instance initial value.
///
EFI_HANDLE  gBdsHandle = NULL;

EFI_BDS_ARCH_PROTOCOL  gBds = {
  BdsEntry
};

///
/// The read-only variables defined in UEFI Spec.
///
CHAR16  *mReadOnlyVariables[] = {
  EFI_PLATFORM_LANG_CODES_VARIABLE_NAME
  EFI_LANG_CODES_VARIABLE_NAME,
  EFI_BOOT_OPTION_SUPPORT_VARIABLE_NAME,
  EFI_HW_ERR_REC_SUPPORT_VARIABLE_NAME,
  EFI_OS_INDICATIONS_SUPPORT_VARIABLE_NAME
};

/**

  Install Boot Device Selection Protocol

  @param ImageHandle     The image handle.
  @param SystemTable     The system table.

  @retval  EFI_SUCEESS  BDS has finished initializing.
                        Return the dispatcher and recall BDS.Entry
  @retval  Other        Return status from AllocatePool() or gBS->InstallProtocolInterface

**/
EFI_STATUS
EFIAPI
BdsInitialize (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;
  VOID        *ReturnUnsupported;

 #ifdef MDE_CPU_X64
  STATIC UINT8  mReturnUnsupported[] = { 0x48, 0xB8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC3 };
 #else
  STATIC UINT8  mReturnUnsupported[] = { 0xB8, 0x03, 0x00, 0x00, 0x80, 0xC3 };
 #endif

  //
  // Provide dummy functions
  //
  Status = gBS->AllocatePool (EfiRuntimeServicesCode, sizeof (mReturnUnsupported), (VOID **)&ReturnUnsupported);
  ASSERT_EFI_ERROR (Status);
  CopyMem (ReturnUnsupported, mReturnUnsupported, sizeof (mReturnUnsupported));
  gRT->UpdateCapsule            = (EFI_UPDATE_CAPSULE)ReturnUnsupported;
  gRT->QueryCapsuleCapabilities = (EFI_QUERY_CAPSULE_CAPABILITIES)ReturnUnsupported;

  //
  // Install protocol interface
  //
  Status = gBS->InstallMultipleProtocolInterfaces (
                  &gBdsHandle,
                  &gEfiBdsArchProtocolGuid,
                  &gBds,
                  &gEfiCapsuleArchProtocolGuid,
                  NULL,
                  NULL
                  );
  ASSERT_EFI_ERROR (Status);

  return Status;
}

STATIC
EFI_STATUS
BdsCheckSignature (
  IN EFI_HANDLE  Handle
  )
{
  EFI_STATUS             Status;
  volatile BOOT1_LOADER  *SelfSignature;
  BOOT1_LOADER           *DiskSignature;
  UINTN                  DiskSignatureSize;
  EFI_BLOCK_IO_PROTOCOL  *BlockIo;
  UINTN                  Index;
  UINT8                  NonZero;

  SelfSignature = (volatile BOOT1_LOADER *)(BOOT1_BASE);

  if (SelfSignature->Magic != BOOT1_MAGIC) {
    return EFI_UNSUPPORTED;
  }

  Status = gBS->HandleProtocol (
                  Handle,
                  &gEfiBlockIoProtocolGuid,
                  (VOID **)&BlockIo
                  );

  if (EFI_ERROR (Status)) {
    return Status;
  }

  DiskSignatureSize = ALIGN_VALUE (
                        MAX (sizeof (*DiskSignature), BlockIo->Media->BlockSize),
                        BlockIo->Media->BlockSize
                        );
  DiskSignature = AllocatePool (DiskSignatureSize);

  if (DiskSignature == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  Status = BlockIo->ReadBlocks (
                      BlockIo,
                      BlockIo->Media->MediaId,
                      0,
                      DiskSignatureSize,
                      DiskSignature
                      );

  if (!EFI_ERROR (Status) && (DiskSignature->Magic == SelfSignature->Magic)) {
    NonZero = 0;

    for (Index = 0; Index < sizeof (SelfSignature->Signature); ++Index) {
      if (SelfSignature->Signature[Index] != DiskSignature->Signature[Index]) {
        Status = EFI_NOT_FOUND;
        break;
      }

      NonZero |= SelfSignature->Signature[Index];
    }

    if (NonZero == 0) {
      Status = EFI_NOT_FOUND;
    }
  } else {
    Status = EFI_UNSUPPORTED;
  }

  FreePool (DiskSignature);

  return Status;
}

/**

  This function attempts to boot for the boot order specified
  by platform policy.

**/
STATIC
VOID
BdsBootDeviceSelect (
  IN BOOLEAN  RequireValidDisk
  )
{
  EFI_STATUS                Status;
  UINTN                     Index;
  EFI_HANDLE                *FileSystemHandles;
  UINTN                     NumberFileSystemHandles;
  EFI_DEVICE_PATH_PROTOCOL  *DevicePath;
  EFI_HANDLE                ImageHandle;

  //
  // If there is simple file protocol which does not consume block Io protocol, create a boot option for it here.
  //
  Status = gBS->LocateHandleBuffer (
                  ByProtocol,
                  &gEfiSimpleFileSystemProtocolGuid,
                  NULL,
                  &NumberFileSystemHandles,
                  &FileSystemHandles
                  );

  if (EFI_ERROR (Status)) {
    return;
  }

  for (Index = 0; Index < NumberFileSystemHandles; Index++) {
    //
    // Check matching volume DuetPkg was booted from.
    //
    if (RequireValidDisk) {
      Status = BdsCheckSignature (FileSystemHandles[Index]);
      if (EFI_ERROR (Status)) {
        continue;
      }
    }

    //
    // Do the removable Media thing. \EFI\BOOT\boot{machinename}.EFI
    //  machinename is ia32, ia64, x64, ...
    //
    DevicePath = FileDevicePath (
                   FileSystemHandles[Index],
                   L"\\EFI\\OC\\OpenCore.efi"
                   );

    if (DevicePath == NULL) {
      continue;
    }

    ImageHandle = NULL;
    Status      = gBS->LoadImage (
                         TRUE,
                         gImageHandle,
                         DevicePath,
                         NULL,
                         0,
                         &ImageHandle
                         );

    if (!EFI_ERROR (Status)) {
      gBS->StartImage (
             ImageHandle,
             0,
             NULL
             );
    }

    FreePool (DevicePath);
  }

  if (NumberFileSystemHandles != 0) {
    FreePool (FileSystemHandles);
  }
}

/**

  Validate input console variable data.

  If found the device path is not a valid device path, remove the variable.

  @param VariableName             Input console variable name.

**/
STATIC
VOID
BdsFormalizeConsoleVariable (
  IN  CHAR16  *VariableName
  )
{
  EFI_DEVICE_PATH_PROTOCOL  *DevicePath;
  UINTN                     VariableSize;
  EFI_STATUS                Status;

  GetEfiGlobalVariable2 (VariableName, (VOID **)&DevicePath, &VariableSize);
  if ((DevicePath != NULL) && !IsDevicePathValid (DevicePath, VariableSize)) {
    Status = gRT->SetVariable (
                    VariableName,
                    &gEfiGlobalVariableGuid,
                    EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS | EFI_VARIABLE_NON_VOLATILE,
                    0,
                    NULL
                    );
    //
    // Deleting variable with current variable implementation shouldn't fail.
    //
    ASSERT_EFI_ERROR (Status);
  }

  if (DevicePath != NULL) {
    FreePool (DevicePath);
  }
}

/**

  Formalize Bds global variables.

 1. For ConIn/ConOut/ConErr, if found the device path is not a valid device path, remove the variable.
 2. For OsIndicationsSupported, Create a BS/RT/UINT64 variable to report caps
 3. Delete OsIndications variable if it is not NV/BS/RT UINT64
 Item 3 is used to solve case when OS corrupts OsIndications. Here simply delete this NV variable.

**/
STATIC
VOID
BdsFormalizeEfiGlobalVariable (
  VOID
  )
{
  UINT64  OsIndicationSupport;

  //
  // Validate Console variable.
  //
  BdsFormalizeConsoleVariable (EFI_CON_IN_VARIABLE_NAME);
  BdsFormalizeConsoleVariable (EFI_CON_OUT_VARIABLE_NAME);
  BdsFormalizeConsoleVariable (EFI_ERR_OUT_VARIABLE_NAME);

  //
  // OS indicater support variable
  //
  OsIndicationSupport = EFI_OS_INDICATIONS_BOOT_TO_FW_UI \
                        | EFI_OS_INDICATIONS_FMP_CAPSULE_SUPPORTED;

  gRT->SetVariable (
         EFI_OS_INDICATIONS_SUPPORT_VARIABLE_NAME,
         &gEfiGlobalVariableGuid,
         EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS,
         sizeof (UINT64),
         &OsIndicationSupport
         );
}

/**
  Set language related EFI Variables.

**/
STATIC
VOID
InitializeLanguage (
  VOID
  )
{
  CHAR8  *PlatformLangCodes;
  CHAR8  *PlatformLang;

  PlatformLangCodes = (CHAR8 *)PcdGetPtr (PcdUefiVariableDefaultPlatformLangCodes);
  PlatformLang      = (CHAR8 *)PcdGetPtr (PcdUefiVariableDefaultPlatformLang);

  gRT->SetVariable (
         L"PlatformLangCodes",
         &gEfiGlobalVariableGuid,
         EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS,
         AsciiStrSize (PlatformLangCodes),
         PlatformLangCodes
         );

  gRT->SetVariable (
         L"PlatformLang",
         &gEfiGlobalVariableGuid,
         EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS | EFI_VARIABLE_NON_VOLATILE,
         AsciiStrSize (PlatformLang),
         PlatformLang
         );
}

/**

  Service routine for BdsInstance->Entry(). Devices are connected, the
  consoles are initialized, and the boot options are tried.

  @param This             Protocol Instance structure.

**/
VOID
EFIAPI
BdsEntry (
  IN EFI_BDS_ARCH_PROTOCOL  *This
  )
{
  CHAR16                        *FirmwareVendor;
  EFI_STATUS                    Status;
  UINT16                        BootTimeOut;
  UINTN                         Index;
  EDKII_VARIABLE_LOCK_PROTOCOL  *VariableLock;

  //
  // Fill in FirmwareVendor and FirmwareRevision from PCDs
  //
  FirmwareVendor      = (CHAR16 *)PcdGetPtr (PcdFirmwareVendor);
  gST->FirmwareVendor = AllocateRuntimeCopyPool (StrSize (FirmwareVendor), FirmwareVendor);
  ASSERT (gST->FirmwareVendor != NULL);
  gST->FirmwareRevision = PcdGet32 (PcdFirmwareRevision);

  //
  // Fixup Table CRC after we updated Firmware Vendor and Revision
  //
  gST->Hdr.CRC32 = 0;
  gBS->CalculateCrc32 ((VOID *)gST, sizeof (EFI_SYSTEM_TABLE), &gST->Hdr.CRC32);

  //
  // Validate Variable.
  // TODO: Explore.
  //
  BdsFormalizeEfiGlobalVariable ();

  //
  // Mark the read-only variables if the Variable Lock protocol exists
  //
  Status = gBS->LocateProtocol (&gEdkiiVariableLockProtocolGuid, NULL, (VOID **)&VariableLock);
  DEBUG ((EFI_D_INFO, "[BdsDxe] Locate Variable Lock protocol - %r\n", Status));
  if (!EFI_ERROR (Status)) {
    for (Index = 0; Index < ARRAY_SIZE (mReadOnlyVariables); Index++) {
      Status = VariableLock->RequestToLock (VariableLock, mReadOnlyVariables[Index], &gEfiGlobalVariableGuid);
      ASSERT_EFI_ERROR (Status);
    }
  }

  //
  // Initialize L"Timeout" EFI global variable.
  //
  BootTimeOut = 0;
  gRT->SetVariable (
         L"Timeout",
         &gEfiGlobalVariableGuid,
         EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS | EFI_VARIABLE_NON_VOLATILE,
         sizeof (UINT16),
         &BootTimeOut
         );

  //
  // Platform specific code
  // Initialize the platform specific string and language
  //
  InitializeLanguage ();

  //
  // Do the platform init, can be customized by OEM/IBV
  //
  PlatformBdsInit ();

  //
  // Signal EndOfDxe
  //
  EfiEventGroupSignal (&gEfiEndOfDxeEventGroupGuid);

  //
  // Setup some platform policy here
  //
  PlatformBdsPolicyBehavior ();

  //
  // Signal the EVT_SIGNAL_READY_TO_BOOT event
  //
  EfiSignalEventReadyToBoot ();

  //
  // BDS select the boot device to load OS
  //
  BdsBootDeviceSelect (TRUE);

  //
  // Try to boot any volume
  //
  gST->ConOut->OutputString (gST->ConOut, L"BOOT MISMATCH!\r\n");
  gBS->Stall (3000000);
  BdsBootDeviceSelect (FALSE);

  //
  // Abort with error.
  //
  gST->ConOut->OutputString (gST->ConOut, L"BOOT FAIL!\r\n");
  gBS->Stall (3000000);
  CpuDeadLoop ();

  //
  // Only assert here since this is the right behavior, we should never
  // return back to DxeCore.
  //
  ASSERT (FALSE);
}
