/** @file
*
*  Copyright (c) 2013, ARM Limited. All rights reserved.
*  Copyright (c) 2017, Intel Corporation. All rights reserved.<BR>
*
*  SPDX-License-Identifier: BSD-2-Clause-Patent
*
**/

#include "CpuDxe.h"

/**
  Searches memory descriptors covered by given memory range.

  This function searches into the Gcd Memory Space for descriptors
  (from StartIndex to EndIndex) that contains the memory range
  specified by BaseAddress and Length.

  @param  MemorySpaceMap       Gcd Memory Space Map as array.
  @param  NumberOfDescriptors  Number of descriptors in map.
  @param  BaseAddress          BaseAddress for the requested range.
  @param  Length               Length for the requested range.
  @param  StartIndex           Start index into the Gcd Memory Space Map.
  @param  EndIndex             End index into the Gcd Memory Space Map.

  @retval EFI_SUCCESS          Search successfully.
  @retval EFI_NOT_FOUND        The requested descriptors does not exist.

**/
STATIC
EFI_STATUS
SearchGcdMemorySpaces (
  IN EFI_GCD_MEMORY_SPACE_DESCRIPTOR  *MemorySpaceMap,
  IN UINTN                            NumberOfDescriptors,
  IN EFI_PHYSICAL_ADDRESS             BaseAddress,
  IN UINT64                           Length,
  OUT UINTN                           *StartIndex,
  OUT UINTN                           *EndIndex
  )
{
  UINTN  Index;

  *StartIndex = 0;
  *EndIndex   = 0;
  for (Index = 0; Index < NumberOfDescriptors; Index++) {
    if ((BaseAddress >= MemorySpaceMap[Index].BaseAddress) &&
        (BaseAddress < (MemorySpaceMap[Index].BaseAddress + MemorySpaceMap[Index].Length)))
    {
      *StartIndex = Index;
    }

    if (((BaseAddress + Length - 1) >= MemorySpaceMap[Index].BaseAddress) &&
        ((BaseAddress + Length - 1) < (MemorySpaceMap[Index].BaseAddress + MemorySpaceMap[Index].Length)))
    {
      *EndIndex = Index;
      return EFI_SUCCESS;
    }
  }

  return EFI_NOT_FOUND;
}

/**
  Sets the attributes for a specified range in Gcd Memory Space Map.

  This function sets the attributes for a specified range in
  Gcd Memory Space Map.

  @param  MemorySpaceMap       Gcd Memory Space Map as array
  @param  NumberOfDescriptors  Number of descriptors in map
  @param  BaseAddress          BaseAddress for the range
  @param  Length               Length for the range
  @param  Attributes           Attributes to set

  @retval EFI_SUCCESS          Memory attributes set successfully
  @retval EFI_NOT_FOUND        The specified range does not exist in Gcd Memory Space

**/
EFI_STATUS
SetGcdMemorySpaceAttributes (
  IN EFI_GCD_MEMORY_SPACE_DESCRIPTOR  *MemorySpaceMap,
  IN UINTN                            NumberOfDescriptors,
  IN EFI_PHYSICAL_ADDRESS             BaseAddress,
  IN UINT64                           Length,
  IN UINT64                           Attributes
  )
{
  EFI_STATUS            Status;
  UINTN                 Index;
  UINTN                 StartIndex;
  UINTN                 EndIndex;
  EFI_PHYSICAL_ADDRESS  RegionStart;
  UINT64                RegionLength;
  UINT64                Capabilities;

  DEBUG ((
    DEBUG_GCD,
    "SetGcdMemorySpaceAttributes[0x%lX; 0x%lX] = 0x%lX\n",
    BaseAddress,
    BaseAddress + Length,
    Attributes
    ));

  // We do not support a smaller granularity than 4KB on ARM Architecture
  if ((Length & EFI_PAGE_MASK) != 0) {
    DEBUG ((
      DEBUG_WARN,
      "Warning: We do not support smaller granularity than 4KB on ARM Architecture (passed length: 0x%lX).\n",
      Length
      ));
  }

  //
  // Get all memory descriptors covered by the memory range
  //
  Status = SearchGcdMemorySpaces (
             MemorySpaceMap,
             NumberOfDescriptors,
             BaseAddress,
             Length,
             &StartIndex,
             &EndIndex
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Go through all related descriptors and set attributes accordingly
  //
  for (Index = StartIndex; Index <= EndIndex; Index++) {
    if (MemorySpaceMap[Index].GcdMemoryType == EfiGcdMemoryTypeNonExistent) {
      continue;
    }

    //
    // Calculate the start and end address of the overlapping range
    //
    if (BaseAddress >= MemorySpaceMap[Index].BaseAddress) {
      RegionStart = BaseAddress;
    } else {
      RegionStart = MemorySpaceMap[Index].BaseAddress;
    }

    if ((BaseAddress + Length - 1) < (MemorySpaceMap[Index].BaseAddress + MemorySpaceMap[Index].Length)) {
      RegionLength = BaseAddress + Length - RegionStart;
    } else {
      RegionLength = MemorySpaceMap[Index].BaseAddress + MemorySpaceMap[Index].Length - RegionStart;
    }

    // Always add RO, RP, and XP, as all memory is capable of supporting these types (they are software constructs,
    // not hardware features) and they are critical to maintaining a security boundary.
    Capabilities = MemorySpaceMap[Index].Capabilities | EFI_MEMORY_RO | EFI_MEMORY_RP | EFI_MEMORY_XP;

    // Update GCD capabilities as these may have changed in the page table from the original GCD setting
    // this follows the same pattern as x86 GCD and Page Table syncing
    Status = gDS->SetMemorySpaceCapabilities (
                    RegionStart,
                    RegionLength,
                    Capabilities
                    );

    if (EFI_ERROR (Status)) {
      DEBUG ((
        DEBUG_ERROR,
        "%a - failed to update GCD capabilities: 0x%llx on memory region: 0x%llx length: 0x%llx Status: %r\n",
        __func__,
        Capabilities,
        RegionStart,
        RegionLength,
        Status
        ));

      // If we fail to set capabilities, we should assert as this is a GCD internal error, but follow the previous
      // behavior and try to set the attributes (which may or may not fail)
      ASSERT_EFI_ERROR (Status);
    }

    //
    // Set memory attributes according to page table attributes and the original attributes of descriptor
    //
    Status = gDS->SetMemorySpaceAttributes (
                    RegionStart,
                    RegionLength,
                    (MemorySpaceMap[Index].Attributes & ~EFI_MEMORY_CACHETYPE_MASK) | (Attributes & Capabilities)
                    );

    if (EFI_ERROR (Status)) {
      DEBUG ((
        DEBUG_ERROR,
        "%a - failed to update GCD attributes: 0x%llx on memory region: 0x%llx length: 0x%llx Status: %r\n",
        __func__,
        Attributes,
        RegionStart,
        RegionLength,
        Status
        ));

      ASSERT_EFI_ERROR (Status);
    }
  }

  return EFI_SUCCESS;
}

/**
  This function modifies the attributes for the memory region specified by BaseAddress and
  Length from their current attributes to the attributes specified by Attributes.

  @param  This             The EFI_CPU_ARCH_PROTOCOL instance.
  @param  BaseAddress      The physical address that is the start address of a memory region.
  @param  Length           The size in bytes of the memory region.
  @param  Attributes       The bit mask of attributes to set for the memory region.

  @retval EFI_SUCCESS           The attributes were set for the memory region.
  @retval EFI_ACCESS_DENIED     The attributes for the memory resource range specified by
                                BaseAddress and Length cannot be modified.
  @retval EFI_INVALID_PARAMETER Length is zero.
  @retval EFI_OUT_OF_RESOURCES  There are not enough system resources to modify the attributes of
                                the memory resource range.
  @retval EFI_UNSUPPORTED       The processor does not support one or more bytes of the memory
                                resource range specified by BaseAddress and Length.
                                The bit mask of attributes is not support for the memory resource
                                range specified by BaseAddress and Length.

**/
EFI_STATUS
EFIAPI
CpuSetMemoryAttributes (
  IN EFI_CPU_ARCH_PROTOCOL  *This,
  IN EFI_PHYSICAL_ADDRESS   BaseAddress,
  IN UINT64                 Length,
  IN UINT64                 EfiAttributes
  )
{
  EFI_STATUS  Status;
  UINTN       ArmAttributes;
  UINTN       RegionBaseAddress;
  UINTN       RegionLength;
  UINTN       RegionArmAttributes;

  if (mIsFlushingGCD) {
    return EFI_SUCCESS;
  }

  if ((BaseAddress & (SIZE_4KB - 1)) != 0) {
    // Minimum granularity is SIZE_4KB (4KB on ARM)
    DEBUG ((DEBUG_PAGE, "CpuSetMemoryAttributes(%lx, %lx, %lx): Minimum granularity is SIZE_4KB\n", BaseAddress, Length, EfiAttributes));
    return EFI_UNSUPPORTED;
  }

  // Convert the 'Attribute' into ARM Attribute
  ArmAttributes = EfiAttributeToArmAttribute (EfiAttributes);

  // Get the region starting from 'BaseAddress' and its 'Attribute'
  RegionBaseAddress = BaseAddress;
  Status            = GetMemoryRegion (&RegionBaseAddress, &RegionLength, &RegionArmAttributes);

  // Data & Instruction Caches are flushed when we set new memory attributes.
  // So, we only set the attributes if the new region is different.
  if (EFI_ERROR (Status) || (RegionArmAttributes != ArmAttributes) ||
      ((BaseAddress + Length) > (RegionBaseAddress + RegionLength)))
  {
    return ArmSetMemoryAttributes (BaseAddress, Length, EfiAttributes, 0);
  } else {
    return EFI_SUCCESS;
  }
}
