/** @file
  Implementation for iSCSI Boot Firmware Table publication.

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

**/

#include "IScsiImpl.h"

BOOLEAN mIbftInstalled = FALSE;
UINTN   mTableKey;

/**
  Initialize the header of the iSCSI Boot Firmware Table.

  @param[out]  Header     The header of the iSCSI Boot Firmware Table.
  @param[in]   OemId      The OEM ID.
  @param[in]   OemTableId The OEM table ID for the iBFT.

**/
VOID
IScsiInitIbfTableHeader (
  OUT EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_HEADER   *Header,
  IN  UINT8                                       *OemId,
  IN  UINT64                                      *OemTableId
  )
{
  Header->Signature = EFI_ACPI_3_0_ISCSI_BOOT_FIRMWARE_TABLE_SIGNATURE;
  Header->Length    = IBFT_HEAP_OFFSET;
  Header->Revision  = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_REVISION;
  Header->Checksum  = 0;

  CopyMem (Header->OemId, OemId, sizeof (Header->OemId));
  CopyMem (&Header->OemTableId, OemTableId, sizeof (UINT64));
}


/**
  Initialize the control section of the iSCSI Boot Firmware Table.

  @param[in]  Table       The ACPI table.

**/
VOID
IScsiInitControlSection (
  IN EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_HEADER  *Table
  )
{
  EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_CONTROL_STRUCTURE  *Control;
  UINTN                                                 NumOffset;

  Control = (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_CONTROL_STRUCTURE *) (Table + 1);

  Control->Header.StructureId = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_CONTROL_STRUCTURE_ID;
  Control->Header.Version     = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_CONTROL_STRUCTURE_VERSION;
  Control->Header.Length      = (UINT16) sizeof (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_CONTROL_STRUCTURE);

  //
  // If in multipathing mode, enable the Boot Failover Flag.
  // If in single path mode, disable it. Mix-model is not allowed.
  //
  // BUGBUG: if Boot Failover Flag is set to 1, the OS installer cannot
  // find the iSCSI mapped disk. So still keep not set for single path mode.
  //
  if (mPrivate->EnableMpio) {
    Control->Header.Flags = 0;
    NumOffset = 2 * (mPrivate->MpioCount - mPrivate->Krb5MpioCount);
  } else {
    NumOffset = 2 * mPrivate->ValidSinglePathCount;
  }

  //
  // Each attempt occupies two offsets: one for the NIC section;
  // the other for the Target section.
  //
  if (NumOffset > 4) {
    //
    // Need expand the control section if more than 2 NIC/Target attempts
    // exist.
    //
    Control->Header.Length = (UINT16) (Control->Header.Length + (NumOffset - 4) * sizeof (UINT16));
  }
}


/**
  Add one item into the heap.

  @param[in, out]  Heap  On input, the current address of the heap. On output, the address of
                         the heap after the item is added.
  @param[in]       Data  The data to add into the heap.
  @param[in]       Len   Length of the Data in byte.

**/
VOID
IScsiAddHeapItem (
  IN OUT UINT8  **Heap,
  IN     VOID   *Data,
  IN     UINTN  Len
  )
{
  //
  // Add one byte for the NULL delimiter.
  //
  *Heap -= Len + 1;

  CopyMem (*Heap, Data, Len);
  *(*Heap + Len) = 0;
}


/**
  Fill the Initiator section of the iSCSI Boot Firmware Table.

  @param[in]       Table    The ACPI table.
  @param[in, out]  Heap     The heap.

**/
VOID
IScsiFillInitiatorSection (
  IN     EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_HEADER  *Table,
  IN OUT UINT8                                      **Heap
  )
{
  EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_CONTROL_STRUCTURE    *Control;
  EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_INITIATOR_STRUCTURE  *Initiator;

  Control = (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_CONTROL_STRUCTURE *) (Table + 1);

  //
  // Initiator section immediately follows the control section.
  //
  Initiator = (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_INITIATOR_STRUCTURE *)
              ((UINT8 *) Control + IBFT_ROUNDUP (Control->Header.Length));

  Control->InitiatorOffset = (UINT16) ((UINTN) Initiator - (UINTN) Table);

  Initiator->Header.StructureId = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_INITIATOR_STRUCTURE_ID;
  Initiator->Header.Version     = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_INITIATOR_STRUCTURE_VERSION;
  Initiator->Header.Length      = (UINT16) sizeof (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_INITIATOR_STRUCTURE);
  Initiator->Header.Flags       = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_INITIATOR_STRUCTURE_FLAG_BLOCK_VALID |
                                  EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_INITIATOR_STRUCTURE_FLAG_BOOT_SELECTED;

  //
  // Fill the iSCSI Initiator Name into the heap.
  //
  IScsiAddHeapItem (Heap, mPrivate->InitiatorName, mPrivate->InitiatorNameLength - 1);

  Initiator->IScsiNameLength  = (UINT16) (mPrivate->InitiatorNameLength - 1);
  Initiator->IScsiNameOffset  = (UINT16) ((UINTN) *Heap - (UINTN) Table);
}


/**
  Map the v4 IP address into v6 IP address.

  @param[in]   V4 The v4 IP address.
  @param[out]  V6 The v6 IP address.

**/
VOID
IScsiMapV4ToV6Addr (
  IN  EFI_IPv4_ADDRESS *V4,
  OUT EFI_IPv6_ADDRESS *V6
  )
{
  UINTN Index;

  ZeroMem (V6, sizeof (EFI_IPv6_ADDRESS));

  V6->Addr[10]  = 0xff;
  V6->Addr[11]  = 0xff;

  for (Index = 0; Index < 4; Index++) {
    V6->Addr[12 + Index] = V4->Addr[Index];
  }
}


/**
  Fill the NIC and target sections in iSCSI Boot Firmware Table.

  @param[in]       Table    The buffer of the ACPI table.
  @param[in, out]  Heap     The heap buffer used to store the variable length
                            parameters such as iSCSI name.

**/
VOID
IScsiFillNICAndTargetSections (
  IN     EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_HEADER  *Table,
  IN OUT UINT8                                      **Heap
  )
{
  EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_CONTROL_STRUCTURE  *Control;
  EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE      *Nic;
  EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE   *Target;
  ISCSI_SESSION_CONFIG_NVDATA                           *NvData;
  ISCSI_CHAP_AUTH_CONFIG_NVDATA                         *AuthConfig;
  UINT16                                                *SectionOffset;
  UINTN                                                 Index;
  UINT16                                                Length;
  LIST_ENTRY                                            *Entry;
  ISCSI_ATTEMPT_CONFIG_NVDATA                           *Attempt;
  ISCSI_NIC_INFO                                        *NicInfo;
  BOOLEAN                                               Flag;

  //
  // Get the offset of the first Nic and Target section.
  //
  Control = (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_CONTROL_STRUCTURE *) (Table + 1);
  Nic     = (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE *) ((UINTN) Table +
          Control->InitiatorOffset + IBFT_ROUNDUP (sizeof (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_INITIATOR_STRUCTURE)));
  Target  = (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE *) ((UINTN) Nic +
          IBFT_ROUNDUP (sizeof (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE)));

  SectionOffset = &Control->NIC0Offset;

  Index = 0;
  Flag  = TRUE;

  NET_LIST_FOR_EACH (Entry, &mPrivate->AttemptConfigs) {
    if (Index == 0) {
      //
      // First entry should be boot selected entry.
      //
      Attempt = IScsiConfigGetAttemptByConfigIndex (mPrivate->BootSelectedIndex);
      if (Attempt == NULL) {
        //
        // First boot selected entry can not be found.
        //
        break;
      }

      ASSERT (Attempt->SessionConfigData.Enabled != ISCSI_DISABLED);

    } else {
      if (Index == 1 && Flag) {
        Entry = mPrivate->AttemptConfigs.ForwardLink;
        Flag = FALSE;
      }

      Attempt = NET_LIST_USER_STRUCT (Entry, ISCSI_ATTEMPT_CONFIG_NVDATA, Link);
      if (Attempt->AttemptConfigIndex == mPrivate->BootSelectedIndex) {
        continue;
      }
    }

    if (Attempt->SessionConfigData.Enabled == ISCSI_DISABLED) {
      continue;
    }

    //
    // Krb5 attempt will not be recorded in iBFT.
    //
    if (Attempt->AuthenticationType == ISCSI_AUTH_TYPE_KRB) {
      continue;
    }

    //
    // If multipath mode is enabled, only the attempts in MPIO will be recorded in iBFT.
    //
    if (mPrivate->EnableMpio && Attempt->SessionConfigData.Enabled != ISCSI_ENABLED_FOR_MPIO) {
      continue;
    }

    //
    // Only the valid attempts will be recorded.
    //
    if (!Attempt->ValidiBFTPath) {
      continue;
    }

    NvData     = &Attempt->SessionConfigData;
    AuthConfig = &Attempt->AuthConfigData.CHAP;

    //
    // Fill the Nic section.
    //

    Nic->Header.StructureId = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE_ID;
    Nic->Header.Version     = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE_VERSION;
    Nic->Header.Length      = (UINT16) sizeof (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE);
    Nic->Header.Index       = (UINT8) Index;
    Nic->Header.Flags       = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE_FLAG_BLOCK_VALID |
                            EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE_FLAG_GLOBAL;

    if (Index == 0) {
      Nic->Header.Flags    |= EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE_FLAG_BOOT_SELECTED;
    }

    if (NvData->InitiatorInfoFromDhcp) {
      Nic->Origin = IpPrefixOriginDhcp;
    } else {
      Nic->Origin = IpPrefixOriginManual;
    }

    if (NvData->IpMode == IP_MODE_IP4 || NvData->IpMode == IP_MODE_AUTOCONFIG) {
      //
      // Get the subnet mask prefix length.
      //
      Nic->SubnetMaskPrefixLength = IScsiGetSubnetMaskPrefixLength (&NvData->SubnetMask);

      //
      // Map the various v4 addresses into v6 addresses.
      //
      IScsiMapV4ToV6Addr (&NvData->LocalIp.v4, &Nic->Ip);
      IScsiMapV4ToV6Addr (&NvData->Gateway.v4, &Nic->Gateway);
      IScsiMapV4ToV6Addr (&Attempt->PrimaryDns.v4, &Nic->PrimaryDns);
      IScsiMapV4ToV6Addr (&Attempt->SecondaryDns.v4, &Nic->SecondaryDns);
      IScsiMapV4ToV6Addr (&Attempt->DhcpServer.v4, &Nic->DhcpServer);

    } else if (NvData->IpMode == IP_MODE_IP6 || NvData->IpMode == IP_MODE_AUTOCONFIG) {

      Nic->SubnetMaskPrefixLength = NvData->PrefixLength;
      CopyMem (&Nic->Ip, &NvData->LocalIp, sizeof (EFI_IPv6_ADDRESS));
      CopyMem (&Nic->Gateway, &NvData->Gateway, sizeof (EFI_IPv6_ADDRESS));
      CopyMem (&Nic->PrimaryDns, &Attempt->PrimaryDns, sizeof (EFI_IPv6_ADDRESS));
      CopyMem (&Nic->SecondaryDns, &Attempt->SecondaryDns, sizeof (EFI_IPv6_ADDRESS));
      CopyMem (&Nic->DhcpServer, &Attempt->DhcpServer, sizeof (EFI_IPv6_ADDRESS));

    } else {
      ASSERT (FALSE);
    }

    //
    // Get Nic Info: VLAN tag, Mac address, PCI location.
    //
    NicInfo = IScsiGetNicInfoByIndex (Attempt->NicIndex);
    ASSERT (NicInfo != NULL);

    Nic->VLanTag = NicInfo->VlanId;
    CopyMem (Nic->Mac, &NicInfo->PermanentAddress, sizeof (Nic->Mac));
    Nic->PciLocation = (UINT16) ((NicInfo->BusNumber << 8)    |
                                 (NicInfo->DeviceNumber << 3) | NicInfo->FunctionNumber);
    *SectionOffset    = (UINT16) ((UINTN) Nic - (UINTN) Table);
    SectionOffset++;

    //
    // Fill the Target section.
    //

    Target->Header.StructureId  = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE_ID;
    Target->Header.Version      = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE_VERSION;
    Target->Header.Length       = (UINT16) sizeof (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE);
    Target->Header.Index        = (UINT8) Index;
    Target->Header.Flags        = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE_FLAG_BLOCK_VALID;

    if (Index == 0) {
      Target->Header.Flags     |= EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE_FLAG_BOOT_SELECTED;
    }

    Target->Port                = NvData->TargetPort;

    if (Attempt->AuthenticationType == ISCSI_AUTH_TYPE_CHAP) {
      if (AuthConfig->CHAPType == ISCSI_CHAP_UNI) {
        Target->CHAPType = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE_CHAP_TYPE_CHAP;
      } else if (AuthConfig->CHAPType == ISCSI_CHAP_MUTUAL) {
        Target->CHAPType = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE_CHAP_TYPE_MUTUAL_CHAP;
      }
    } else if (Attempt->AuthenticationType == ISCSI_AUTH_TYPE_NONE) {
      Target->CHAPType = EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE_CHAP_TYPE_NO_CHAP;
    }

    Target->NicIndex            = (UINT8) Index;

    if (NvData->IpMode == IP_MODE_IP4 || NvData->IpMode == IP_MODE_AUTOCONFIG) {
      IScsiMapV4ToV6Addr (&NvData->TargetIp.v4, &Target->Ip);
    } else if (NvData->IpMode == IP_MODE_IP6 || NvData->IpMode == IP_MODE_AUTOCONFIG) {
      CopyMem (&Target->Ip, &NvData->TargetIp, sizeof (EFI_IPv6_ADDRESS));
    } else {
      ASSERT (FALSE);
    }

    CopyMem (Target->BootLun, NvData->BootLun, sizeof (Target->BootLun));

    //
    // Target iSCSI Name, CHAP name/secret, reverse CHAP name/secret.
    //
    Length = (UINT16) AsciiStrLen (NvData->TargetName);
    IScsiAddHeapItem (Heap, NvData->TargetName, Length);

    Target->IScsiNameLength = Length;
    Target->IScsiNameOffset = (UINT16) ((UINTN) *Heap - (UINTN) Table);

    if (Attempt->AuthenticationType == ISCSI_AUTH_TYPE_CHAP) {
      //
      // CHAP Name
      //
      Length = (UINT16) AsciiStrLen (AuthConfig->CHAPName);
      IScsiAddHeapItem (Heap, AuthConfig->CHAPName, Length);
      Target->CHAPNameLength  = Length;
      Target->CHAPNameOffset  = (UINT16) ((UINTN) *Heap - (UINTN) Table);

      //
      // CHAP Secret
      //
      Length = (UINT16) AsciiStrLen (AuthConfig->CHAPSecret);
      IScsiAddHeapItem (Heap, AuthConfig->CHAPSecret, Length);
      Target->CHAPSecretLength  = Length;
      Target->CHAPSecretOffset  = (UINT16) ((UINTN) *Heap - (UINTN) Table);

      if (Target->CHAPType == EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE_CHAP_TYPE_MUTUAL_CHAP) {
        //
        // Reverse CHAP Name.
        //
        Length = (UINT16) AsciiStrLen (AuthConfig->ReverseCHAPName);
        IScsiAddHeapItem (Heap, AuthConfig->ReverseCHAPName, Length);
        Target->ReverseCHAPNameLength = Length;
        Target->ReverseCHAPNameOffset = (UINT16) ((UINTN) *Heap - (UINTN) Table);

        //
        // Reverse CHAP Secret.
        //
        Length = (UINT16) AsciiStrLen (AuthConfig->ReverseCHAPSecret);
        IScsiAddHeapItem (Heap, AuthConfig->ReverseCHAPSecret, Length);
        Target->ReverseCHAPSecretLength = Length;
        Target->ReverseCHAPSecretOffset = (UINT16) ((UINTN) *Heap - (UINTN) Table);
      }
    }

    *SectionOffset = (UINT16) ((UINTN) Target - (UINTN) Table);
    SectionOffset++;

    //
    // Advance to the next NIC/Target pair.
    //
    Nic    = (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE *) ((UINTN) Target +
           IBFT_ROUNDUP (sizeof (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE)));
    Target = (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_TARGET_STRUCTURE *) ((UINTN) Nic +
           IBFT_ROUNDUP (sizeof (EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_NIC_STRUCTURE)));

    Index++;
  }
}


/**
  Publish and remove the iSCSI Boot Firmware Table according to the iSCSI
  session status.

**/
VOID
IScsiPublishIbft (
  IN VOID
  )
{
  EFI_STATUS                                    Status;
  EFI_ACPI_TABLE_PROTOCOL                       *AcpiTableProtocol;
  EFI_ACPI_ISCSI_BOOT_FIRMWARE_TABLE_HEADER     *Table;
  EFI_ACPI_3_0_ROOT_SYSTEM_DESCRIPTION_POINTER  *Rsdp;
  EFI_ACPI_DESCRIPTION_HEADER                   *Rsdt;
  EFI_ACPI_DESCRIPTION_HEADER                   *Xsdt;
  UINT8                                         *Heap;
  UINT8                                         Checksum;

  Rsdt = NULL;
  Xsdt = NULL;

  Status = gBS->LocateProtocol (&gEfiAcpiTableProtocolGuid, NULL, (VOID **) &AcpiTableProtocol);
  if (EFI_ERROR (Status)) {
    return ;
  }

  //
  // Find ACPI table RSD_PTR from the system table.
  //
  Status = EfiGetSystemConfigurationTable (&gEfiAcpiTableGuid, (VOID **) &Rsdp);
  if (EFI_ERROR (Status)) {
    Status = EfiGetSystemConfigurationTable (&gEfiAcpi10TableGuid, (VOID **) &Rsdp);
  }

  if (EFI_ERROR (Status) || (Rsdp == NULL)) {
    return ;
  } else if (Rsdp->Revision >= EFI_ACPI_2_0_ROOT_SYSTEM_DESCRIPTION_POINTER_REVISION && Rsdp->XsdtAddress != 0) {
    Xsdt = (EFI_ACPI_DESCRIPTION_HEADER *) (UINTN) Rsdp->XsdtAddress;
  } else if (Rsdp->RsdtAddress != 0) {
    Rsdt = (EFI_ACPI_DESCRIPTION_HEADER *) (UINTN) Rsdp->RsdtAddress;
  }

  if ((Xsdt == NULL) && (Rsdt == NULL)) {
    return ;
  }

  if (mIbftInstalled) {
    Status = AcpiTableProtocol->UninstallAcpiTable (
                                  AcpiTableProtocol,
                                  mTableKey
                                  );
    if (EFI_ERROR (Status)) {
      return ;
    }
    mIbftInstalled = FALSE;
  }

  //
  // If there is no valid attempt configuration, just return.
  //
  if ((!mPrivate->EnableMpio && mPrivate->ValidSinglePathCount == 0) ||
      (mPrivate->EnableMpio && mPrivate->MpioCount <= mPrivate->Krb5MpioCount)) {
    return ;
  }

  //
  // Allocate 4k bytes to hold the ACPI table.
  //
  Table = AllocateZeroPool (IBFT_MAX_SIZE);
  if (Table == NULL) {
    return ;
  }

  Heap = (UINT8 *) Table + IBFT_HEAP_OFFSET;

  //
  // Fill in the various section of the iSCSI Boot Firmware Table.
  //
  if (Rsdp->Revision >= EFI_ACPI_2_0_ROOT_SYSTEM_DESCRIPTION_POINTER_REVISION) {
    IScsiInitIbfTableHeader (Table, Xsdt->OemId, &Xsdt->OemTableId);
  } else {
    IScsiInitIbfTableHeader (Table, Rsdt->OemId, &Rsdt->OemTableId);
  }

  IScsiInitControlSection (Table);
  IScsiFillInitiatorSection (Table, &Heap);
  IScsiFillNICAndTargetSections (Table, &Heap);

  Checksum = CalculateCheckSum8((UINT8 *)Table, Table->Length);
  Table->Checksum = Checksum;

  //
  // Install or update the iBFT table.
  //
  Status = AcpiTableProtocol->InstallAcpiTable (
                                AcpiTableProtocol,
                                Table,
                                Table->Length,
                                &mTableKey
                                );
  if (EFI_ERROR(Status)) {
    return;
  }

  mIbftInstalled = TRUE;
  FreePool (Table);
}
