/** @file
  Boot functions implementation for UefiPxeBc Driver.

  Copyright (c) 2009 - 2018, Intel Corporation. All rights reserved.<BR>
  (C) Copyright 2016 Hewlett Packard Enterprise Development LP<BR>

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

**/

#include "PxeBcImpl.h"

/**
  Display the string of the boot item.

  If the length of the boot item string beyond 70 Char, just display 70 Char.

  @param[in]  Str           The pointer to the string.
  @param[in]  Len           The length of the string.

**/
VOID
PxeBcDisplayBootItem (
  IN UINT8  *Str,
  IN UINT8  Len
  )
{
  UINT8  Tmp;

  //
  // Cut off the chars behind 70th.
  //
  Len      = (UINT8)MIN (PXEBC_DISPLAY_MAX_LINE, Len);
  Tmp      = Str[Len];
  Str[Len] = 0;
  AsciiPrint ("%a \n", Str);

  //
  // Restore the original 70th char.
  //
  Str[Len] = Tmp;
}

/**
  Select and maintain the boot prompt if needed.

  @param[in]  Private          Pointer to PxeBc private data.

  @retval EFI_SUCCESS          Selected boot prompt done.
  @retval EFI_TIMEOUT          Selected boot prompt timed out.
  @retval EFI_NOT_FOUND        The proxy offer is not Pxe10.
  @retval EFI_ABORTED          User cancelled the operation.
  @retval EFI_NOT_READY        Reading the input key from the keyboard has not finish.

**/
EFI_STATUS
PxeBcSelectBootPrompt (
  IN PXEBC_PRIVATE_DATA  *Private
  )
{
  PXEBC_DHCP_PACKET_CACHE  *Cache;
  PXEBC_VENDOR_OPTION      *VendorOpt;
  EFI_PXE_BASE_CODE_MODE   *Mode;
  EFI_EVENT                TimeoutEvent;
  EFI_EVENT                DescendEvent;
  EFI_INPUT_KEY            InputKey;
  EFI_STATUS               Status;
  UINT32                   OfferType;
  UINT8                    Timeout;
  UINT8                    *Prompt;
  UINT8                    PromptLen;
  INT32                    SecCol;
  INT32                    SecRow;

  TimeoutEvent = NULL;
  DescendEvent = NULL;
  Mode         = Private->PxeBc.Mode;
  Cache        = Mode->ProxyOfferReceived ? &Private->ProxyOffer : &Private->DhcpAck;
  OfferType    = Mode->UsingIpv6 ? Cache->Dhcp6.OfferType : Cache->Dhcp4.OfferType;

  //
  // Only DhcpPxe10 and ProxyPxe10 offer needs boot prompt.
  //
  if ((OfferType != PxeOfferTypeProxyPxe10) && (OfferType != PxeOfferTypeDhcpPxe10)) {
    return EFI_NOT_FOUND;
  }

  //
  // There is no specified ProxyPxe10 for IPv6 in PXE and UEFI spec.
  //
  ASSERT (!Mode->UsingIpv6);

  VendorOpt = &Cache->Dhcp4.VendorOpt;
  //
  // According to the PXE specification 2.1, Table 2-1 PXE DHCP Options,
  // we must not consider a boot prompt or boot menu if all of the following hold:
  //   - the PXE_DISCOVERY_CONTROL tag(6) is present inside the Vendor Options(43), and has bit 3 set
  //   - a boot file name has been presented in the initial DHCP or ProxyDHCP offer packet.
  //
  if (IS_DISABLE_PROMPT_MENU (VendorOpt->DiscoverCtrl) &&
      (Cache->Dhcp4.OptList[PXEBC_DHCP4_TAG_INDEX_BOOTFILE] != NULL))
  {
    return EFI_ABORTED;
  }

  if (!IS_VALID_BOOT_PROMPT (VendorOpt->BitMap)) {
    return EFI_TIMEOUT;
  }

  Timeout   = VendorOpt->MenuPrompt->Timeout;
  Prompt    = VendorOpt->MenuPrompt->Prompt;
  PromptLen = (UINT8)(VendorOpt->MenuPromptLen - 1);

  //
  // The valid scope of Timeout refers to PXE2.1 spec.
  //
  if (Timeout == 0) {
    return EFI_TIMEOUT;
  }

  if (Timeout == 255) {
    return EFI_SUCCESS;
  }

  //
  // Create and start a timer as timeout event.
  //
  Status = gBS->CreateEvent (
                  EVT_TIMER,
                  TPL_CALLBACK,
                  NULL,
                  NULL,
                  &TimeoutEvent
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = gBS->SetTimer (
                  TimeoutEvent,
                  TimerRelative,
                  MultU64x32 (Timeout, TICKS_PER_SECOND)
                  );
  if (EFI_ERROR (Status)) {
    goto ON_EXIT;
  }

  //
  // Create and start a periodic timer as descend event by second.
  //
  Status = gBS->CreateEvent (
                  EVT_TIMER,
                  TPL_CALLBACK,
                  NULL,
                  NULL,
                  &DescendEvent
                  );
  if (EFI_ERROR (Status)) {
    goto ON_EXIT;
  }

  Status = gBS->SetTimer (
                  DescendEvent,
                  TimerPeriodic,
                  TICKS_PER_SECOND
                  );
  if (EFI_ERROR (Status)) {
    goto ON_EXIT;
  }

  //
  // Display the boot item and cursor on the screen.
  //
  SecCol = gST->ConOut->Mode->CursorColumn;
  SecRow = gST->ConOut->Mode->CursorRow;

  PxeBcDisplayBootItem (Prompt, PromptLen);

  gST->ConOut->SetCursorPosition (gST->ConOut, SecCol + PromptLen, SecRow);
  AsciiPrint ("(%d) ", Timeout--);

  Status = EFI_TIMEOUT;
  while (EFI_ERROR (gBS->CheckEvent (TimeoutEvent))) {
    if (!EFI_ERROR (gBS->CheckEvent (DescendEvent))) {
      gST->ConOut->SetCursorPosition (gST->ConOut, SecCol + PromptLen, SecRow);
      AsciiPrint ("(%d) ", Timeout--);
    }

    if (gST->ConIn->ReadKeyStroke (gST->ConIn, &InputKey) == EFI_NOT_READY) {
      gBS->Stall (10 * TICKS_PER_MS);
      continue;
    }

    //
    // Parse the input key by user.
    // If <F8> or <Ctrl> + <M> is pressed, return success to display the boot menu.
    //
    if (InputKey.ScanCode == 0) {
      switch (InputKey.UnicodeChar) {
        case CTRL ('c'):
          Status = EFI_ABORTED;
          break;

        case CTRL ('m'):
        case 'm':
        case 'M':
          Status = EFI_SUCCESS;
          break;

        default:
          continue;
      }
    } else {
      switch (InputKey.ScanCode) {
        case SCAN_F8:
          Status = EFI_SUCCESS;
          break;

        case SCAN_ESC:
          Status = EFI_ABORTED;
          break;

        default:
          continue;
      }
    }

    break;
  }

  //
  // Reset the cursor on the screen.
  //
  gST->ConOut->SetCursorPosition (gST->ConOut, 0, SecRow + 1);

ON_EXIT:
  if (DescendEvent != NULL) {
    gBS->CloseEvent (DescendEvent);
  }

  if (TimeoutEvent != NULL) {
    gBS->CloseEvent (TimeoutEvent);
  }

  return Status;
}

/**
  Select the boot menu by user's input.

  @param[in]  Private         Pointer to PxeBc private data.
  @param[out] Type            The type of the menu.
  @param[in]  UseDefaultItem  Use default item or not.

  @retval EFI_ABORTED     User cancel operation.
  @retval EFI_SUCCESS     Select the boot menu success.
  @retval EFI_NOT_READY   Read the input key from the keyboard has not finish.

**/
EFI_STATUS
PxeBcSelectBootMenu (
  IN  PXEBC_PRIVATE_DATA  *Private,
  OUT UINT16              *Type,
  IN  BOOLEAN             UseDefaultItem
  )
{
  EFI_PXE_BASE_CODE_MODE   *Mode;
  PXEBC_DHCP_PACKET_CACHE  *Cache;
  PXEBC_VENDOR_OPTION      *VendorOpt;
  EFI_INPUT_KEY            InputKey;
  UINT32                   OfferType;
  UINT8                    MenuSize;
  UINT8                    MenuNum;
  INT32                    TopRow;
  UINT16                   Select;
  UINT16                   LastSelect;
  UINT8                    Index;
  BOOLEAN                  Finish;
  CHAR8                    Blank[PXEBC_DISPLAY_MAX_LINE];
  PXEBC_BOOT_MENU_ENTRY    *MenuItem;
  PXEBC_BOOT_MENU_ENTRY    *MenuArray[PXEBC_MENU_MAX_NUM];

  Finish    = FALSE;
  Select    = 0;
  Index     = 0;
  *Type     = 0;
  Mode      = Private->PxeBc.Mode;
  Cache     = Mode->ProxyOfferReceived ? &Private->ProxyOffer : &Private->DhcpAck;
  OfferType = Mode->UsingIpv6 ? Cache->Dhcp6.OfferType : Cache->Dhcp4.OfferType;

  //
  // There is no specified DhcpPxe10/ProxyPxe10 for IPv6 in PXE and UEFI spec.
  //
  ASSERT (!Mode->UsingIpv6);
  ASSERT (OfferType == PxeOfferTypeProxyPxe10 || OfferType == PxeOfferTypeDhcpPxe10);

  VendorOpt = &Cache->Dhcp4.VendorOpt;
  if (!IS_VALID_BOOT_MENU (VendorOpt->BitMap)) {
    return EFI_SUCCESS;
  }

  //
  // Display the boot menu on the screen.
  //
  SetMem (Blank, sizeof (Blank), ' ');

  MenuSize = VendorOpt->BootMenuLen;
  MenuItem = VendorOpt->BootMenu;

  if (MenuSize == 0) {
    return EFI_DEVICE_ERROR;
  }

  while (MenuSize > 0 && Index < PXEBC_MENU_MAX_NUM) {
    ASSERT (MenuItem != NULL);
    MenuArray[Index] = MenuItem;
    MenuSize         = (UINT8)(MenuSize - (MenuItem->DescLen + 3));
    MenuItem         = (PXEBC_BOOT_MENU_ENTRY *)((UINT8 *)MenuItem + MenuItem->DescLen + 3);
    Index++;
  }

  if (UseDefaultItem) {
    ASSERT (MenuArray[0] != NULL);
    CopyMem (Type, &MenuArray[0]->Type, sizeof (UINT16));
    *Type = NTOHS (*Type);
    return EFI_SUCCESS;
  }

  MenuNum = Index;

  for (Index = 0; Index < MenuNum; Index++) {
    ASSERT (MenuArray[Index] != NULL);
    PxeBcDisplayBootItem (MenuArray[Index]->DescStr, MenuArray[Index]->DescLen);
  }

  TopRow = gST->ConOut->Mode->CursorRow - MenuNum;

  //
  // Select the boot item by user in the boot menu.
  //
  do {
    //
    // Highlight selected row.
    //
    gST->ConOut->SetAttribute (gST->ConOut, EFI_TEXT_ATTR (EFI_BLACK, EFI_LIGHTGRAY));
    gST->ConOut->SetCursorPosition (gST->ConOut, 0, TopRow + Select);
    ASSERT (Select < PXEBC_MENU_MAX_NUM);
    ASSERT (MenuArray[Select] != NULL);
    Blank[MenuArray[Select]->DescLen] = 0;
    AsciiPrint ("%a\r", Blank);
    PxeBcDisplayBootItem (MenuArray[Select]->DescStr, MenuArray[Select]->DescLen);
    gST->ConOut->SetCursorPosition (gST->ConOut, 0, TopRow + MenuNum);
    LastSelect = Select;

    while (gST->ConIn->ReadKeyStroke (gST->ConIn, &InputKey) == EFI_NOT_READY) {
      gBS->Stall (10 * TICKS_PER_MS);
    }

    if (InputKey.ScanCode == 0) {
      switch (InputKey.UnicodeChar) {
        case CTRL ('c'):
          InputKey.ScanCode = SCAN_ESC;
          break;

        case CTRL ('j'): /* linefeed */
        case CTRL ('m'): /* return */
          Finish = TRUE;
          break;

        case CTRL ('i'): /* tab */
        case ' ':
        case 'd':
        case 'D':
          InputKey.ScanCode = SCAN_DOWN;
          break;

        case CTRL ('h'): /* backspace */
        case 'u':
        case 'U':
          InputKey.ScanCode = SCAN_UP;
          break;

        default:
          InputKey.ScanCode = 0;
      }
    }

    switch (InputKey.ScanCode) {
      case SCAN_LEFT:
      case SCAN_UP:
        if (Select != 0) {
          Select--;
        }

        break;

      case SCAN_DOWN:
      case SCAN_RIGHT:
        if (++Select == MenuNum) {
          Select--;
        }

        break;

      case SCAN_PAGE_UP:
      case SCAN_HOME:
        Select = 0;
        break;

      case SCAN_PAGE_DOWN:
      case SCAN_END:
        Select = (UINT16)(MenuNum - 1);
        break;

      case SCAN_ESC:
        return EFI_ABORTED;
    }

    //
    // Unhighlight the last selected row.
    //
    gST->ConOut->SetAttribute (gST->ConOut, EFI_TEXT_ATTR (EFI_LIGHTGRAY, EFI_BLACK));
    gST->ConOut->SetCursorPosition (gST->ConOut, 0, TopRow + LastSelect);
    ASSERT (LastSelect < PXEBC_MENU_MAX_NUM);
    ASSERT (MenuArray[LastSelect] != NULL);
    Blank[MenuArray[LastSelect]->DescLen] = 0;
    AsciiPrint ("%a\r", Blank);
    PxeBcDisplayBootItem (MenuArray[LastSelect]->DescStr, MenuArray[LastSelect]->DescLen);
    gST->ConOut->SetCursorPosition (gST->ConOut, 0, TopRow + MenuNum);
  } while (!Finish);

  //
  // Swap the byte order.
  //
  ASSERT (Select < PXEBC_MENU_MAX_NUM);
  ASSERT (MenuArray[Select] != NULL);
  CopyMem (Type, &MenuArray[Select]->Type, sizeof (UINT16));
  *Type = NTOHS (*Type);

  return EFI_SUCCESS;
}

/**
  Parse out the boot information from the last Dhcp4 reply packet.

  @param[in, out] Private      Pointer to PxeBc private data.
  @param[out]     BufferSize   Size of the boot file to be downloaded.

  @retval EFI_SUCCESS          Successfully parsed out all the boot information.
  @retval Others               Failed to parse out the boot information.

**/
EFI_STATUS
PxeBcDhcp4BootInfo (
  IN OUT PXEBC_PRIVATE_DATA  *Private,
  OUT UINT64                 *BufferSize
  )
{
  EFI_PXE_BASE_CODE_PROTOCOL  *PxeBc;
  EFI_PXE_BASE_CODE_MODE      *Mode;
  EFI_STATUS                  Status;
  PXEBC_DHCP4_PACKET_CACHE    *Cache4;
  UINT16                      Value;
  PXEBC_VENDOR_OPTION         *VendorOpt;
  PXEBC_BOOT_SVR_ENTRY        *Entry;

  PxeBc       = &Private->PxeBc;
  Mode        = PxeBc->Mode;
  Status      = EFI_SUCCESS;
  *BufferSize = 0;

  //
  // Get the last received Dhcp4 reply packet.
  //
  if (Mode->PxeReplyReceived) {
    Cache4 = &Private->PxeReply.Dhcp4;
  } else if (Mode->ProxyOfferReceived) {
    Cache4 = &Private->ProxyOffer.Dhcp4;
  } else {
    Cache4 = &Private->DhcpAck.Dhcp4;
  }

  if (Cache4->OptList[PXEBC_DHCP4_TAG_INDEX_BOOTFILE] == NULL) {
    //
    // This should never happen in a correctly configured DHCP / PXE
    // environment. One misconfiguration that can cause it is two DHCP servers
    // mistakenly running on the same network segment at the same time, and
    // racing each other in answering DHCP requests. Thus, the DHCP packets
    // that the edk2 PXE client considers "belonging together" may actually be
    // entirely independent, coming from two (competing) DHCP servers.
    //
    // Try to deal with this gracefully. Note that this check is not
    // comprehensive, as we don't try to identify all such errors.
    //
    return EFI_PROTOCOL_ERROR;
  }

  //
  // Parse the boot server address.
  // If prompt/discover is disabled, get the first boot server from the boot servers list.
  // Otherwise, parse the boot server Ipv4 address from next server address field in DHCP header.
  // If all these fields are not available, use option 54 instead.
  //
  VendorOpt = &Cache4->VendorOpt;
  if (IS_DISABLE_PROMPT_MENU (VendorOpt->DiscoverCtrl) && IS_VALID_BOOT_SERVERS (VendorOpt->BitMap)) {
    Entry = VendorOpt->BootSvr;
    if ((VendorOpt->BootSvrLen >= sizeof (PXEBC_BOOT_SVR_ENTRY)) && (Entry->IpCnt > 0)) {
      CopyMem (
        &Private->ServerIp,
        &Entry->IpAddr[0],
        sizeof (EFI_IPv4_ADDRESS)
        );
    }
  }

  if (Private->ServerIp.Addr[0] == 0) {
    //
    // ServerIp.Addr[0] equals zero means we failed to get IP address from boot server list.
    // Try to use next server address field.
    //
    CopyMem (
      &Private->ServerIp,
      &Cache4->Packet.Offer.Dhcp4.Header.ServerAddr,
      sizeof (EFI_IPv4_ADDRESS)
      );
  }

  if (Private->ServerIp.Addr[0] == 0) {
    //
    // Still failed , use the IP address from option 54.
    //
    CopyMem (
      &Private->ServerIp,
      Cache4->OptList[PXEBC_DHCP4_TAG_INDEX_SERVER_ID]->Data,
      sizeof (EFI_IPv4_ADDRESS)
      );
  }

  //
  // Parse the boot file name by option.
  //
  Private->BootFileName = Cache4->OptList[PXEBC_DHCP4_TAG_INDEX_BOOTFILE]->Data;

  if (Cache4->OptList[PXEBC_DHCP4_TAG_INDEX_BOOTFILE_LEN] != NULL) {
    //
    // Parse the boot file size by option.
    //
    CopyMem (&Value, Cache4->OptList[PXEBC_DHCP4_TAG_INDEX_BOOTFILE_LEN]->Data, sizeof (Value));
    Value = NTOHS (Value);
    //
    // The field of boot file size is 512 bytes in unit.
    //
    *BufferSize = 512 * Value;
  } else {
    //
    // Get the bootfile size by tftp command if no option available.
    //
    Status = PxeBc->Mtftp (
                      PxeBc,
                      EFI_PXE_BASE_CODE_TFTP_GET_FILE_SIZE,
                      NULL,
                      FALSE,
                      BufferSize,
                      &Private->BlockSize,
                      &Private->ServerIp,
                      Private->BootFileName,
                      NULL,
                      FALSE
                      );
  }

  //
  // Save the value of boot file size.
  //
  Private->BootFileSize = (UINTN)*BufferSize;

  //
  // Display all the information: boot server address, boot file name and boot file size.
  //
  AsciiPrint ("\n  Server IP address is ");
  PxeBcShowIp4Addr (&Private->ServerIp.v4);
  AsciiPrint ("\n  NBP filename is %a", Private->BootFileName);
  AsciiPrint ("\n  NBP filesize is %d Bytes", Private->BootFileSize);

  return Status;
}

/**
  Parse out the boot information from the last Dhcp6 reply packet.

  @param[in, out] Private      Pointer to PxeBc private data.
  @param[out]     BufferSize   Size of the boot file to be downloaded.

  @retval EFI_SUCCESS          Successfully parsed out all the boot information.
  @retval EFI_BUFFER_TOO_SMALL
  @retval Others               Failed to parse out the boot information.

**/
EFI_STATUS
PxeBcDhcp6BootInfo (
  IN OUT PXEBC_PRIVATE_DATA  *Private,
  OUT UINT64                 *BufferSize
  )
{
  EFI_PXE_BASE_CODE_PROTOCOL  *PxeBc;
  EFI_PXE_BASE_CODE_MODE      *Mode;
  EFI_STATUS                  Status;
  PXEBC_DHCP6_PACKET_CACHE    *Cache6;
  UINT16                      Value;

  PxeBc       = &Private->PxeBc;
  Mode        = PxeBc->Mode;
  Status      = EFI_SUCCESS;
  *BufferSize = 0;

  //
  // Get the last received Dhcp6 reply packet.
  //
  if (Mode->PxeReplyReceived) {
    Cache6 = &Private->PxeReply.Dhcp6;
  } else if (Mode->ProxyOfferReceived) {
    Cache6 = &Private->ProxyOffer.Dhcp6;
  } else {
    Cache6 = &Private->DhcpAck.Dhcp6;
  }

  if (Cache6->OptList[PXEBC_DHCP6_IDX_BOOT_FILE_URL] == NULL) {
    //
    // This should never happen in a correctly configured DHCP / PXE
    // environment. One misconfiguration that can cause it is two DHCP servers
    // mistakenly running on the same network segment at the same time, and
    // racing each other in answering DHCP requests. Thus, the DHCP packets
    // that the edk2 PXE client considers "belonging together" may actually be
    // entirely independent, coming from two (competing) DHCP servers.
    //
    // Try to deal with this gracefully. Note that this check is not
    // comprehensive, as we don't try to identify all such errors.
    //
    return EFI_PROTOCOL_ERROR;
  }

  //
  // Set the station address to IP layer.
  //
  Status = PxeBcSetIp6Address (Private);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Parse (m)tftp server ip address and bootfile name.
  //
  Status = PxeBcExtractBootFileUrl (
             Private,
             &Private->BootFileName,
             &Private->ServerIp.v6,
             (CHAR8 *)(Cache6->OptList[PXEBC_DHCP6_IDX_BOOT_FILE_URL]->Data),
             NTOHS (Cache6->OptList[PXEBC_DHCP6_IDX_BOOT_FILE_URL]->OpLen)
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Parse the value of boot file size.
  //
  if (Cache6->OptList[PXEBC_DHCP6_IDX_BOOT_FILE_PARAM] != NULL) {
    //
    // Parse it out if have the boot file parameter option.
    //
    Status = PxeBcExtractBootFileParam ((CHAR8 *)Cache6->OptList[PXEBC_DHCP6_IDX_BOOT_FILE_PARAM]->Data, &Value);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    //
    // The field of boot file size is 512 bytes in unit.
    //
    *BufferSize = 512 * Value;
  } else {
    //
    // Send get file size command by tftp if option unavailable.
    //
    Status = PxeBc->Mtftp (
                      PxeBc,
                      EFI_PXE_BASE_CODE_TFTP_GET_FILE_SIZE,
                      NULL,
                      FALSE,
                      BufferSize,
                      &Private->BlockSize,
                      &Private->ServerIp,
                      Private->BootFileName,
                      NULL,
                      FALSE
                      );
  }

  //
  // Save the value of boot file size.
  //
  Private->BootFileSize = (UINTN)*BufferSize;

  //
  // Display all the information: boot server address, boot file name and boot file size.
  //
  AsciiPrint ("\n  Server IP address is ");
  PxeBcShowIp6Addr (&Private->ServerIp.v6);
  AsciiPrint ("\n  NBP filename is %a", Private->BootFileName);
  AsciiPrint ("\n  NBP filesize is %d Bytes", Private->BootFileSize);

  return Status;
}

/**
  Extract the discover information and boot server entry from the
  cached packets if unspecified.

  @param[in]      Private      Pointer to PxeBc private data.
  @param[in]      Type         The type of bootstrap to perform.
  @param[in, out] DiscoverInfo Pointer to EFI_PXE_BASE_CODE_DISCOVER_INFO.
  @param[out]     BootEntry    Pointer to PXEBC_BOOT_SVR_ENTRY.
  @param[out]     SrvList      Pointer to EFI_PXE_BASE_CODE_SRVLIST.

  @retval EFI_SUCCESS       Successfully extracted the information.
  @retval EFI_DEVICE_ERROR  Failed to extract the information.

**/
EFI_STATUS
PxeBcExtractDiscoverInfo (
  IN     PXEBC_PRIVATE_DATA               *Private,
  IN     UINT16                           Type,
  IN OUT EFI_PXE_BASE_CODE_DISCOVER_INFO  **DiscoverInfo,
  OUT PXEBC_BOOT_SVR_ENTRY                **BootEntry,
  OUT EFI_PXE_BASE_CODE_SRVLIST           **SrvList
  )
{
  EFI_PXE_BASE_CODE_MODE           *Mode;
  PXEBC_DHCP4_PACKET_CACHE         *Cache4;
  PXEBC_VENDOR_OPTION              *VendorOpt;
  PXEBC_BOOT_SVR_ENTRY             *Entry;
  BOOLEAN                          IsFound;
  EFI_PXE_BASE_CODE_DISCOVER_INFO  *Info;
  UINT16                           Index;

  Mode = Private->PxeBc.Mode;
  Info = *DiscoverInfo;

  if (Mode->UsingIpv6) {
    Info->IpCnt    = 1;
    Info->UseUCast = TRUE;

    Info->SrvList[0].Type              = Type;
    Info->SrvList[0].AcceptAnyResponse = FALSE;

    //
    // There is no vendor options specified in DHCPv6, so take BootFileUrl in the last cached packet.
    //
    CopyMem (&Info->SrvList[0].IpAddr, &Private->ServerIp, sizeof (EFI_IP_ADDRESS));

    *SrvList = Info->SrvList;
  } else {
    Entry     = NULL;
    IsFound   = FALSE;
    Cache4    = (Mode->ProxyOfferReceived) ? &Private->ProxyOffer.Dhcp4 : &Private->DhcpAck.Dhcp4;
    VendorOpt = &Cache4->VendorOpt;

    if (!Mode->DhcpAckReceived || !IS_VALID_DISCOVER_VENDOR_OPTION (VendorOpt->BitMap)) {
      //
      // Address is not acquired or no discovery options.
      //
      return EFI_INVALID_PARAMETER;
    }

    //
    // Parse the boot server entry from the vendor option in the last cached packet.
    //
    Info->UseMCast    = (BOOLEAN) !IS_DISABLE_MCAST_DISCOVER (VendorOpt->DiscoverCtrl);
    Info->UseBCast    = (BOOLEAN) !IS_DISABLE_BCAST_DISCOVER (VendorOpt->DiscoverCtrl);
    Info->MustUseList = (BOOLEAN)IS_ENABLE_USE_SERVER_LIST (VendorOpt->DiscoverCtrl);
    Info->UseUCast    = (BOOLEAN)IS_VALID_BOOT_SERVERS (VendorOpt->BitMap);

    if (Info->UseMCast) {
      //
      // Get the multicast discover ip address from vendor option if has.
      //
      CopyMem (&Info->ServerMCastIp.v4, &VendorOpt->DiscoverMcastIp, sizeof (EFI_IPv4_ADDRESS));
    }

    Info->IpCnt = 0;

    if (Info->UseUCast) {
      Entry = VendorOpt->BootSvr;

      while (((UINT8)(Entry - VendorOpt->BootSvr)) < VendorOpt->BootSvrLen) {
        if (Entry->Type == HTONS (Type)) {
          IsFound = TRUE;
          break;
        }

        Entry = GET_NEXT_BOOT_SVR_ENTRY (Entry);
      }

      if (!IsFound) {
        return EFI_DEVICE_ERROR;
      }

      Info->IpCnt = Entry->IpCnt;
      if (Info->IpCnt >= 1) {
        *DiscoverInfo = AllocatePool (sizeof (*Info) + (Info->IpCnt - 1) * sizeof (**SrvList));
        if (*DiscoverInfo == NULL) {
          return EFI_OUT_OF_RESOURCES;
        }

        CopyMem (*DiscoverInfo, Info, sizeof (*Info));
        Info = *DiscoverInfo;
      }

      for (Index = 0; Index < Info->IpCnt; Index++) {
        CopyMem (&Info->SrvList[Index].IpAddr, &Entry->IpAddr[Index], sizeof (EFI_IPv4_ADDRESS));
        Info->SrvList[Index].AcceptAnyResponse = !Info->MustUseList;
        Info->SrvList[Index].Type              = NTOHS (Entry->Type);
      }
    }

    *BootEntry = Entry;
    *SrvList   = Info->SrvList;
  }

  return EFI_SUCCESS;
}

/**
  Build the discover packet and send out for boot server.

  @param[in]  Private               Pointer to PxeBc private data.
  @param[in]  Type                  PxeBc option boot item type.
  @param[in]  Layer                 Pointer to option boot item layer.
  @param[in]  UseBis                Use BIS or not.
  @param[in]  DestIp                Pointer to the destination address.
  @param[in]  IpCount               The count of the server address.
  @param[in]  SrvList               Pointer to the server address list.

  @retval     EFI_SUCCESS           Successfully discovered boot file.
  @retval     EFI_OUT_OF_RESOURCES  Failed to allocate resource.
  @retval     EFI_NOT_FOUND         Can't get the PXE reply packet.
  @retval     Others                Failed to discover boot file.

**/
EFI_STATUS
PxeBcDiscoverBootServer (
  IN  PXEBC_PRIVATE_DATA         *Private,
  IN  UINT16                     Type,
  IN  UINT16                     *Layer,
  IN  BOOLEAN                    UseBis,
  IN  EFI_IP_ADDRESS             *DestIp,
  IN  UINT16                     IpCount,
  IN  EFI_PXE_BASE_CODE_SRVLIST  *SrvList
  )
{
  if (Private->PxeBc.Mode->UsingIpv6) {
    return PxeBcDhcp6Discover (
             Private,
             Type,
             Layer,
             UseBis,
             DestIp
             );
  } else {
    return PxeBcDhcp4Discover (
             Private,
             Type,
             Layer,
             UseBis,
             DestIp,
             IpCount,
             SrvList
             );
  }
}

/**
  Discover all the boot information for boot file.

  @param[in, out] Private      Pointer to PxeBc private data.
  @param[out]     BufferSize   Size of the boot file to be downloaded.

  @retval EFI_SUCCESS          Successfully obtained all the boot information .
  @retval EFI_BUFFER_TOO_SMALL The buffer size is not enough for boot file.
  @retval EFI_ABORTED          User cancel current operation.
  @retval Others               Failed to parse out the boot information.

**/
EFI_STATUS
PxeBcDiscoverBootFile (
  IN OUT PXEBC_PRIVATE_DATA  *Private,
  OUT UINT64                 *BufferSize
  )
{
  EFI_PXE_BASE_CODE_PROTOCOL  *PxeBc;
  EFI_PXE_BASE_CODE_MODE      *Mode;
  EFI_STATUS                  Status;
  UINT16                      Type;
  UINT16                      Layer;
  BOOLEAN                     UseBis;

  PxeBc = &Private->PxeBc;
  Mode  = PxeBc->Mode;
  Type  = EFI_PXE_BASE_CODE_BOOT_TYPE_BOOTSTRAP;
  Layer = EFI_PXE_BASE_CODE_BOOT_LAYER_INITIAL;

  //
  // Start D.O.R.A/S.A.R.R exchange to acquire station ip address and
  // other pxe boot information.
  //
  Status = PxeBc->Dhcp (PxeBc, TRUE);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Select a boot server from boot server list.
  //
  Status = PxeBcSelectBootPrompt (Private);

  if (Status == EFI_SUCCESS) {
    //
    // Choose by user's input.
    //
    Status = PxeBcSelectBootMenu (Private, &Type, FALSE);
  } else if (Status == EFI_TIMEOUT) {
    //
    // Choose by default item.
    //
    Status = PxeBcSelectBootMenu (Private, &Type, TRUE);
  }

  if (!EFI_ERROR (Status)) {
    if (Type == EFI_PXE_BASE_CODE_BOOT_TYPE_BOOTSTRAP) {
      //
      // Local boot(PXE bootstrap server) need abort
      //
      return EFI_ABORTED;
    }

    //
    // Start to discover the boot server to get (m)tftp server ip address, bootfile
    // name and bootfile size.
    //
    UseBis = (BOOLEAN)(Mode->BisSupported && Mode->BisDetected);
    Status = PxeBc->Discover (PxeBc, Type, &Layer, UseBis, NULL);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    if (Mode->PxeReplyReceived && !Mode->ProxyOfferReceived) {
      //
      // Some network boot loader only search the packet in Mode.ProxyOffer to get its server
      // IP address, so we need to store a copy of Mode.PxeReply packet into Mode.ProxyOffer.
      //
      if (Mode->UsingIpv6) {
        CopyMem (
          &Mode->ProxyOffer.Dhcpv6,
          &Mode->PxeReply.Dhcpv6,
          Private->PxeReply.Dhcp6.Packet.Ack.Length
          );
      } else {
        CopyMem (
          &Mode->ProxyOffer.Dhcpv4,
          &Mode->PxeReply.Dhcpv4,
          Private->PxeReply.Dhcp4.Packet.Ack.Length
          );
      }

      Mode->ProxyOfferReceived = TRUE;
    }
  }

  //
  // Parse the boot information.
  //
  if (Mode->UsingIpv6) {
    Status = PxeBcDhcp6BootInfo (Private, BufferSize);
  } else {
    Status = PxeBcDhcp4BootInfo (Private, BufferSize);
  }

  return Status;
}

/**
  Install PxeBaseCodeCallbackProtocol if not installed before.

  @param[in, out] Private           Pointer to PxeBc private data.
  @param[out]     NewMakeCallback   If TRUE, it is a new callback.
                                    Otherwise, it is not new callback.
  @retval EFI_SUCCESS          PxeBaseCodeCallbackProtocol installed successfully.
  @retval Others               Failed to install PxeBaseCodeCallbackProtocol.

**/
EFI_STATUS
PxeBcInstallCallback (
  IN OUT PXEBC_PRIVATE_DATA  *Private,
  OUT BOOLEAN                *NewMakeCallback
  )
{
  EFI_PXE_BASE_CODE_PROTOCOL  *PxeBc;
  EFI_STATUS                  Status;

  //
  // Check whether PxeBaseCodeCallbackProtocol already installed.
  //
  PxeBc  = &Private->PxeBc;
  Status = gBS->HandleProtocol (
                  Private->Mode.UsingIpv6 ? Private->Ip6Nic->Controller : Private->Ip4Nic->Controller,
                  &gEfiPxeBaseCodeCallbackProtocolGuid,
                  (VOID **)&Private->PxeBcCallback
                  );
  if (Status == EFI_UNSUPPORTED) {
    CopyMem (
      &Private->LoadFileCallback,
      &gPxeBcCallBackTemplate,
      sizeof (EFI_PXE_BASE_CODE_CALLBACK_PROTOCOL)
      );

    //
    // Install a default callback if user didn't offer one.
    //
    Status = gBS->InstallProtocolInterface (
                    Private->Mode.UsingIpv6 ? &Private->Ip6Nic->Controller : &Private->Ip4Nic->Controller,
                    &gEfiPxeBaseCodeCallbackProtocolGuid,
                    EFI_NATIVE_INTERFACE,
                    &Private->LoadFileCallback
                    );

    (*NewMakeCallback) = (BOOLEAN)(Status == EFI_SUCCESS);

    Status = PxeBc->SetParameters (PxeBc, NULL, NULL, NULL, NULL, NewMakeCallback);
    if (EFI_ERROR (Status)) {
      PxeBc->Stop (PxeBc);
      return Status;
    }
  }

  return EFI_SUCCESS;
}

/**
  Uninstall PxeBaseCodeCallbackProtocol.

  @param[in]  Private           Pointer to PxeBc private data.
  @param[in]  NewMakeCallback   If TRUE, it is a new callback.
                                Otherwise, it is not new callback.

**/
VOID
PxeBcUninstallCallback (
  IN PXEBC_PRIVATE_DATA  *Private,
  IN BOOLEAN             NewMakeCallback
  )
{
  EFI_PXE_BASE_CODE_PROTOCOL  *PxeBc;

  PxeBc = &Private->PxeBc;

  if (NewMakeCallback) {
    NewMakeCallback = FALSE;

    PxeBc->SetParameters (PxeBc, NULL, NULL, NULL, NULL, &NewMakeCallback);

    gBS->UninstallProtocolInterface (
           Private->Mode.UsingIpv6 ? Private->Ip6Nic->Controller : Private->Ip4Nic->Controller,
           &gEfiPxeBaseCodeCallbackProtocolGuid,
           &Private->LoadFileCallback
           );
  }
}

/**
  Download one of boot file in the list, and it's special for IPv6.

  @param[in]      Private           Pointer to PxeBc private data.
  @param[in, out] BufferSize        Size of user buffer for input;
                                    required buffer size for output.
  @param[in]      Buffer            Pointer to user buffer.

  @retval EFI_SUCCESS               Read one of boot file in the list successfully.
  @retval EFI_BUFFER_TOO_SMALL      The buffer size is not enough for boot file.
  @retval EFI_NOT_FOUND             There is no proper boot file available.
  @retval Others                    Failed to download boot file in the list.

**/
EFI_STATUS
PxeBcReadBootFileList (
  IN     PXEBC_PRIVATE_DATA  *Private,
  IN OUT UINT64              *BufferSize,
  IN     VOID                *Buffer           OPTIONAL
  )
{
  EFI_STATUS                  Status;
  EFI_PXE_BASE_CODE_PROTOCOL  *PxeBc;

  PxeBc = &Private->PxeBc;

  //
  // Try to download the boot file if everything is ready.
  //
  if (Buffer != NULL) {
    Status = PxeBc->Mtftp (
                      PxeBc,
                      EFI_PXE_BASE_CODE_TFTP_READ_FILE,
                      Buffer,
                      FALSE,
                      BufferSize,
                      &Private->BlockSize,
                      &Private->ServerIp,
                      Private->BootFileName,
                      NULL,
                      FALSE
                      );
  } else {
    Status = EFI_BUFFER_TOO_SMALL;
  }

  return Status;
}

/**
  Load boot file into user buffer.

  @param[in]      Private           Pointer to PxeBc private data.
  @param[in, out] BufferSize        Size of user buffer for input;
                                    required buffer size for output.
  @param[in]      Buffer            Pointer to user buffer.

  @retval EFI_SUCCESS          Get all the boot information successfully.
  @retval EFI_BUFFER_TOO_SMALL The buffer size is not enough for boot file.
  @retval EFI_ABORTED          User cancelled the current operation.
  @retval Others               Failed to parse out the boot information.

**/
EFI_STATUS
PxeBcLoadBootFile (
  IN     PXEBC_PRIVATE_DATA  *Private,
  IN OUT UINTN               *BufferSize,
  IN     VOID                *Buffer         OPTIONAL
  )
{
  BOOLEAN                     NewMakeCallback;
  UINT64                      RequiredSize;
  UINT64                      CurrentSize;
  EFI_STATUS                  Status;
  EFI_PXE_BASE_CODE_PROTOCOL  *PxeBc;
  EFI_PXE_BASE_CODE_MODE      *PxeBcMode;

  NewMakeCallback = FALSE;
  PxeBc           = &Private->PxeBc;
  PxeBcMode       = &Private->Mode;
  CurrentSize     = *BufferSize;
  RequiredSize    = 0;

  //
  // Install pxebc callback protocol if hasn't been installed yet.
  //
  Status = PxeBcInstallCallback (Private, &NewMakeCallback);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  if (Private->BootFileSize == 0) {
    //
    // Discover the boot information about the bootfile if hasn't.
    //
    Status = PxeBcDiscoverBootFile (Private, &RequiredSize);
    if (EFI_ERROR (Status)) {
      goto ON_EXIT;
    }

    if (PXEBC_IS_SIZE_OVERFLOWED (RequiredSize)) {
      //
      // It's error if the required buffer size is beyond the system scope.
      //
      Status = EFI_DEVICE_ERROR;
      goto ON_EXIT;
    } else if (RequiredSize > 0) {
      //
      // Get the right buffer size of the bootfile required.
      //
      if ((CurrentSize < RequiredSize) || (Buffer == NULL)) {
        //
        // It's buffer too small if the size of user buffer is smaller than the required.
        //
        CurrentSize = RequiredSize;
        Status      = EFI_BUFFER_TOO_SMALL;
        goto ON_EXIT;
      }

      CurrentSize = RequiredSize;
    } else if ((RequiredSize == 0) && PxeBcMode->UsingIpv6) {
      //
      // Try to download another bootfile in list if failed to get the filesize of the last one.
      // It's special for the case of IPv6.
      //
      Status = PxeBcReadBootFileList (Private, &CurrentSize, Buffer);
      goto ON_EXIT;
    }
  } else if ((CurrentSize < Private->BootFileSize) || (Buffer == NULL)) {
    //
    // It's buffer too small if the size of user buffer is smaller than the required.
    //
    CurrentSize = Private->BootFileSize;
    Status      = EFI_BUFFER_TOO_SMALL;
    goto ON_EXIT;
  }

  //
  // Begin to download the bootfile if everything is ready.
  //
  AsciiPrint ("\n Downloading NBP file...\n");
  if (PxeBcMode->UsingIpv6) {
    Status = PxeBcReadBootFileList (
               Private,
               &CurrentSize,
               Buffer
               );
  } else {
    Status = PxeBc->Mtftp (
                      PxeBc,
                      EFI_PXE_BASE_CODE_TFTP_READ_FILE,
                      Buffer,
                      FALSE,
                      &CurrentSize,
                      &Private->BlockSize,
                      &Private->ServerIp,
                      Private->BootFileName,
                      NULL,
                      FALSE
                      );
  }

ON_EXIT:
  *BufferSize = (UINTN)CurrentSize;
  PxeBcUninstallCallback (Private, NewMakeCallback);

  if (Status == EFI_SUCCESS) {
    AsciiPrint ("\n  NBP file downloaded successfully.\n");
    return EFI_SUCCESS;
  } else if ((Status == EFI_BUFFER_TOO_SMALL) && (Buffer != NULL)) {
    AsciiPrint ("\n  PXE-E05: Buffer size is smaller than the requested file.\n");
  } else if (Status == EFI_DEVICE_ERROR) {
    AsciiPrint ("\n  PXE-E07: Network device error.\n");
  } else if (Status == EFI_OUT_OF_RESOURCES) {
    AsciiPrint ("\n  PXE-E09: Could not allocate I/O buffers.\n");
  } else if (Status == EFI_NO_MEDIA) {
    AsciiPrint ("\n  PXE-E12: Could not detect network connection.\n");
  } else if (Status == EFI_NO_RESPONSE) {
    AsciiPrint ("\n  PXE-E16: No valid offer received.\n");
  } else if (Status == EFI_TIMEOUT) {
    AsciiPrint ("\n  PXE-E18: Server response timeout.\n");
  } else if (Status == EFI_ABORTED) {
    AsciiPrint ("\n  PXE-E21: Remote boot cancelled.\n");
  } else if (Status == EFI_ICMP_ERROR) {
    AsciiPrint ("\n  PXE-E22: Client received ICMP error from server.\n");
  } else if (Status == EFI_TFTP_ERROR) {
    AsciiPrint ("\n  PXE-E23: Client received TFTP error from server.\n");
  } else if (Status == EFI_NOT_FOUND) {
    AsciiPrint ("\n  PXE-E53: No boot filename received.\n");
  } else if (Status != EFI_BUFFER_TOO_SMALL) {
    AsciiPrint ("\n  PXE-E99: Unexpected network error.\n");
  }

  REPORT_STATUS_CODE_WITH_EXTENDED_DATA (
    EFI_ERROR_CODE,
    (EFI_STATUS_CODE_VALUE)(EFI_IO_BUS_IP_NETWORK | EFI_OEM_SPECIFIC | ((EFI_STATUS_CODE_VALUE)(Status & 0x1F))),
    (VOID *)&(PxeBcMode->UsingIpv6),
    sizeof (PxeBcMode->UsingIpv6)
    );

  return Status;
}
