/** @file
  Var Check PCD handler.

Copyright (c) 2015 - 2017, Intel Corporation. All rights reserved.<BR>
This program and the accompanying materials
are licensed and made available under the terms and conditions of the BSD License
which accompanies this distribution.  The full text of the license may be found at
http://opensource.org/licenses/bsd-license.php

THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.

**/

#include <Library/VarCheckLib.h>
#include <Library/BaseLib.h>
#include <Library/DebugLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/DxeServicesLib.h>

#include "VarCheckPcdStructure.h"

//#define DUMP_VAR_CHECK_PCD

GLOBAL_REMOVE_IF_UNREFERENCED CONST CHAR8 mVarCheckPcdHex[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

/**
  Dump some hexadecimal data.

  @param[in] Indent     How many spaces to indent the output.
  @param[in] Offset     The offset of the dump.
  @param[in] DataSize   The size in bytes of UserData.
  @param[in] UserData   The data to dump.

**/
VOID
VarCheckPcdInternalDumpHex (
  IN UINTN        Indent,
  IN UINTN        Offset,
  IN UINTN        DataSize,
  IN VOID         *UserData
  )
{
  UINT8 *Data;

  CHAR8 Val[50];

  CHAR8 Str[20];

  UINT8 TempByte;
  UINTN Size;
  UINTN Index;

  Data = UserData;
  while (DataSize != 0) {
    Size = 16;
    if (Size > DataSize) {
      Size = DataSize;
    }

    for (Index = 0; Index < Size; Index += 1) {
      TempByte            = Data[Index];
      Val[Index * 3 + 0]  = mVarCheckPcdHex[TempByte >> 4];
      Val[Index * 3 + 1]  = mVarCheckPcdHex[TempByte & 0xF];
      Val[Index * 3 + 2]  = (CHAR8) ((Index == 7) ? '-' : ' ');
      Str[Index]          = (CHAR8) ((TempByte < ' ' || TempByte > 'z') ? '.' : TempByte);
    }

    Val[Index * 3]  = 0;
    Str[Index]      = 0;
    DEBUG ((EFI_D_INFO, "%*a%08X: %-48a *%a*\r\n", Indent, "", Offset, Val, Str));

    Data += Size;
    Offset += Size;
    DataSize -= Size;
  }
}

/**
  Var Check Pcd ValidData.

  @param[in] PcdValidData   Pointer to Pcd ValidData
  @param[in] Data           Data pointer.
  @param[in] DataSize       Size of Data to set.

  @retval TRUE  Check pass
  @retval FALSE Check fail.

**/
BOOLEAN
VarCheckPcdValidData (
  IN VAR_CHECK_PCD_VALID_DATA_HEADER    *PcdValidData,
  IN VOID                               *Data,
  IN UINTN                              DataSize
  )
{
  UINT64   OneData;
  UINT64   Minimum;
  UINT64   Maximum;
  UINT64   OneValue;
  UINT8    *Ptr;

  OneData = 0;
  CopyMem (&OneData, (UINT8 *) Data + PcdValidData->VarOffset, PcdValidData->StorageWidth);

  switch (PcdValidData->Type) {
    case VarCheckPcdValidList:
      Ptr = (UINT8 *) ((VAR_CHECK_PCD_VALID_LIST *) PcdValidData + 1);
      while ((UINTN) Ptr < (UINTN) PcdValidData + PcdValidData->Length) {
        OneValue = 0;
        CopyMem (&OneValue, Ptr, PcdValidData->StorageWidth);
        if (OneData == OneValue) {
          //
          // Match
          //
          break;
        }
        Ptr += PcdValidData->StorageWidth;
      }
      if ((UINTN) Ptr >= ((UINTN) PcdValidData + PcdValidData->Length)) {
        //
        // No match
        //
        DEBUG ((EFI_D_INFO, "VarCheckPcdValidData fail: ValidList mismatch (0x%lx)\n", OneData));
        DEBUG_CODE (VarCheckPcdInternalDumpHex (2, 0, PcdValidData->Length, (UINT8 *) PcdValidData););
        return FALSE;
      }
      break;

    case VarCheckPcdValidRange:
      Minimum = 0;
      Maximum = 0;
      Ptr = (UINT8 *) ((VAR_CHECK_PCD_VALID_RANGE *) PcdValidData + 1);
      while ((UINTN) Ptr < (UINTN) PcdValidData + PcdValidData->Length) {
        CopyMem (&Minimum, Ptr, PcdValidData->StorageWidth);
        Ptr += PcdValidData->StorageWidth;
        CopyMem (&Maximum, Ptr, PcdValidData->StorageWidth);
        Ptr += PcdValidData->StorageWidth;

        if ((OneData >= Minimum) && (OneData <= Maximum)) {
          return TRUE;
        }
      }
      DEBUG ((EFI_D_INFO, "VarCheckPcdValidData fail: ValidRange mismatch (0x%lx)\n", OneData));
      DEBUG_CODE (VarCheckPcdInternalDumpHex (2, 0, PcdValidData->Length, (UINT8 *) PcdValidData););
      return FALSE;
      break;

    default:
      ASSERT (FALSE);
      break;
  }

  return TRUE;
}

VAR_CHECK_PCD_VARIABLE_HEADER   *mVarCheckPcdBin = NULL;
UINTN                           mVarCheckPcdBinSize = 0;

/**
  SetVariable check handler PCD.

  @param[in] VariableName       Name of Variable to set.
  @param[in] VendorGuid         Variable vendor GUID.
  @param[in] Attributes         Attribute value of the variable.
  @param[in] DataSize           Size of Data to set.
  @param[in] Data               Data pointer.

  @retval EFI_SUCCESS               The SetVariable check result was success.
  @retval EFI_SECURITY_VIOLATION    Check fail.

**/
EFI_STATUS
EFIAPI
SetVariableCheckHandlerPcd (
  IN CHAR16     *VariableName,
  IN EFI_GUID   *VendorGuid,
  IN UINT32     Attributes,
  IN UINTN      DataSize,
  IN VOID       *Data
  )
{
  VAR_CHECK_PCD_VARIABLE_HEADER     *PcdVariable;
  VAR_CHECK_PCD_VALID_DATA_HEADER   *PcdValidData;

  if (mVarCheckPcdBin == NULL) {
    return EFI_SUCCESS;
  }

  if ((((Attributes & EFI_VARIABLE_APPEND_WRITE) == 0) && (DataSize == 0)) || (Attributes == 0)) {
    //
    // Do not check delete variable.
    //
    return EFI_SUCCESS;
  }

  //
  // For Pcd Variable header align.
  //
  PcdVariable = (VAR_CHECK_PCD_VARIABLE_HEADER *) HEADER_ALIGN (mVarCheckPcdBin);
  while ((UINTN) PcdVariable < ((UINTN) mVarCheckPcdBin + mVarCheckPcdBinSize)) {
    if ((StrCmp ((CHAR16 *) (PcdVariable + 1), VariableName) == 0) &&
        (CompareGuid (&PcdVariable->Guid, VendorGuid))) {
      //
      // Found the Pcd Variable that could be used to do check.
      //
      DEBUG ((EFI_D_INFO, "VarCheckPcdVariable - %s:%g with Attributes = 0x%08x Size = 0x%x\n", VariableName, VendorGuid, Attributes, DataSize));
      if ((PcdVariable->Attributes != 0) && PcdVariable->Attributes != Attributes) {
        DEBUG ((EFI_D_INFO, "VarCheckPcdVariable fail for Attributes - 0x%08x\n", PcdVariable->Attributes));
        return EFI_SECURITY_VIOLATION;
      }

      if (DataSize == 0) {
        DEBUG ((EFI_D_INFO, "VarCheckPcdVariable - CHECK PASS with DataSize == 0 !\n"));
        return EFI_SUCCESS;
      }

      //
      // Do the check.
      // For Pcd ValidData header align.
      //
      PcdValidData = (VAR_CHECK_PCD_VALID_DATA_HEADER *) HEADER_ALIGN (((UINTN) PcdVariable + PcdVariable->HeaderLength));
      while ((UINTN) PcdValidData < ((UINTN) PcdVariable + PcdVariable->Length)) {
        if (((UINTN) PcdValidData->VarOffset + PcdValidData->StorageWidth) <= DataSize) {
          if (!VarCheckPcdValidData (PcdValidData, Data, DataSize)) {
            return EFI_SECURITY_VIOLATION;
          }
        }
        //
        // For Pcd ValidData header align.
        //
        PcdValidData = (VAR_CHECK_PCD_VALID_DATA_HEADER *) HEADER_ALIGN (((UINTN) PcdValidData + PcdValidData->Length));
      }

      DEBUG ((EFI_D_INFO, "VarCheckPcdVariable - ALL CHECK PASS!\n"));
      return EFI_SUCCESS;
    }
    //
    // For Pcd Variable header align.
    //
    PcdVariable = (VAR_CHECK_PCD_VARIABLE_HEADER *) HEADER_ALIGN (((UINTN) PcdVariable + PcdVariable->Length));
  }

  // Not found, so pass.
  return EFI_SUCCESS;
}

#ifdef DUMP_VAR_CHECK_PCD
/**
  Dump Pcd ValidData.

  @param[in] PcdValidData    Pointer to Pcd ValidData.

**/
VOID
DumpPcdValidData (
  IN VAR_CHECK_PCD_VALID_DATA_HEADER    *PcdValidData
  )
{
  UINT64    Minimum;
  UINT64    Maximum;
  UINT64    OneValue;
  UINT8     *Ptr;

  DEBUG ((EFI_D_INFO, "  VAR_CHECK_PCD_VALID_DATA_HEADER\n"));
  DEBUG ((EFI_D_INFO, "    Type          - 0x%02x\n", PcdValidData->Type));
  DEBUG ((EFI_D_INFO, "    Length        - 0x%02x\n", PcdValidData->Length));
  DEBUG ((EFI_D_INFO, "    VarOffset     - 0x%04x\n", PcdValidData->VarOffset));
  DEBUG ((EFI_D_INFO, "    StorageWidth  - 0x%02x\n", PcdValidData->StorageWidth));

  switch (PcdValidData->Type) {
    case VarCheckPcdValidList:
      Ptr = (UINT8 *) ((VAR_CHECK_PCD_VALID_LIST *) PcdValidData + 1);
      while ((UINTN) Ptr < ((UINTN) PcdValidData + PcdValidData->Length)) {
        OneValue = 0;
        CopyMem (&OneValue, Ptr, PcdValidData->StorageWidth);
        switch (PcdValidData->StorageWidth) {
          case sizeof (UINT8):
            DEBUG ((EFI_D_INFO, "    ValidList   - 0x%02x\n", OneValue));
            break;
          case sizeof (UINT16):
            DEBUG ((EFI_D_INFO, "    ValidList   - 0x%04x\n", OneValue));
            break;
          case sizeof (UINT32):
            DEBUG ((EFI_D_INFO, "    ValidList   - 0x%08x\n", OneValue));
            break;
          case sizeof (UINT64):
            DEBUG ((EFI_D_INFO, "    ValidList   - 0x%016lx\n", OneValue));
            break;
          default:
            ASSERT (FALSE);
            break;
        }
        Ptr += PcdValidData->StorageWidth;
      }
      break;

    case VarCheckPcdValidRange:
      Minimum = 0;
      Maximum = 0;
      Ptr = (UINT8 *) ((VAR_CHECK_PCD_VALID_RANGE *) PcdValidData + 1);
      while ((UINTN) Ptr < (UINTN) PcdValidData + PcdValidData->Length) {
        CopyMem (&Minimum, Ptr, PcdValidData->StorageWidth);
        Ptr += PcdValidData->StorageWidth;
        CopyMem (&Maximum, Ptr, PcdValidData->StorageWidth);
        Ptr += PcdValidData->StorageWidth;

        switch (PcdValidData->StorageWidth) {
          case sizeof (UINT8):
            DEBUG ((EFI_D_INFO, "    Minimum       - 0x%02x\n", Minimum));
            DEBUG ((EFI_D_INFO, "    Maximum       - 0x%02x\n", Maximum));
            break;
          case sizeof (UINT16):
            DEBUG ((EFI_D_INFO, "    Minimum       - 0x%04x\n", Minimum));
            DEBUG ((EFI_D_INFO, "    Maximum       - 0x%04x\n", Maximum));
            break;
          case sizeof (UINT32):
            DEBUG ((EFI_D_INFO, "    Minimum       - 0x%08x\n", Minimum));
            DEBUG ((EFI_D_INFO, "    Maximum       - 0x%08x\n", Maximum));
            break;
          case sizeof (UINT64):
            DEBUG ((EFI_D_INFO, "    Minimum       - 0x%016lx\n", Minimum));
            DEBUG ((EFI_D_INFO, "    Maximum       - 0x%016lx\n", Maximum));
            break;
          default:
            ASSERT (FALSE);
            break;
        }
      }
      break;

    default:
      ASSERT (FALSE);
      break;
  }
}

/**
  Dump Pcd Variable.

  @param[in] PcdVariable    Pointer to Pcd Variable.

**/
VOID
DumpPcdVariable (
  IN VAR_CHECK_PCD_VARIABLE_HEADER  *PcdVariable
  )
{
  VAR_CHECK_PCD_VALID_DATA_HEADER *PcdValidData;

  DEBUG ((EFI_D_INFO, "VAR_CHECK_PCD_VARIABLE_HEADER\n"));
  DEBUG ((EFI_D_INFO, "  Revision        - 0x%04x\n", PcdVariable->Revision));
  DEBUG ((EFI_D_INFO, "  HeaderLength    - 0x%04x\n", PcdVariable->HeaderLength));
  DEBUG ((EFI_D_INFO, "  Length          - 0x%08x\n", PcdVariable->Length));
  DEBUG ((EFI_D_INFO, "  Type            - 0x%02x\n", PcdVariable->Type));
  DEBUG ((EFI_D_INFO, "  Attributes      - 0x%08x\n", PcdVariable->Attributes));
  DEBUG ((EFI_D_INFO, "  Guid            - %g\n", &PcdVariable->Guid));
  DEBUG ((EFI_D_INFO, "  Name            - %s\n", PcdVariable + 1));

  //
  // For Pcd ValidData header align.
  //
  PcdValidData = (VAR_CHECK_PCD_VALID_DATA_HEADER *) HEADER_ALIGN (((UINTN) PcdVariable + PcdVariable->HeaderLength));
  while ((UINTN) PcdValidData < ((UINTN) PcdVariable + PcdVariable->Length)) {
    //
    // Dump Pcd ValidData related to the Pcd Variable.
    //
    DumpPcdValidData (PcdValidData);
    //
    // For Pcd ValidData header align.
    //
    PcdValidData = (VAR_CHECK_PCD_VALID_DATA_HEADER *) HEADER_ALIGN (((UINTN) PcdValidData + PcdValidData->Length));
  }
}

/**
  Dump Var Check PCD.

  @param[in] VarCheckPcdBin     Pointer to VarCheckPcdBin.
  @param[in] VarCheckPcdBinSize VarCheckPcdBin size.

**/
VOID
DumpVarCheckPcd (
  IN VOID   *VarCheckPcdBin,
  IN UINTN  VarCheckPcdBinSize
  )
{
  VAR_CHECK_PCD_VARIABLE_HEADER     *PcdVariable;

  DEBUG ((EFI_D_INFO, "DumpVarCheckPcd\n"));

  //
  // For Pcd Variable header align.
  //
  PcdVariable = (VAR_CHECK_PCD_VARIABLE_HEADER *) HEADER_ALIGN (VarCheckPcdBin);
  while ((UINTN) PcdVariable < ((UINTN) VarCheckPcdBin + VarCheckPcdBinSize)) {
    DumpPcdVariable (PcdVariable);
    //
    // For Pcd Variable header align.
    //
    PcdVariable = (VAR_CHECK_PCD_VARIABLE_HEADER *) HEADER_ALIGN (((UINTN) PcdVariable + PcdVariable->Length));
  }
}
#endif

/**
  Locate VarCheckPcdBin.

**/
VOID
EFIAPI
LocateVarCheckPcdBin (
  VOID
  )
{
  EFI_STATUS                        Status;
  VAR_CHECK_PCD_VARIABLE_HEADER     *VarCheckPcdBin;
  UINTN                             VarCheckPcdBinSize;

  //
  // Search the VarCheckPcdBin from the first RAW section of current FFS.
  //
  Status = GetSectionFromFfs (
             EFI_SECTION_RAW,
             0,
             (VOID **) &VarCheckPcdBin,
             &VarCheckPcdBinSize
             );
  if (!EFI_ERROR (Status)) {
    //
    // AllocateRuntimeZeroPool () from MemoryAllocateLib is used for runtime access
    // in SetVariable check handler.
    //
    mVarCheckPcdBin = AllocateRuntimeCopyPool (VarCheckPcdBinSize, VarCheckPcdBin);
    ASSERT (mVarCheckPcdBin != NULL);
    //
    // Make sure the allocated buffer for VarCheckPcdBin at required alignment.
    //
    ASSERT ((((UINTN) mVarCheckPcdBin) & (HEADER_ALIGNMENT - 1)) == 0);
    mVarCheckPcdBinSize = VarCheckPcdBinSize;
    FreePool (VarCheckPcdBin);

    DEBUG ((EFI_D_INFO, "VarCheckPcdBin - at 0x%x size = 0x%x\n", mVarCheckPcdBin, mVarCheckPcdBinSize));

#ifdef DUMP_VAR_CHECK_PCD
    DEBUG_CODE (
      DumpVarCheckPcd (mVarCheckPcdBin, mVarCheckPcdBinSize);
    );
#endif
  } else {
    DEBUG ((EFI_D_INFO, "[VarCheckPcd] No VarCheckPcdBin found at the first RAW section\n"));
  }
}

/**
  Constructor function of VarCheckPcdLib to register var check PCD handler.

  @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 constructor executed correctly.

**/
EFI_STATUS
EFIAPI
VarCheckPcdLibNullClassConstructor (
  IN EFI_HANDLE             ImageHandle,
  IN EFI_SYSTEM_TABLE       *SystemTable
  )
{
  LocateVarCheckPcdBin ();
  VarCheckLibRegisterAddressPointer ((VOID **) &mVarCheckPcdBin);
  VarCheckLibRegisterSetVariableCheckHandler (SetVariableCheckHandlerPcd);

  return EFI_SUCCESS;
}
