/** @file

  Stateful and implicitly initialized fw_cfg library implementation.

  Copyright (C) 2013 - 2014, Red Hat, Inc.
  Copyright (c) 2011 - 2013, Intel Corporation. All rights reserved.<BR>
  (C) Copyright 2021 Hewlett Packard Enterprise Development LP<BR>

  SPDX-License-Identifier: BSD-2-Clause-Patent
**/

#include <Uefi.h>

#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/IoLib.h>
#include <Library/QemuFwCfgLib.h>
#include <Library/UefiBootServicesTableLib.h>

#include <Protocol/FdtClient.h>

STATIC UINTN  mFwCfgSelectorAddress;
STATIC UINTN  mFwCfgDataAddress;
STATIC UINTN  mFwCfgDmaAddress;

/**
  Reads firmware configuration bytes into a buffer

  @param[in] Size    Size in bytes to read
  @param[in] Buffer  Buffer to store data into  (OPTIONAL if Size is 0)

**/
typedef
VOID(EFIAPI READ_BYTES_FUNCTION)(
  IN UINTN Size,
  IN VOID  *Buffer OPTIONAL
  );

/**
  Writes bytes from a buffer to firmware configuration

  @param[in] Size    Size in bytes to write
  @param[in] Buffer  Buffer to transfer data from (OPTIONAL if Size is 0)

**/
typedef
VOID(EFIAPI WRITE_BYTES_FUNCTION)(
  IN UINTN Size,
  IN VOID  *Buffer OPTIONAL
  );

/**
  Skips bytes in firmware configuration

  @param[in] Size  Size in bytes to skip

**/
typedef
VOID(EFIAPI SKIP_BYTES_FUNCTION)(
  IN UINTN Size
  );

//
// Forward declaration of the two implementations we have.
//
STATIC READ_BYTES_FUNCTION   MmioReadBytes;
STATIC WRITE_BYTES_FUNCTION  MmioWriteBytes;
STATIC SKIP_BYTES_FUNCTION   MmioSkipBytes;
STATIC READ_BYTES_FUNCTION   DmaReadBytes;
STATIC WRITE_BYTES_FUNCTION  DmaWriteBytes;
STATIC SKIP_BYTES_FUNCTION   DmaSkipBytes;

//
// These correspond to the implementation we detect at runtime.
//
STATIC READ_BYTES_FUNCTION   *InternalQemuFwCfgReadBytes  = MmioReadBytes;
STATIC WRITE_BYTES_FUNCTION  *InternalQemuFwCfgWriteBytes = MmioWriteBytes;
STATIC SKIP_BYTES_FUNCTION   *InternalQemuFwCfgSkipBytes  = MmioSkipBytes;

/**
  Returns a boolean indicating if the firmware configuration interface
  is available or not.

  This function may change fw_cfg state.

  @retval TRUE   The interface is available
  @retval FALSE  The interface is not available

**/
BOOLEAN
EFIAPI
QemuFwCfgIsAvailable (
  VOID
  )
{
  return (BOOLEAN)(mFwCfgSelectorAddress != 0 && mFwCfgDataAddress != 0);
}

RETURN_STATUS
EFIAPI
QemuFwCfgInitialize (
  VOID
  )
{
  EFI_STATUS           Status;
  FDT_CLIENT_PROTOCOL  *FdtClient;
  CONST UINT64         *Reg;
  UINT32               RegSize;
  UINTN                AddressCells, SizeCells;
  UINT64               FwCfgSelectorAddress;
  UINT64               FwCfgSelectorSize;
  UINT64               FwCfgDataAddress;
  UINT64               FwCfgDataSize;
  UINT64               FwCfgDmaAddress;
  UINT64               FwCfgDmaSize;

  Status = gBS->LocateProtocol (
                  &gFdtClientProtocolGuid,
                  NULL,
                  (VOID **)&FdtClient
                  );
  ASSERT_EFI_ERROR (Status);

  Status = FdtClient->FindCompatibleNodeReg (
                        FdtClient,
                        "qemu,fw-cfg-mmio",
                        (CONST VOID **)&Reg,
                        &AddressCells,
                        &SizeCells,
                        &RegSize
                        );
  if (EFI_ERROR (Status)) {
    DEBUG ((
      DEBUG_WARN,
      "%a: No 'qemu,fw-cfg-mmio' compatible DT node found (Status == %r)\n",
      __func__,
      Status
      ));
    return EFI_SUCCESS;
  }

  ASSERT (AddressCells == 2);
  ASSERT (SizeCells == 2);
  ASSERT (RegSize == 2 * sizeof (UINT64));

  FwCfgDataAddress     = SwapBytes64 (Reg[0]);
  FwCfgDataSize        = 8;
  FwCfgSelectorAddress = FwCfgDataAddress + FwCfgDataSize;
  FwCfgSelectorSize    = 2;

  //
  // The following ASSERT()s express
  //
  //   Address + Size - 1 <= MAX_UINTN
  //
  // for both registers, that is, that the last byte in each MMIO range is
  // expressible as a MAX_UINTN. The form below is mathematically
  // equivalent, and it also prevents any unsigned overflow before the
  // comparison.
  //
  ASSERT (FwCfgSelectorAddress <= MAX_UINTN - FwCfgSelectorSize + 1);
  ASSERT (FwCfgDataAddress     <= MAX_UINTN - FwCfgDataSize     + 1);

  mFwCfgSelectorAddress = FwCfgSelectorAddress;
  mFwCfgDataAddress     = FwCfgDataAddress;

  DEBUG ((
    DEBUG_INFO,
    "Found FwCfg @ 0x%Lx/0x%Lx\n",
    FwCfgSelectorAddress,
    FwCfgDataAddress
    ));

  if (SwapBytes64 (Reg[1]) >= 0x18) {
    FwCfgDmaAddress = FwCfgDataAddress + 0x10;
    FwCfgDmaSize    = 0x08;

    //
    // See explanation above.
    //
    ASSERT (FwCfgDmaAddress <= MAX_UINTN - FwCfgDmaSize + 1);

    DEBUG ((DEBUG_INFO, "Found FwCfg DMA @ 0x%Lx\n", FwCfgDmaAddress));
  } else {
    FwCfgDmaAddress = 0;
  }

  if (QemuFwCfgIsAvailable ()) {
    UINT32  Signature;

    QemuFwCfgSelectItem (QemuFwCfgItemSignature);
    Signature = QemuFwCfgRead32 ();
    if (Signature == SIGNATURE_32 ('Q', 'E', 'M', 'U')) {
      //
      // For DMA support, we require the DTB to advertise the register, and the
      // feature bitmap (which we read without DMA) to confirm the feature.
      //
      if (FwCfgDmaAddress != 0) {
        UINT32  Features;

        QemuFwCfgSelectItem (QemuFwCfgItemInterfaceVersion);
        Features = QemuFwCfgRead32 ();
        if ((Features & FW_CFG_F_DMA) != 0) {
          mFwCfgDmaAddress            = FwCfgDmaAddress;
          InternalQemuFwCfgReadBytes  = DmaReadBytes;
          InternalQemuFwCfgWriteBytes = DmaWriteBytes;
          InternalQemuFwCfgSkipBytes  = DmaSkipBytes;
        }
      }
    } else {
      mFwCfgSelectorAddress = 0;
      mFwCfgDataAddress     = 0;
    }
  }

  return RETURN_SUCCESS;
}

/**
  Selects a firmware configuration item for reading.

  Following this call, any data read from this item will start from the
  beginning of the configuration item's data.

  @param[in] QemuFwCfgItem  Firmware Configuration item to read

**/
VOID
EFIAPI
QemuFwCfgSelectItem (
  IN FIRMWARE_CONFIG_ITEM  QemuFwCfgItem
  )
{
  if (QemuFwCfgIsAvailable ()) {
    MmioWrite16 (mFwCfgSelectorAddress, SwapBytes16 ((UINT16)QemuFwCfgItem));
  }
}

/**
  Slow READ_BYTES_FUNCTION.
**/
STATIC
VOID
EFIAPI
MmioReadBytes (
  IN UINTN  Size,
  IN VOID   *Buffer OPTIONAL
  )
{
  UINTN  Left;
  UINT8  *Ptr;
  UINT8  *End;

 #if defined (MDE_CPU_AARCH64) || defined (MDE_CPU_RISCV64)
  Left = Size & 7;
 #else
  Left = Size & 3;
 #endif

  Size -= Left;
  Ptr   = Buffer;
  End   = Ptr + Size;

 #if defined (MDE_CPU_AARCH64) || defined (MDE_CPU_RISCV64)
  while (Ptr < End) {
    *(UINT64 *)Ptr = MmioRead64 (mFwCfgDataAddress);
    Ptr           += 8;
  }

  if (Left & 4) {
    *(UINT32 *)Ptr = MmioRead32 (mFwCfgDataAddress);
    Ptr           += 4;
  }

 #else
  while (Ptr < End) {
    *(UINT32 *)Ptr = MmioRead32 (mFwCfgDataAddress);
    Ptr           += 4;
  }

 #endif

  if (Left & 2) {
    *(UINT16 *)Ptr = MmioRead16 (mFwCfgDataAddress);
    Ptr           += 2;
  }

  if (Left & 1) {
    *Ptr = MmioRead8 (mFwCfgDataAddress);
  }
}

/**
  Transfer an array of bytes, or skip a number of bytes, using the DMA
  interface.

  @param[in]     Size     Size in bytes to transfer or skip.

  @param[in,out] Buffer   Buffer to read data into or write data from. Ignored,
                          and may be NULL, if Size is zero, or Control is
                          FW_CFG_DMA_CTL_SKIP.

  @param[in]     Control  One of the following:
                          FW_CFG_DMA_CTL_WRITE - write to fw_cfg from Buffer.
                          FW_CFG_DMA_CTL_READ  - read from fw_cfg into Buffer.
                          FW_CFG_DMA_CTL_SKIP  - skip bytes in fw_cfg.
**/
STATIC
VOID
DmaTransferBytes (
  IN     UINTN   Size,
  IN OUT VOID    *Buffer OPTIONAL,
  IN     UINT32  Control
  )
{
  volatile FW_CFG_DMA_ACCESS  Access;
  UINT32                      Status;

  ASSERT (
    Control == FW_CFG_DMA_CTL_WRITE || Control == FW_CFG_DMA_CTL_READ ||
    Control == FW_CFG_DMA_CTL_SKIP
    );

  if (Size == 0) {
    return;
  }

  ASSERT (Size <= MAX_UINT32);

  Access.Control = SwapBytes32 (Control);
  Access.Length  = SwapBytes32 ((UINT32)Size);
  Access.Address = SwapBytes64 ((UINT64)(UINTN)Buffer);

  //
  // We shouldn't start the transfer before setting up Access.
  //
  MemoryFence ();

  //
  // This will fire off the transfer.
  //
 #if defined (MDE_CPU_AARCH64) || defined (MDE_CPU_RISCV64)
  MmioWrite64 (mFwCfgDmaAddress, SwapBytes64 ((UINT64)&Access));
 #else
  MmioWrite32 ((UINT32)(mFwCfgDmaAddress + 4), SwapBytes32 ((UINT32)&Access));
 #endif

  //
  // We shouldn't look at Access.Control before starting the transfer.
  //
  MemoryFence ();

  do {
    Status = SwapBytes32 (Access.Control);
    ASSERT ((Status & FW_CFG_DMA_CTL_ERROR) == 0);
  } while (Status != 0);

  //
  // The caller will want to access the transferred data.
  //
  MemoryFence ();
}

/**
  Fast READ_BYTES_FUNCTION.
**/
STATIC
VOID
EFIAPI
DmaReadBytes (
  IN UINTN  Size,
  IN VOID   *Buffer OPTIONAL
  )
{
  DmaTransferBytes (Size, Buffer, FW_CFG_DMA_CTL_READ);
}

/**
  Reads firmware configuration bytes into a buffer

  If called multiple times, then the data read will continue at the offset of
  the firmware configuration item where the previous read ended.

  @param[in] Size    Size in bytes to read
  @param[in] Buffer  Buffer to store data into

**/
VOID
EFIAPI
QemuFwCfgReadBytes (
  IN UINTN  Size,
  IN VOID   *Buffer
  )
{
  if (QemuFwCfgIsAvailable ()) {
    InternalQemuFwCfgReadBytes (Size, Buffer);
  } else {
    ZeroMem (Buffer, Size);
  }
}

/**
  Slow WRITE_BYTES_FUNCTION.
**/
STATIC
VOID
EFIAPI
MmioWriteBytes (
  IN UINTN  Size,
  IN VOID   *Buffer OPTIONAL
  )
{
  UINTN  Idx;

  for (Idx = 0; Idx < Size; ++Idx) {
    MmioWrite8 (mFwCfgDataAddress, ((UINT8 *)Buffer)[Idx]);
  }
}

/**
  Fast WRITE_BYTES_FUNCTION.
**/
STATIC
VOID
EFIAPI
DmaWriteBytes (
  IN UINTN  Size,
  IN VOID   *Buffer OPTIONAL
  )
{
  DmaTransferBytes (Size, Buffer, FW_CFG_DMA_CTL_WRITE);
}

/**
  Write firmware configuration bytes from a buffer

  If called multiple times, then the data written will continue at the offset
  of the firmware configuration item where the previous write ended.

  @param[in] Size    Size in bytes to write
  @param[in] Buffer  Buffer to read data from

**/
VOID
EFIAPI
QemuFwCfgWriteBytes (
  IN UINTN  Size,
  IN VOID   *Buffer
  )
{
  if (QemuFwCfgIsAvailable ()) {
    InternalQemuFwCfgWriteBytes (Size, Buffer);
  }
}

/**
  Slow SKIP_BYTES_FUNCTION.
**/
STATIC
VOID
EFIAPI
MmioSkipBytes (
  IN UINTN  Size
  )
{
  UINTN  ChunkSize;
  UINT8  SkipBuffer[256];

  //
  // Emulate the skip by reading data in chunks, and throwing it away. The
  // implementation below doesn't affect the static data footprint for client
  // modules. Large skips are not expected, therefore this fallback is not
  // performance critical. The size of SkipBuffer is thought not to exert a
  // large pressure on the stack.
  //
  while (Size > 0) {
    ChunkSize = MIN (Size, sizeof SkipBuffer);
    MmioReadBytes (ChunkSize, SkipBuffer);
    Size -= ChunkSize;
  }
}

/**
  Fast SKIP_BYTES_FUNCTION.
**/
STATIC
VOID
EFIAPI
DmaSkipBytes (
  IN UINTN  Size
  )
{
  DmaTransferBytes (Size, NULL, FW_CFG_DMA_CTL_SKIP);
}

/**
  Skip bytes in the firmware configuration item.

  Increase the offset of the firmware configuration item without transferring
  bytes between the item and a caller-provided buffer. Subsequent read, write
  or skip operations will commence at the increased offset.

  @param[in] Size  Number of bytes to skip.
**/
VOID
EFIAPI
QemuFwCfgSkipBytes (
  IN UINTN  Size
  )
{
  if (QemuFwCfgIsAvailable ()) {
    InternalQemuFwCfgSkipBytes (Size);
  }
}

/**
  Reads a UINT8 firmware configuration value

  @return  Value of Firmware Configuration item read

**/
UINT8
EFIAPI
QemuFwCfgRead8 (
  VOID
  )
{
  UINT8  Result;

  QemuFwCfgReadBytes (sizeof Result, &Result);
  return Result;
}

/**
  Reads a UINT16 firmware configuration value

  @return  Value of Firmware Configuration item read

**/
UINT16
EFIAPI
QemuFwCfgRead16 (
  VOID
  )
{
  UINT16  Result;

  QemuFwCfgReadBytes (sizeof Result, &Result);
  return Result;
}

/**
  Reads a UINT32 firmware configuration value

  @return  Value of Firmware Configuration item read

**/
UINT32
EFIAPI
QemuFwCfgRead32 (
  VOID
  )
{
  UINT32  Result;

  QemuFwCfgReadBytes (sizeof Result, &Result);
  return Result;
}

/**
  Reads a UINT64 firmware configuration value

  @return  Value of Firmware Configuration item read

**/
UINT64
EFIAPI
QemuFwCfgRead64 (
  VOID
  )
{
  UINT64  Result;

  QemuFwCfgReadBytes (sizeof Result, &Result);
  return Result;
}

/**
  Find the configuration item corresponding to the firmware configuration file.

  @param[in]  Name  Name of file to look up.
  @param[out] Item  Configuration item corresponding to the file, to be passed
                    to QemuFwCfgSelectItem ().
  @param[out] Size  Number of bytes in the file.

  @retval RETURN_SUCCESS      If file is found.
  @retval RETURN_NOT_FOUND    If file is not found.
  @retval RETURN_UNSUPPORTED  If firmware configuration is unavailable.

**/
RETURN_STATUS
EFIAPI
QemuFwCfgFindFile (
  IN   CONST CHAR8           *Name,
  OUT  FIRMWARE_CONFIG_ITEM  *Item,
  OUT  UINTN                 *Size
  )
{
  UINT32  Count;
  UINT32  Idx;

  if (!QemuFwCfgIsAvailable ()) {
    return RETURN_UNSUPPORTED;
  }

  QemuFwCfgSelectItem (QemuFwCfgItemFileDir);
  Count = SwapBytes32 (QemuFwCfgRead32 ());

  for (Idx = 0; Idx < Count; ++Idx) {
    UINT32  FileSize;
    UINT16  FileSelect;
    CHAR8   FName[QEMU_FW_CFG_FNAME_SIZE];

    FileSize   = QemuFwCfgRead32 ();
    FileSelect = QemuFwCfgRead16 ();
    QemuFwCfgRead16 (); // skip the field called "reserved"
    InternalQemuFwCfgReadBytes (sizeof (FName), FName);

    if (AsciiStrCmp (Name, FName) == 0) {
      *Item = (FIRMWARE_CONFIG_ITEM)SwapBytes16 (FileSelect);
      *Size = SwapBytes32 (FileSize);
      return RETURN_SUCCESS;
    }
  }

  return RETURN_NOT_FOUND;
}
