/** @file

  Implement the Fault Tolerant Write (FTW) protocol based on SMM FTW
  module.

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

**/

#include "FaultTolerantWriteSmmDxe.h"

EFI_HANDLE                      mHandle            = NULL;
EFI_MM_COMMUNICATION2_PROTOCOL  *mMmCommunication2 = NULL;
UINTN                           mPrivateDataSize   = 0;

EFI_FAULT_TOLERANT_WRITE_PROTOCOL  mFaultTolerantWriteDriver = {
  FtwGetMaxBlockSize,
  FtwAllocate,
  FtwWrite,
  FtwRestart,
  FtwAbort,
  FtwGetLastWrite
};

/**
  Initialize the communicate buffer using DataSize and Function number.

  @param[out]      CommunicateBuffer The communicate buffer. Caller should free it after use.
  @param[out]      DataPtr           Points to the data in the communicate buffer. Caller should not free it.
  @param[in]       DataSize          The payload size.
  @param[in]       Function          The function number used to initialize the communicate header.

**/
VOID
InitCommunicateBuffer (
  OUT     VOID   **CommunicateBuffer,
  OUT     VOID   **DataPtr,
  IN      UINTN  DataSize,
  IN      UINTN  Function
  )
{
  EFI_MM_COMMUNICATE_HEADER            *SmmCommunicateHeader;
  SMM_FTW_COMMUNICATE_FUNCTION_HEADER  *SmmFtwFunctionHeader;

  //
  // The whole buffer size: SMM_COMMUNICATE_HEADER_SIZE + SMM_FTW_COMMUNICATE_HEADER_SIZE + DataSize.
  //
  SmmCommunicateHeader = AllocateZeroPool (DataSize + SMM_COMMUNICATE_HEADER_SIZE + SMM_FTW_COMMUNICATE_HEADER_SIZE);
  ASSERT (SmmCommunicateHeader != NULL);

  //
  // Prepare data buffer.
  //
  CopyGuid (&SmmCommunicateHeader->HeaderGuid, &gEfiSmmFaultTolerantWriteProtocolGuid);
  SmmCommunicateHeader->MessageLength = DataSize + SMM_FTW_COMMUNICATE_HEADER_SIZE;

  SmmFtwFunctionHeader           = (SMM_FTW_COMMUNICATE_FUNCTION_HEADER *)SmmCommunicateHeader->Data;
  SmmFtwFunctionHeader->Function = Function;

  *CommunicateBuffer = SmmCommunicateHeader;
  if (DataPtr != NULL) {
    *DataPtr = SmmFtwFunctionHeader->Data;
  }
}

/**
  Send the data in communicate buffer to SMI handler and get response.

  @param[in, out]  SmmCommunicateHeader    The communicate buffer.
  @param[in]       DataSize                The payload size.

**/
EFI_STATUS
SendCommunicateBuffer (
  IN OUT  EFI_MM_COMMUNICATE_HEADER  *SmmCommunicateHeader,
  IN      UINTN                      DataSize
  )
{
  EFI_STATUS                           Status;
  UINTN                                CommSize;
  SMM_FTW_COMMUNICATE_FUNCTION_HEADER  *SmmFtwFunctionHeader;

  CommSize = DataSize + SMM_COMMUNICATE_HEADER_SIZE + SMM_FTW_COMMUNICATE_HEADER_SIZE;
  Status   = mMmCommunication2->Communicate (
                                  mMmCommunication2,
                                  SmmCommunicateHeader,
                                  SmmCommunicateHeader,
                                  &CommSize
                                  );
  ASSERT_EFI_ERROR (Status);

  SmmFtwFunctionHeader = (SMM_FTW_COMMUNICATE_FUNCTION_HEADER *)SmmCommunicateHeader->Data;
  return SmmFtwFunctionHeader->ReturnStatus;
}

/**
  Get the FvbBaseAddress and FvbAttributes from the FVB handle FvbHandle.

  @param[in]   FvbHandle         The handle of FVB protocol that provides services.
  @param[out]  FvbBaseAddress    The base address of the FVB attached with FvbHandle.
  @param[out]  FvbAttributes     The attributes of the FVB attached with FvbHandle.

  @retval EFI_SUCCESS            The function completed successfully.
  @retval Others                 The function could not complete successfully.

**/
EFI_STATUS
ConvertFvbHandle (
  IN  EFI_HANDLE            FvbHandle,
  OUT EFI_PHYSICAL_ADDRESS  *FvbBaseAddress,
  OUT EFI_FVB_ATTRIBUTES_2  *FvbAttributes
  )
{
  EFI_STATUS                          Status;
  EFI_FIRMWARE_VOLUME_BLOCK_PROTOCOL  *Fvb;

  Status = gBS->HandleProtocol (FvbHandle, &gEfiFirmwareVolumeBlockProtocolGuid, (VOID **)&Fvb);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = Fvb->GetPhysicalAddress (Fvb, FvbBaseAddress);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = Fvb->GetAttributes (Fvb, FvbAttributes);
  return Status;
}

/**
  Get the size of the largest block that can be updated in a fault-tolerant manner.

  @param[in]  This             Indicates a pointer to the calling context.
  @param[out] BlockSize        A pointer to a caller-allocated UINTN that is
                               updated to indicate the size of the largest block
                               that can be updated.

  @retval EFI_SUCCESS          The function completed successfully.
  @retval EFI_ABORTED          The function could not complete successfully.

**/
EFI_STATUS
EFIAPI
FtwGetMaxBlockSize (
  IN EFI_FAULT_TOLERANT_WRITE_PROTOCOL  *This,
  OUT UINTN                             *BlockSize
  )
{
  EFI_STATUS                         Status;
  UINTN                              PayloadSize;
  EFI_MM_COMMUNICATE_HEADER          *SmmCommunicateHeader;
  SMM_FTW_GET_MAX_BLOCK_SIZE_HEADER  *SmmFtwBlockSizeHeader;

  //
  // Initialize the communicate buffer.
  //
  PayloadSize = sizeof (SMM_FTW_GET_MAX_BLOCK_SIZE_HEADER);
  InitCommunicateBuffer ((VOID **)&SmmCommunicateHeader, (VOID **)&SmmFtwBlockSizeHeader, PayloadSize, FTW_FUNCTION_GET_MAX_BLOCK_SIZE);

  //
  // Send data to SMM.
  //
  Status = SendCommunicateBuffer (SmmCommunicateHeader, PayloadSize);

  //
  // Get data from SMM
  //
  *BlockSize = SmmFtwBlockSizeHeader->BlockSize;
  FreePool (SmmCommunicateHeader);

  return Status;
}

/**
  Allocates space for the protocol to maintain information about writes.
  Since writes must be completed in a fault-tolerant manner and multiple
  writes require more resources to be successful, this function
  enables the protocol to ensure that enough space exists to track
  information about upcoming writes.

  @param[in]  This             A pointer to the calling context.
  @param[in]  CallerId         The GUID identifying the write.
  @param[in]  PrivateDataSize  The size of the caller's private data  that must be
                               recorded for each write.
  @param[in]  NumberOfWrites   The number of fault tolerant block writes that will
                               need to occur.

  @retval EFI_SUCCESS          The function completed successfully
  @retval EFI_ABORTED          The function could not complete successfully.
  @retval EFI_ACCESS_DENIED    Not all allocated writes have been completed.  All
                               writes must be completed or aborted before another
                               fault tolerant write can occur.

**/
EFI_STATUS
EFIAPI
FtwAllocate (
  IN EFI_FAULT_TOLERANT_WRITE_PROTOCOL  *This,
  IN EFI_GUID                           *CallerId,
  IN UINTN                              PrivateDataSize,
  IN UINTN                              NumberOfWrites
  )
{
  EFI_STATUS                 Status;
  UINTN                      PayloadSize;
  EFI_MM_COMMUNICATE_HEADER  *SmmCommunicateHeader;
  SMM_FTW_ALLOCATE_HEADER    *SmmFtwAllocateHeader;

  //
  // Initialize the communicate buffer.
  //
  PayloadSize = sizeof (SMM_FTW_ALLOCATE_HEADER);
  InitCommunicateBuffer ((VOID **)&SmmCommunicateHeader, (VOID **)&SmmFtwAllocateHeader, PayloadSize, FTW_FUNCTION_ALLOCATE);
  CopyGuid (&SmmFtwAllocateHeader->CallerId, CallerId);
  SmmFtwAllocateHeader->PrivateDataSize = PrivateDataSize;
  SmmFtwAllocateHeader->NumberOfWrites  = NumberOfWrites;

  //
  // Send data to SMM.
  //
  Status = SendCommunicateBuffer (SmmCommunicateHeader, PayloadSize);
  if (!EFI_ERROR (Status)) {
    mPrivateDataSize = PrivateDataSize;
  }

  FreePool (SmmCommunicateHeader);
  return Status;
}

/**
  Starts a target block update. This records information about the write
  in fault tolerant storage, and will complete the write in a recoverable
  manner, ensuring at all times that either the original contents or
  the modified contents are available.

  @param[in]  This             The calling context.
  @param[in]  Lba              The logical block address of the target block.
  @param[in]  Offset           The offset within the target block to place the
                               data.
  @param[in]  Length           The number of bytes to write to the target block.
  @param[in]  PrivateData      A pointer to private data that the caller requires
                               to complete any pending writes in the event of a
                               fault.
  @param[in]  FvBlockHandle    The handle of FVB protocol that provides services
                               for reading, writing, and erasing the target block.
  @param[in]  Buffer           The data to write.

  @retval EFI_SUCCESS          The function completed successfully.
  @retval EFI_ABORTED          The function could not complete successfully.
  @retval EFI_BAD_BUFFER_SIZE  The write would span a block boundary, which is not
                               a valid action.
  @retval EFI_ACCESS_DENIED    No writes have been allocated.
  @retval EFI_NOT_READY        The last write has not been completed. Restart()
                               must be called to complete it.

**/
EFI_STATUS
EFIAPI
FtwWrite (
  IN EFI_FAULT_TOLERANT_WRITE_PROTOCOL  *This,
  IN EFI_LBA                            Lba,
  IN UINTN                              Offset,
  IN UINTN                              Length,
  IN VOID                               *PrivateData,
  IN EFI_HANDLE                         FvBlockHandle,
  IN VOID                               *Buffer
  )
{
  EFI_STATUS                 Status;
  UINTN                      PayloadSize;
  EFI_MM_COMMUNICATE_HEADER  *SmmCommunicateHeader;
  SMM_FTW_WRITE_HEADER       *SmmFtwWriteHeader;

  //
  // Initialize the communicate buffer.
  //
  PayloadSize = OFFSET_OF (SMM_FTW_WRITE_HEADER, Data) + Length;
  if (PrivateData != NULL) {
    //
    // The private data buffer size should be the same one in FtwAllocate API.
    //
    PayloadSize += mPrivateDataSize;
  }

  InitCommunicateBuffer ((VOID **)&SmmCommunicateHeader, (VOID **)&SmmFtwWriteHeader, PayloadSize, FTW_FUNCTION_WRITE);

  //
  // FvBlockHandle can not be used in SMM environment. Here we get the FVB protocol first, then get FVB base address
  // and its attribute. Send these information to SMM handler, the SMM handler will find the proper FVB to write data.
  //
  Status = ConvertFvbHandle (FvBlockHandle, &SmmFtwWriteHeader->FvbBaseAddress, &SmmFtwWriteHeader->FvbAttributes);
  if (EFI_ERROR (Status)) {
    FreePool (SmmCommunicateHeader);
    return EFI_ABORTED;
  }

  SmmFtwWriteHeader->Lba    = Lba;
  SmmFtwWriteHeader->Offset = Offset;
  SmmFtwWriteHeader->Length = Length;
  CopyMem (SmmFtwWriteHeader->Data, Buffer, Length);
  if (PrivateData == NULL) {
    SmmFtwWriteHeader->PrivateDataSize = 0;
  } else {
    SmmFtwWriteHeader->PrivateDataSize = mPrivateDataSize;
    CopyMem (&SmmFtwWriteHeader->Data[Length], PrivateData, mPrivateDataSize);
  }

  //
  // Send data to SMM.
  //
  Status = SendCommunicateBuffer (SmmCommunicateHeader, PayloadSize);
  FreePool (SmmCommunicateHeader);
  return Status;
}

/**
  Restarts a previously interrupted write. The caller must provide the
  block protocol needed to complete the interrupted write.

  @param[in]  This             The calling context.
  @param[in]  FvBlockHandle    The handle of FVB protocol that provides services.

  @retval EFI_SUCCESS          The function completed successfully.
  @retval EFI_ABORTED          The function could not complete successfully.
  @retval EFI_ACCESS_DENIED    No pending writes exist.

**/
EFI_STATUS
EFIAPI
FtwRestart (
  IN EFI_FAULT_TOLERANT_WRITE_PROTOCOL  *This,
  IN EFI_HANDLE                         FvBlockHandle
  )
{
  EFI_STATUS                 Status;
  UINTN                      PayloadSize;
  EFI_MM_COMMUNICATE_HEADER  *SmmCommunicateHeader;
  SMM_FTW_RESTART_HEADER     *SmmFtwRestartHeader;

  //
  // Initialize the communicate buffer.
  //
  PayloadSize = sizeof (SMM_FTW_RESTART_HEADER);
  InitCommunicateBuffer ((VOID **)&SmmCommunicateHeader, (VOID **)&SmmFtwRestartHeader, PayloadSize, FTW_FUNCTION_RESTART);

  //
  // FvBlockHandle can not be used in SMM environment. Here we get the FVB protocol first, then get FVB base address
  // and its attribute. Send these information to SMM handler, the SMM handler will find the proper FVB to write data.
  //
  Status = ConvertFvbHandle (FvBlockHandle, &SmmFtwRestartHeader->FvbBaseAddress, &SmmFtwRestartHeader->FvbAttributes);
  if (EFI_ERROR (Status)) {
    FreePool (SmmCommunicateHeader);
    return EFI_ABORTED;
  }

  //
  // Send data to SMM.
  //
  Status = SendCommunicateBuffer (SmmCommunicateHeader, PayloadSize);
  FreePool (SmmCommunicateHeader);
  return Status;
}

/**
  Aborts all previously allocated writes.

  @param[in]  This             The calling context.

  @retval EFI_SUCCESS          The function completed successfully.
  @retval EFI_ABORTED          The function could not complete successfully.
  @retval EFI_NOT_FOUND        No allocated writes exist.

**/
EFI_STATUS
EFIAPI
FtwAbort (
  IN EFI_FAULT_TOLERANT_WRITE_PROTOCOL  *This
  )
{
  EFI_STATUS                 Status;
  EFI_MM_COMMUNICATE_HEADER  *SmmCommunicateHeader;

  //
  // Initialize the communicate buffer.
  //
  InitCommunicateBuffer ((VOID **)&SmmCommunicateHeader, NULL, 0, FTW_FUNCTION_ABORT);

  //
  // Send data to SMM.
  //
  Status = SendCommunicateBuffer (SmmCommunicateHeader, 0);

  FreePool (SmmCommunicateHeader);
  return Status;
}

/**
  Starts a target block update. This function records information about the write
  in fault-tolerant storage and completes the write in a recoverable
  manner, ensuring at all times that either the original contents or
  the modified contents are available.

  @param[in]      This            Indicates a pointer to the calling context.
  @param[out]     CallerId        The GUID identifying the last write.
  @param[out]     Lba             The logical block address of the last write.
  @param[out]     Offset          The offset within the block of the last write.
  @param[out]     Length          The length of the last write.
  @param[in, out] PrivateDataSize On input, the size of the PrivateData buffer. On
                                  output, the size of the private data stored for
                                  this write.
  @param[out]     PrivateData     A pointer to a buffer. The function will copy
                                  PrivateDataSize bytes from the private data stored
                                  for this write.
  @param[out]     Complete        A Boolean value with TRUE indicating that the write
                                  was completed.

  @retval EFI_SUCCESS             The function completed successfully.
  @retval EFI_ABORTED             The function could not complete successfully.
  @retval EFI_NOT_FOUND           No allocated writes exist.

**/
EFI_STATUS
EFIAPI
FtwGetLastWrite (
  IN EFI_FAULT_TOLERANT_WRITE_PROTOCOL  *This,
  OUT EFI_GUID                          *CallerId,
  OUT EFI_LBA                           *Lba,
  OUT UINTN                             *Offset,
  OUT UINTN                             *Length,
  IN OUT UINTN                          *PrivateDataSize,
  OUT VOID                              *PrivateData,
  OUT BOOLEAN                           *Complete
  )
{
  EFI_STATUS                     Status;
  UINTN                          PayloadSize;
  EFI_MM_COMMUNICATE_HEADER      *SmmCommunicateHeader;
  SMM_FTW_GET_LAST_WRITE_HEADER  *SmmFtwGetLastWriteHeader;

  //
  // Initialize the communicate buffer.
  //
  PayloadSize = OFFSET_OF (SMM_FTW_GET_LAST_WRITE_HEADER, Data) + *PrivateDataSize;
  InitCommunicateBuffer ((VOID **)&SmmCommunicateHeader, (VOID **)&SmmFtwGetLastWriteHeader, PayloadSize, FTW_FUNCTION_GET_LAST_WRITE);
  SmmFtwGetLastWriteHeader->PrivateDataSize = *PrivateDataSize;

  //
  // Send data to SMM.
  //
  Status = SendCommunicateBuffer (SmmCommunicateHeader, PayloadSize);

  //
  // Get data from SMM
  //
  *PrivateDataSize = SmmFtwGetLastWriteHeader->PrivateDataSize;
  if ((Status == EFI_SUCCESS) || (Status == EFI_BUFFER_TOO_SMALL)) {
    *Lba      = SmmFtwGetLastWriteHeader->Lba;
    *Offset   = SmmFtwGetLastWriteHeader->Offset;
    *Length   = SmmFtwGetLastWriteHeader->Length;
    *Complete = SmmFtwGetLastWriteHeader->Complete;
    CopyGuid (CallerId, &SmmFtwGetLastWriteHeader->CallerId);
    if (Status == EFI_SUCCESS) {
      CopyMem (PrivateData, SmmFtwGetLastWriteHeader->Data, *PrivateDataSize);
    }
  } else if (Status == EFI_NOT_FOUND) {
    *Complete = SmmFtwGetLastWriteHeader->Complete;
  }

  FreePool (SmmCommunicateHeader);
  return Status;
}

/**
  SMM Fault Tolerant Write Protocol notification event handler.

  Install Fault Tolerant Write Protocol.

  @param[in] Event    Event whose notification function is being invoked.
  @param[in] Context  Pointer to the notification function's context.
**/
VOID
EFIAPI
SmmFtwReady (
  IN  EFI_EVENT  Event,
  IN  VOID       *Context
  )
{
  EFI_STATUS                         Status;
  EFI_FAULT_TOLERANT_WRITE_PROTOCOL  *FtwProtocol;

  //
  // Just return to avoid install SMM FaultTolerantWriteProtocol again
  // if Fault Tolerant Write protocol had been installed.
  //
  Status = gBS->LocateProtocol (&gEfiFaultTolerantWriteProtocolGuid, NULL, (VOID **)&FtwProtocol);
  if (!EFI_ERROR (Status)) {
    return;
  }

  Status = gBS->LocateProtocol (&gEfiMmCommunication2ProtocolGuid, NULL, (VOID **)&mMmCommunication2);
  ASSERT_EFI_ERROR (Status);

  //
  // Install protocol interface
  //
  Status = gBS->InstallProtocolInterface (
                  &mHandle,
                  &gEfiFaultTolerantWriteProtocolGuid,
                  EFI_NATIVE_INTERFACE,
                  &mFaultTolerantWriteDriver
                  );
  ASSERT_EFI_ERROR (Status);

  Status = gBS->CloseEvent (Event);
  ASSERT_EFI_ERROR (Status);
}

/**
  The driver entry point for Fault Tolerant Write driver.

  The function does the necessary initialization work.

  @param[in]  ImageHandle       The firmware allocated handle for the UEFI image.
  @param[in]  SystemTable       A pointer to the EFI system table.

  @retval     EFI_SUCCESS       This function always return EFI_SUCCESS.

**/
EFI_STATUS
EFIAPI
FaultTolerantWriteSmmInitialize (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  VOID  *SmmFtwRegistration;

  //
  // Smm FTW driver is ready
  //
  EfiCreateProtocolNotifyEvent (
    &gEfiSmmFaultTolerantWriteProtocolGuid,
    TPL_CALLBACK,
    SmmFtwReady,
    NULL,
    &SmmFtwRegistration
    );

  return EFI_SUCCESS;
}
