/** @file

  This file contains the implementation for a Platform Runtime Mechanism (PRM) configuration driver.

  Copyright (c) Microsoft Corporation
  Copyright (c) 2020, Intel Corporation. All rights reserved.<BR>
  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/DxeServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/UefiLib.h>

#include <PiDxe.h>
#include <PrmContextBuffer.h>
#include <PrmDataBuffer.h>
#include <PrmMmio.h>
#include <Protocol/PrmConfig.h>

#define _DBGMSGID_  "[PRMCONFIG]"

STATIC  UINTN  mMaxRuntimeMmioRangeCount;

GLOBAL_REMOVE_IF_UNREFERENCED STATIC  PRM_RUNTIME_MMIO_RANGES  **mRuntimeMmioRanges;

/**
  Converts the runtime memory range physical addresses to virtual addresses.

  @param[in]  RuntimeMmioRanges   A pointer to a PRM_RUNTIME_MMIO_RANGES buffer.

**/
VOID
ConvertRuntimeMemoryRangeAddresses (
  IN  PRM_RUNTIME_MMIO_RANGES  *RuntimeMmioRanges
  )
{
  UINTN  Index;

  if ((RuntimeMmioRanges == NULL) || (RuntimeMmioRanges->Count == 0)) {
    return;
  }

  for (Index = 0; Index < (UINTN)RuntimeMmioRanges->Count; Index++) {
    RuntimeMmioRanges->Range[Index].VirtualBaseAddress = RuntimeMmioRanges->Range[Index].PhysicalBaseAddress;
    gRT->ConvertPointer (0x0, (VOID **)&(RuntimeMmioRanges->Range[Index].VirtualBaseAddress));
  }
}

/**
  Sets the runtime memory range attributes.

  The EFI_MEMORY_RUNTIME attribute is set for each PRM_RUNTIME_MMIO_RANGE present
  in the buffer provided.

  @param[in]  RuntimeMmioRanges     A pointer to a PRM_RUNTIME_MMIO_RANGES buffer.

**/
VOID
SetRuntimeMemoryRangeAttributes (
  IN  PRM_RUNTIME_MMIO_RANGES  *RuntimeMmioRanges
  )
{
  EFI_STATUS                       Status;
  EFI_STATUS                       Status2;
  UINTN                            Index;
  EFI_GCD_MEMORY_SPACE_DESCRIPTOR  Descriptor;

  DEBUG ((DEBUG_INFO, "%a %a - Entry.\n", _DBGMSGID_, __func__));

  if ((RuntimeMmioRanges == NULL) || (RuntimeMmioRanges->Count == 0)) {
    return;
  }

  for (Index = 0; Index < (UINTN)RuntimeMmioRanges->Count; Index++) {
    DEBUG ((
      DEBUG_INFO,
      "      %a %a: Runtime MMIO Range [%d].\n",
      _DBGMSGID_,
      __func__,
      Index
      ));
    DEBUG ((
      DEBUG_INFO,
      "      %a %a: Physical address = 0x%016x. Length = 0x%x.\n",
      _DBGMSGID_,
      __func__,
      RuntimeMmioRanges->Range[Index].PhysicalBaseAddress,
      RuntimeMmioRanges->Range[Index].Length
      ));

    // Runtime memory ranges should cover ranges on a page boundary
    ASSERT ((RuntimeMmioRanges->Range[Index].PhysicalBaseAddress & EFI_PAGE_MASK) == 0);
    ASSERT ((RuntimeMmioRanges->Range[Index].Length & EFI_PAGE_MASK) == 0);

    Status2 = EFI_NOT_FOUND;
    Status  = gDS->GetMemorySpaceDescriptor (RuntimeMmioRanges->Range[Index].PhysicalBaseAddress, &Descriptor);
    if (!EFI_ERROR (Status) &&
        (
         ((Descriptor.GcdMemoryType != EfiGcdMemoryTypeMemoryMappedIo) && (Descriptor.GcdMemoryType != EfiGcdMemoryTypeReserved)) ||
         ((Descriptor.Length & EFI_PAGE_MASK) != 0)
        )
        )
    {
      Status2 =  gDS->RemoveMemorySpace (
                        RuntimeMmioRanges->Range[Index].PhysicalBaseAddress,
                        Descriptor.Length
                        );
    }

    if ((Status == EFI_NOT_FOUND) || !EFI_ERROR (Status2)) {
      Status = gDS->AddMemorySpace (
                      EfiGcdMemoryTypeMemoryMappedIo,
                      RuntimeMmioRanges->Range[Index].PhysicalBaseAddress,
                      (UINT64)RuntimeMmioRanges->Range[Index].Length,
                      EFI_MEMORY_UC | EFI_MEMORY_RUNTIME
                      );
      ASSERT_EFI_ERROR (Status);

      Status = gDS->AllocateMemorySpace (
                      EfiGcdAllocateAddress,
                      EfiGcdMemoryTypeMemoryMappedIo,
                      0,
                      (UINT64)RuntimeMmioRanges->Range[Index].Length,
                      &RuntimeMmioRanges->Range[Index].PhysicalBaseAddress,
                      gImageHandle,
                      NULL
                      );
      ASSERT_EFI_ERROR (Status);
    }

    Status = gDS->GetMemorySpaceDescriptor (RuntimeMmioRanges->Range[Index].PhysicalBaseAddress, &Descriptor);
    ASSERT_EFI_ERROR (Status);
    if (EFI_ERROR (Status)) {
      DEBUG ((
        DEBUG_ERROR,
        "      %a %a: Error [%r] finding descriptor for runtime memory range 0x%016x.\n",
        _DBGMSGID_,
        __func__,
        Status,
        RuntimeMmioRanges->Range[Index].PhysicalBaseAddress
        ));
      continue;
    }

    if ((Descriptor.Attributes & EFI_MEMORY_RUNTIME) != 0) {
      continue;
    }

    // The memory space descriptor access attributes are not accurate. Don't pass
    // in access attributes so SetMemorySpaceAttributes() doesn't update them.
    // EFI_MEMORY_RUNTIME is not a CPU arch attribute, so calling
    // SetMemorySpaceAttributes() with only it set will not clear existing page table
    // attributes for this region, such as EFI_MEMORY_XP
    Status = gDS->SetMemorySpaceAttributes (
                    RuntimeMmioRanges->Range[Index].PhysicalBaseAddress,
                    (UINT64)RuntimeMmioRanges->Range[Index].Length,
                    EFI_MEMORY_RUNTIME
                    );
    ASSERT_EFI_ERROR (Status);
    if (EFI_ERROR (Status)) {
      DEBUG ((
        DEBUG_ERROR,
        "      %a %a: Error [%r] setting descriptor for runtime memory range 0x%016x.\n",
        _DBGMSGID_,
        __func__,
        Status,
        RuntimeMmioRanges->Range[Index].PhysicalBaseAddress
        ));
    } else {
      DEBUG ((DEBUG_INFO, "      %a %a: Successfully set runtime attribute for the MMIO range.\n", _DBGMSGID_, __func__));
    }
  }
}

/**
  Stores pointers or pointer to resources that should be converted in the virtual address change event.

**/
VOID
StoreVirtualMemoryAddressChangePointers (
  VOID
  )
{
  EFI_STATUS           Status;
  UINTN                HandleCount;
  UINTN                HandleIndex;
  UINTN                RangeIndex;
  EFI_HANDLE           *HandleBuffer;
  PRM_CONFIG_PROTOCOL  *PrmConfigProtocol;

  DEBUG ((DEBUG_INFO, "%a %a - Entry.\n", _DBGMSGID_, __func__));

  RangeIndex = 0;

  mRuntimeMmioRanges = AllocateRuntimeZeroPool (sizeof (*mRuntimeMmioRanges) * mMaxRuntimeMmioRangeCount);
  if ((mRuntimeMmioRanges == NULL) && (mMaxRuntimeMmioRangeCount > 0)) {
    DEBUG ((
      DEBUG_ERROR,
      "  %a %a: Memory allocation for runtime MMIO pointer array failed.\n",
      _DBGMSGID_,
      __func__
      ));
    ASSERT (FALSE);
    return;
  }

  HandleBuffer = NULL;
  Status       = gBS->LocateHandleBuffer (
                        ByProtocol,
                        &gPrmConfigProtocolGuid,
                        NULL,
                        &HandleCount,
                        &HandleBuffer
                        );
  if (!EFI_ERROR (Status)) {
    for (HandleIndex = 0; HandleIndex < HandleCount; HandleIndex++) {
      Status = gBS->HandleProtocol (
                      HandleBuffer[HandleIndex],
                      &gPrmConfigProtocolGuid,
                      (VOID **)&PrmConfigProtocol
                      );
      ASSERT_EFI_ERROR (Status);
      if (EFI_ERROR (Status) || (PrmConfigProtocol == NULL)) {
        continue;
      }

      if (PrmConfigProtocol->ModuleContextBuffers.RuntimeMmioRanges != NULL) {
        if (RangeIndex >= mMaxRuntimeMmioRangeCount) {
          Status = EFI_BUFFER_TOO_SMALL;
          DEBUG ((
            DEBUG_ERROR,
            "  %a %a: Index out of bounds - Actual count (%d) of runtime MMIO ranges exceeds maximum count (%d).\n",
            _DBGMSGID_,
            __func__,
            RangeIndex + 1,
            mMaxRuntimeMmioRangeCount
            ));
          ASSERT_EFI_ERROR (Status);
          return;
        }

        mRuntimeMmioRanges[RangeIndex++] = PrmConfigProtocol->ModuleContextBuffers.RuntimeMmioRanges;
      }
    }

    DEBUG ((
      DEBUG_INFO,
      "  %a %a: %d MMIO ranges buffers saved for future virtual memory conversion.\n",
      _DBGMSGID_,
      __func__,
      RangeIndex
      ));
  }
}

/**
  Validates a data buffer for a PRM module.

  Verifies the buffer header signature is valid and the length meets the minimum size.

  @param[in]  PrmDataBuffer         A pointer to the data buffer for this PRM module.

  @retval EFI_SUCCESS               The data buffer was validated successfully.
  @retval EFI_INVALID_PARAMETER     The pointer given for PrmDataBuffer is NULL.
  @retval EFI_NOT_FOUND             The data buffer signature is not valid.
  @retval EFI_BUFFER_TOO_SMALL      The buffer size is too small.

**/
EFI_STATUS
ValidatePrmDataBuffer (
  IN  CONST PRM_DATA_BUFFER  *PrmDataBuffer
  )
{
  if (PrmDataBuffer == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  if (PrmDataBuffer->Header.Signature != PRM_DATA_BUFFER_HEADER_SIGNATURE) {
    DEBUG ((DEBUG_ERROR, "  %a %a: The PRM data buffer signature is invalid. PRM module.\n", _DBGMSGID_, __func__));
    return EFI_NOT_FOUND;
  }

  if (PrmDataBuffer->Header.Length < sizeof (PRM_DATA_BUFFER_HEADER)) {
    DEBUG ((DEBUG_ERROR, "  %a %a: The PRM data buffer length is invalid.\n", _DBGMSGID_, __func__));
    return EFI_BUFFER_TOO_SMALL;
  }

  return EFI_SUCCESS;
}

/**
  Validates a PRM context buffer.

  Verifies the buffer header signature is valid and the GUID is set to a non-zero value.

  @param[in]  PrmContextBuffer      A pointer to the context buffer for this PRM handler.

  @retval EFI_SUCCESS               The context buffer was validated successfully.
  @retval EFI_INVALID_PARAMETER     The pointer given for ContextBuffer is NULL.
  @retval EFI_NOT_FOUND             The proper value for a field was not found.

**/
EFI_STATUS
ValidatePrmContextBuffer (
  IN  CONST PRM_CONTEXT_BUFFER  *PrmContextBuffer
  )
{
  if (PrmContextBuffer == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  if (PrmContextBuffer->Signature != PRM_CONTEXT_BUFFER_SIGNATURE) {
    DEBUG ((DEBUG_ERROR, "  %a %a: The PRM context buffer signature is invalid.\n", _DBGMSGID_, __func__));
    return EFI_NOT_FOUND;
  }

  if (IsZeroGuid (&PrmContextBuffer->HandlerGuid)) {
    DEBUG ((DEBUG_ERROR, "  %a %a: The PRM context buffer GUID is zero.\n", _DBGMSGID_, __func__));
    return EFI_NOT_FOUND;
  }

  if ((PrmContextBuffer->StaticDataBuffer != NULL) && EFI_ERROR (ValidatePrmDataBuffer (PrmContextBuffer->StaticDataBuffer))) {
    DEBUG ((
      DEBUG_ERROR,
      "    %a %a: Error in static buffer for PRM handler %g.\n",
      _DBGMSGID_,
      __func__,
      &PrmContextBuffer->HandlerGuid
      ));
    return EFI_NOT_FOUND;
  }

  return EFI_SUCCESS;
}

/**
  Notification function of EVT_SIGNAL_VIRTUAL_ADDRESS_CHANGE.

  This is notification function converts any registered PRM_RUNTIME_MMIO_RANGE
  addresses to a virtual address.

  @param[in]  Event        Event whose notification function is being invoked.
  @param[in]  Context      Pointer to the notification function's context.

**/
VOID
EFIAPI
PrmConfigVirtualAddressChangeEvent (
  IN EFI_EVENT  Event,
  IN VOID       *Context
  )
{
  UINTN  Index;

  //
  // Convert runtime MMIO ranges
  //
  for (Index = 0; Index < mMaxRuntimeMmioRangeCount; Index++) {
    ConvertRuntimeMemoryRangeAddresses (mRuntimeMmioRanges[Index]);
  }
}

/**
  The PRM Config END_OF_DXE protocol notification event handler.

  Finds all of the PRM_CONFIG_PROTOCOL instances installed at end of DXE and
  marks all PRM_RUNTIME_MMIO_RANGE entries as EFI_MEMORY_RUNTIME.

  @param[in]  Event           Event whose notification function is being invoked.
  @param[in]  Context         The pointer to the notification function's context,
                              which is implementation-dependent.

**/
VOID
EFIAPI
PrmConfigEndOfDxeNotification (
  IN  EFI_EVENT  Event,
  IN  VOID       *Context
  )
{
  EFI_STATUS           Status;
  UINTN                HandleCount;
  UINTN                BufferIndex;
  UINTN                HandleIndex;
  EFI_HANDLE           *HandleBuffer;
  PRM_CONTEXT_BUFFER   *CurrentContextBuffer;
  PRM_CONFIG_PROTOCOL  *PrmConfigProtocol;

  DEBUG ((DEBUG_INFO, "%a %a - Entry.\n", _DBGMSGID_, __func__));

  HandleBuffer = NULL;
  Status       = gBS->LocateHandleBuffer (
                        ByProtocol,
                        &gPrmConfigProtocolGuid,
                        NULL,
                        &HandleCount,
                        &HandleBuffer
                        );
  if (!EFI_ERROR (Status)) {
    for (HandleIndex = 0; HandleIndex < HandleCount; HandleIndex++) {
      Status = gBS->HandleProtocol (
                      HandleBuffer[HandleIndex],
                      &gPrmConfigProtocolGuid,
                      (VOID **)&PrmConfigProtocol
                      );
      ASSERT_EFI_ERROR (Status);
      if (EFI_ERROR (Status) || (PrmConfigProtocol == NULL)) {
        continue;
      }

      DEBUG ((
        DEBUG_INFO,
        "  %a %a: Found PRM configuration protocol for PRM module %g.\n",
        _DBGMSGID_,
        __func__,
        &PrmConfigProtocol->ModuleContextBuffers.ModuleGuid
        ));

      DEBUG ((DEBUG_INFO, "      %a %a: Validating module context buffers...\n", _DBGMSGID_, __func__));
      for (BufferIndex = 0; BufferIndex < PrmConfigProtocol->ModuleContextBuffers.BufferCount; BufferIndex++) {
        CurrentContextBuffer = &(PrmConfigProtocol->ModuleContextBuffers.Buffer[BufferIndex]);

        Status =  ValidatePrmContextBuffer (CurrentContextBuffer);
        if (EFI_ERROR (Status)) {
          DEBUG ((
            DEBUG_ERROR,
            "        %a %a: Context buffer validation failed for PRM handler %g.\n",
            _DBGMSGID_,
            __func__,
            CurrentContextBuffer->HandlerGuid
            ));
        }
      }

      DEBUG ((DEBUG_INFO, "      %a %a: Module context buffer validation complete.\n", _DBGMSGID_, __func__));

      if (PrmConfigProtocol->ModuleContextBuffers.RuntimeMmioRanges != NULL) {
        DEBUG ((
          DEBUG_INFO,
          "    %a %a: Found %d PRM runtime MMIO ranges.\n",
          _DBGMSGID_,
          __func__,
          PrmConfigProtocol->ModuleContextBuffers.RuntimeMmioRanges->Count
          ));
        SetRuntimeMemoryRangeAttributes (PrmConfigProtocol->ModuleContextBuffers.RuntimeMmioRanges);
        mMaxRuntimeMmioRangeCount++;
      }
    }

    StoreVirtualMemoryAddressChangePointers ();
  }

  if (HandleBuffer != NULL) {
    gBS->FreePool (HandleBuffer);
  }

  gBS->CloseEvent (Event);
}

/**
  The entry point for this module.

  @param[in]  ImageHandle     The firmware allocated handle for the EFI image.
  @param[in]  SystemTable     A pointer to the EFI System Table.

  @retval EFI_SUCCESS         The entry point is executed successfully.
  @retval Others              An error occurred when executing this entry point.

**/
EFI_STATUS
EFIAPI
PrmConfigEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;
  EFI_EVENT   Event;

  DEBUG ((DEBUG_INFO, "%a %a - Entry.\n", _DBGMSGID_, __func__));

  //
  // Register a notification function to change memory attributes at end of DXE
  //
  Event  = NULL;
  Status = gBS->CreateEventEx (
                  EVT_NOTIFY_SIGNAL,
                  TPL_CALLBACK,
                  PrmConfigEndOfDxeNotification,
                  NULL,
                  &gEfiEndOfDxeEventGroupGuid,
                  &Event
                  );
  ASSERT_EFI_ERROR (Status);

  //
  // Register a notification function for virtual address change
  //
  Event  = NULL;
  Status = gBS->CreateEventEx (
                  EVT_NOTIFY_SIGNAL,
                  TPL_NOTIFY,
                  PrmConfigVirtualAddressChangeEvent,
                  NULL,
                  &gEfiEventVirtualAddressChangeGuid,
                  &Event
                  );
  ASSERT_EFI_ERROR (Status);

  return EFI_SUCCESS;
}
