/** @file
  Routines supporting partition discovery and
  logical device reading

Copyright (c) 2019 Intel Corporation. All rights reserved.<BR>

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

**/

#include <IndustryStandard/Mbr.h>
#include <Uefi/UefiGpt.h>
#include <Library/BaseLib.h>
#include "FatLitePeim.h"

//
// Assumption: 'a' and 'blocksize' are all UINT32 or UINT64.
// If 'a' and 'blocksize' are not the same type, should use DivU64xU32 to calculate.
//
#define EFI_SIZE_TO_BLOCKS(a, blocksize)  (((a) / (blocksize)) + (((a) % (blocksize)) ? 1 : 0))

//
// GPT Partition Entry Status
//
typedef struct {
  BOOLEAN    OutOfRange;
  BOOLEAN    Overlap;
  BOOLEAN    OsSpecific;
} EFI_PARTITION_ENTRY_STATUS;

/**
  Check if the CRC field in the Partition table header is valid.

  @param[in]  PartHeader  Partition table header structure

  @retval TRUE      the CRC is valid
  @retval FALSE     the CRC is invalid

**/
BOOLEAN
PartitionCheckGptHeaderCRC (
  IN  EFI_PARTITION_TABLE_HEADER  *PartHeader
  )
{
  UINT32  GptHdrCrc;
  UINT32  Crc;

  GptHdrCrc = PartHeader->Header.CRC32;

  //
  // Set CRC field to zero when doing calculation
  //
  PartHeader->Header.CRC32 = 0;

  Crc = CalculateCrc32 (PartHeader, PartHeader->Header.HeaderSize);

  //
  // Restore Header CRC
  //
  PartHeader->Header.CRC32 = GptHdrCrc;

  return (GptHdrCrc == Crc);
}

/**
  Check if the CRC field in the Partition table header is valid
  for Partition entry array.

  @param[in]  PartHeader  Partition table header structure
  @param[in]  PartEntry   The partition entry array

  @retval TRUE      the CRC is valid
  @retval FALSE     the CRC is invalid

**/
BOOLEAN
PartitionCheckGptEntryArrayCRC (
  IN  EFI_PARTITION_TABLE_HEADER  *PartHeader,
  IN  EFI_PARTITION_ENTRY         *PartEntry
  )
{
  UINT32  Crc;
  UINTN   Size;

  Size = (UINTN)MultU64x32 (PartHeader->NumberOfPartitionEntries, PartHeader->SizeOfPartitionEntry);
  Crc  = CalculateCrc32 (PartEntry, Size);

  return (BOOLEAN)(PartHeader->PartitionEntryArrayCRC32 == Crc);
}

/**
  The function is used for valid GPT table. Both for Primary and Backup GPT header.

  @param[in]  PrivateData       The global memory map
  @param[in]  ParentBlockDevNo  The parent block device
  @param[in]  IsPrimaryHeader   Indicate to which header will be checked.
  @param[in]  PartHdr           Stores the partition table that is read

  @retval TRUE      The partition table is valid
  @retval FALSE     The partition table is not valid

**/
BOOLEAN
PartitionCheckGptHeader (
  IN  PEI_FAT_PRIVATE_DATA        *PrivateData,
  IN  UINTN                       ParentBlockDevNo,
  IN  BOOLEAN                     IsPrimaryHeader,
  IN  EFI_PARTITION_TABLE_HEADER  *PartHdr
  )
{
  PEI_FAT_BLOCK_DEVICE  *ParentBlockDev;
  EFI_PEI_LBA           Lba;
  EFI_PEI_LBA           AlternateLba;
  EFI_PEI_LBA           EntryArrayLastLba;

  UINT64  PartitionEntryArraySize;
  UINT64  PartitionEntryBlockNumb;
  UINT32  EntryArraySizeRemainder;

  ParentBlockDev = &(PrivateData->BlockDevice[ParentBlockDevNo]);

  if (IsPrimaryHeader) {
    Lba          = PRIMARY_PART_HEADER_LBA;
    AlternateLba = ParentBlockDev->LastBlock;
  } else {
    Lba          = ParentBlockDev->LastBlock;
    AlternateLba = PRIMARY_PART_HEADER_LBA;
  }

  if ((PartHdr->Header.Signature != EFI_PTAB_HEADER_ID) ||
      (PartHdr->Header.Revision != 0x00010000) ||
      (PartHdr->Header.HeaderSize < 92) ||
      (PartHdr->Header.HeaderSize > ParentBlockDev->BlockSize) ||
      (!PartitionCheckGptHeaderCRC (PartHdr)) ||
      (PartHdr->Header.Reserved != 0)
      )
  {
    DEBUG ((DEBUG_ERROR, "Invalid efi partition table header\n"));
    return FALSE;
  }

  //
  // |    Block0    |    Block1    |Block2 ~ FirstUsableLBA - 1|FirstUsableLBA, ... ,LastUsableLBA|LastUsableLBA+1 ~ LastBlock-1|  LastBlock  |
  // |Protective MBR|Primary Header|Entry Array(At Least 16384)|             Partition            | Entry Array(At Least 16384) |BackUp Header|
  //
  // 1. Protective MBR is fixed at Block 0.
  // 2. Primary Header is fixed at Block 1.
  // 3. Backup Header is fixed at LastBlock.
  // 4. Must be remain 128*128 bytes for primary entry array.
  // 5. Must be remain 128*128 bytes for backup entry array.
  // 6. SizeOfPartitionEntry must be equals to 128 * 2^n.
  //
  if ((PartHdr->MyLBA != Lba) ||
      (PartHdr->AlternateLBA != AlternateLba) ||
      (PartHdr->FirstUsableLBA < 2 + EFI_SIZE_TO_BLOCKS (EFI_GPT_PART_ENTRY_MIN_SIZE, ParentBlockDev->BlockSize)) ||
      (PartHdr->LastUsableLBA  > ParentBlockDev->LastBlock - 1 - EFI_SIZE_TO_BLOCKS (EFI_GPT_PART_ENTRY_MIN_SIZE, ParentBlockDev->BlockSize)) ||
      (PartHdr->FirstUsableLBA > PartHdr->LastUsableLBA) ||
      (PartHdr->PartitionEntryLBA < 2) ||
      (PartHdr->PartitionEntryLBA > ParentBlockDev->LastBlock - 1) ||
      ((PartHdr->PartitionEntryLBA >= PartHdr->FirstUsableLBA) && (PartHdr->PartitionEntryLBA <= PartHdr->LastUsableLBA)) ||
      (PartHdr->SizeOfPartitionEntry%128 != 0) ||
      (PartHdr->SizeOfPartitionEntry != sizeof (EFI_PARTITION_ENTRY))
      )
  {
    DEBUG ((DEBUG_ERROR, "Invalid efi partition table header\n"));
    return FALSE;
  }

  //
  // Ensure the NumberOfPartitionEntries * SizeOfPartitionEntry doesn't overflow.
  //
  if (PartHdr->NumberOfPartitionEntries > DivU64x32 (MAX_UINTN, PartHdr->SizeOfPartitionEntry)) {
    DEBUG ((DEBUG_ERROR, "Memory overflow in GPT Entry Array\n"));
    return FALSE;
  }

  PartitionEntryArraySize = MultU64x32 (PartHdr->NumberOfPartitionEntries, PartHdr->SizeOfPartitionEntry);
  EntryArraySizeRemainder = 0;
  PartitionEntryBlockNumb = DivU64x32Remainder (PartitionEntryArraySize, ParentBlockDev->BlockSize, &EntryArraySizeRemainder);
  if (EntryArraySizeRemainder != 0) {
    PartitionEntryBlockNumb++;
  }

  if (IsPrimaryHeader) {
    EntryArrayLastLba = PartHdr->FirstUsableLBA;
  } else {
    EntryArrayLastLba = ParentBlockDev->LastBlock;
  }

  //
  // Make sure partition entry array not overlaps with partition area or the LastBlock.
  //
  if (PartHdr->PartitionEntryLBA + PartitionEntryBlockNumb > EntryArrayLastLba) {
    DEBUG ((DEBUG_ERROR, "GPT Partition Entry Array Error!\n"));
    DEBUG ((DEBUG_ERROR, "PartitionEntryArraySize = %lu.\n", PartitionEntryArraySize));
    DEBUG ((DEBUG_ERROR, "PartitionEntryLBA = %lu.\n", PartHdr->PartitionEntryLBA));
    DEBUG ((DEBUG_ERROR, "PartitionEntryBlockNumb = %lu.\n", PartitionEntryBlockNumb));
    DEBUG ((DEBUG_ERROR, "EntryArrayLastLba = %lu.\n", EntryArrayLastLba));
    return FALSE;
  }

  return TRUE;
}

/**
  This function is used to verify each partition in block device.

  @param[in]  PrivateData       The global memory map
  @param[in]  ParentBlockDevNo  The parent block device
  @param[in]  PartHdr           Stores the partition table that is read

  @retval TRUE      The partition is valid
  @retval FALSE     The partition is not valid

**/
BOOLEAN
PartitionCheckGptEntryArray (
  IN  PEI_FAT_PRIVATE_DATA        *PrivateData,
  IN  UINTN                       ParentBlockDevNo,
  IN  EFI_PARTITION_TABLE_HEADER  *PartHdr
  )
{
  EFI_STATUS            Status;
  PEI_FAT_BLOCK_DEVICE  *ParentBlockDev;
  PEI_FAT_BLOCK_DEVICE  *BlockDevPtr;

  UINT64  PartitionEntryArraySize;
  UINT64  PartitionEntryBlockNumb;
  UINT32  EntryArraySizeRemainder;

  EFI_PARTITION_ENTRY         *PartitionEntryBuffer;
  EFI_PARTITION_ENTRY_STATUS  *PartitionEntryStatus;

  BOOLEAN              Found;
  EFI_LBA              StartingLBA;
  EFI_LBA              EndingLBA;
  UINTN                Index;
  UINTN                Index1;
  UINTN                Index2;
  EFI_PARTITION_ENTRY  *Entry;

  PartitionEntryBuffer = NULL;
  PartitionEntryStatus = NULL;

  ParentBlockDev = &(PrivateData->BlockDevice[ParentBlockDevNo]);
  Found          = FALSE;

  PartitionEntryArraySize = MultU64x32 (PartHdr->NumberOfPartitionEntries, PartHdr->SizeOfPartitionEntry);
  EntryArraySizeRemainder = 0;
  PartitionEntryBlockNumb = DivU64x32Remainder (PartitionEntryArraySize, ParentBlockDev->BlockSize, &EntryArraySizeRemainder);
  if (EntryArraySizeRemainder != 0) {
    PartitionEntryBlockNumb++;
  }

  PartitionEntryArraySize = MultU64x32 (PartitionEntryBlockNumb, ParentBlockDev->BlockSize);

  PartitionEntryBuffer = (EFI_PARTITION_ENTRY *)AllocatePages (EFI_SIZE_TO_PAGES ((UINTN)PartitionEntryArraySize));
  if (PartitionEntryBuffer == NULL) {
    DEBUG ((DEBUG_ERROR, "Allocate memory error!\n"));
    goto EXIT;
  }

  PartitionEntryStatus = (EFI_PARTITION_ENTRY_STATUS *)AllocatePages (EFI_SIZE_TO_PAGES (PartHdr->NumberOfPartitionEntries * sizeof (EFI_PARTITION_ENTRY_STATUS)));
  if (PartitionEntryStatus == NULL) {
    DEBUG ((DEBUG_ERROR, "Allocate memory error!\n"));
    goto EXIT;
  }

  ZeroMem (PartitionEntryStatus, PartHdr->NumberOfPartitionEntries * sizeof (EFI_PARTITION_ENTRY_STATUS));

  Status = FatReadBlock (
             PrivateData,
             ParentBlockDevNo,
             PartHdr->PartitionEntryLBA,
             (UINTN)PartitionEntryArraySize,
             PartitionEntryBuffer
             );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Read partition entry array error!\n"));
    goto EXIT;
  }

  if (!PartitionCheckGptEntryArrayCRC (PartHdr, PartitionEntryBuffer)) {
    DEBUG ((DEBUG_ERROR, "Partition entries CRC check fail\n"));
    goto EXIT;
  }

  for (Index1 = 0; Index1 < PartHdr->NumberOfPartitionEntries; Index1++) {
    Entry = (EFI_PARTITION_ENTRY *)((UINT8 *)PartitionEntryBuffer + Index1 * PartHdr->SizeOfPartitionEntry);
    if (CompareGuid (&Entry->PartitionTypeGUID, &gEfiPartTypeUnusedGuid)) {
      continue;
    }

    StartingLBA = Entry->StartingLBA;
    EndingLBA   = Entry->EndingLBA;
    if ((StartingLBA > EndingLBA) ||
        (StartingLBA < PartHdr->FirstUsableLBA) ||
        (StartingLBA > PartHdr->LastUsableLBA) ||
        (EndingLBA < PartHdr->FirstUsableLBA) ||
        (EndingLBA > PartHdr->LastUsableLBA)
        )
    {
      PartitionEntryStatus[Index1].OutOfRange = TRUE;
      continue;
    }

    if ((Entry->Attributes & BIT1) != 0) {
      //
      // If Bit 1 is set, this indicate that this is an OS specific GUID partition.
      //
      PartitionEntryStatus[Index1].OsSpecific = TRUE;
    }

    for (Index2 = Index1 + 1; Index2 < PartHdr->NumberOfPartitionEntries; Index2++) {
      Entry = (EFI_PARTITION_ENTRY *)((UINT8 *)PartitionEntryBuffer + Index2 * PartHdr->SizeOfPartitionEntry);
      if (CompareGuid (&Entry->PartitionTypeGUID, &gEfiPartTypeUnusedGuid)) {
        continue;
      }

      if ((Entry->EndingLBA >= StartingLBA) && (Entry->StartingLBA <= EndingLBA)) {
        //
        // This region overlaps with the Index1'th region
        //
        PartitionEntryStatus[Index1].Overlap = TRUE;
        PartitionEntryStatus[Index2].Overlap = TRUE;
        continue;
      }
    }
  }

  for (Index = 0; Index < PartHdr->NumberOfPartitionEntries; Index++) {
    if (CompareGuid (&PartitionEntryBuffer[Index].PartitionTypeGUID, &gEfiPartTypeUnusedGuid) ||
        PartitionEntryStatus[Index].OutOfRange ||
        PartitionEntryStatus[Index].Overlap ||
        PartitionEntryStatus[Index].OsSpecific)
    {
      //
      // Don't use null EFI Partition Entries, Invalid Partition Entries or OS specific
      // partition Entries
      //
      continue;
    }

    if (PrivateData->BlockDeviceCount >= PEI_FAT_MAX_BLOCK_DEVICE) {
      break;
    }

    Found       = TRUE;
    BlockDevPtr = &(PrivateData->BlockDevice[PrivateData->BlockDeviceCount]);

    BlockDevPtr->BlockSize        = ParentBlockDev->BlockSize;
    BlockDevPtr->LastBlock        = PartitionEntryBuffer[Index].EndingLBA;
    BlockDevPtr->IoAlign          = ParentBlockDev->IoAlign;
    BlockDevPtr->Logical          = TRUE;
    BlockDevPtr->PartitionChecked = FALSE;
    BlockDevPtr->StartingPos      = MultU64x32 (
                                      PartitionEntryBuffer[Index].StartingLBA,
                                      ParentBlockDev->BlockSize
                                      );
    BlockDevPtr->ParentDevNo = ParentBlockDevNo;

    PrivateData->BlockDeviceCount++;

    DEBUG ((DEBUG_INFO, "Find GPT Partition [0x%lx", PartitionEntryBuffer[Index].StartingLBA));
    DEBUG ((DEBUG_INFO, ", 0x%lx]\n", BlockDevPtr->LastBlock));
    DEBUG ((DEBUG_INFO, "         BlockSize %x\n", BlockDevPtr->BlockSize));
  }

EXIT:
  if (PartitionEntryBuffer != NULL) {
    FreePages (PartitionEntryBuffer, EFI_SIZE_TO_PAGES ((UINTN)PartitionEntryArraySize));
  }

  if (PartitionEntryStatus != NULL) {
    FreePages (PartitionEntryStatus, EFI_SIZE_TO_PAGES (PartHdr->NumberOfPartitionEntries * sizeof (EFI_PARTITION_ENTRY_STATUS)));
  }

  return Found;
}

/**
  The function is used to check GPT structure, include GPT header and GPT entry array.

  1. Check GPT header.
  2. Check partition entry array.
  3. Check each partitions.

  @param[in]  PrivateData       The global memory map
  @param[in]  ParentBlockDevNo  The parent block device
  @param[in]  IsPrimary         Indicate primary or backup to be check

  @retval TRUE              Primary or backup GPT structure is valid.
  @retval FALSE             Both primary and backup are invalid.

**/
BOOLEAN
PartitionCheckGptStructure (
  IN  PEI_FAT_PRIVATE_DATA  *PrivateData,
  IN  UINTN                 ParentBlockDevNo,
  IN  BOOLEAN               IsPrimary
  )
{
  EFI_STATUS                  Status;
  PEI_FAT_BLOCK_DEVICE        *ParentBlockDev;
  EFI_PARTITION_TABLE_HEADER  *PartHdr;
  EFI_PEI_LBA                 GptHeaderLBA;

  ParentBlockDev = &(PrivateData->BlockDevice[ParentBlockDevNo]);
  PartHdr        = (EFI_PARTITION_TABLE_HEADER *)PrivateData->BlockData;

  if (IsPrimary) {
    GptHeaderLBA = PRIMARY_PART_HEADER_LBA;
  } else {
    GptHeaderLBA = ParentBlockDev->LastBlock;
  }

  Status = FatReadBlock (
             PrivateData,
             ParentBlockDevNo,
             GptHeaderLBA,
             ParentBlockDev->BlockSize,
             PartHdr
             );
  if (EFI_ERROR (Status)) {
    return FALSE;
  }

  if (!PartitionCheckGptHeader (PrivateData, ParentBlockDevNo, IsPrimary, PartHdr)) {
    return FALSE;
  }

  if (!PartitionCheckGptEntryArray (PrivateData, ParentBlockDevNo, PartHdr)) {
    return FALSE;
  }

  return TRUE;
}

/**
  This function is used to check protective MBR structure before checking GPT.

  @param[in]  PrivateData       The global memory map
  @param[in]  ParentBlockDevNo  The parent block device

  @retval TRUE              Valid protective MBR
  @retval FALSE             Invalid MBR
**/
BOOLEAN
PartitionCheckProtectiveMbr (
  IN  PEI_FAT_PRIVATE_DATA  *PrivateData,
  IN  UINTN                 ParentBlockDevNo
  )
{
  EFI_STATUS            Status;
  MASTER_BOOT_RECORD    *ProtectiveMbr;
  MBR_PARTITION_RECORD  *MbrPartition;
  PEI_FAT_BLOCK_DEVICE  *ParentBlockDev;
  UINTN                 Index;

  ProtectiveMbr  = (MASTER_BOOT_RECORD *)PrivateData->BlockData;
  ParentBlockDev = &(PrivateData->BlockDevice[ParentBlockDevNo]);

  //
  // Read Protective MBR
  //
  Status = FatReadBlock (
             PrivateData,
             ParentBlockDevNo,
             0,
             ParentBlockDev->BlockSize,
             ProtectiveMbr
             );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "GPT Error When Read Protective Mbr From Partition!\n"));
    return FALSE;
  }

  if (ProtectiveMbr->Signature != MBR_SIGNATURE) {
    DEBUG ((DEBUG_ERROR, "Protective Mbr Signature is invalid!\n"));
    return FALSE;
  }

  //
  // The partition define in UEFI Spec Table 17.
  // Boot Code, Unique MBR Disk Signature, Unknown.
  // These parts will not be used by UEFI, so we skip to check them.
  //
  for (Index = 0; Index < MAX_MBR_PARTITIONS; Index++) {
    MbrPartition = (MBR_PARTITION_RECORD *)&ProtectiveMbr->Partition[Index];
    if ((MbrPartition->BootIndicator   == 0x00) &&
        (MbrPartition->StartSector     == 0x02) &&
        (MbrPartition->OSIndicator     == PMBR_GPT_PARTITION) &&
        (UNPACK_UINT32 (MbrPartition->StartingLBA) == 1)
        )
    {
      return TRUE;
    }
  }

  DEBUG ((DEBUG_ERROR, "Protective Mbr, All Partition Entry Are Empty!\n"));
  return FALSE;
}

/**
  This function is used for finding GPT partition on block device.
  As follow UEFI spec we should check protective MBR first and then
  try to check both primary/backup GPT structures.

  @param[in]  PrivateData       The global memory map
  @param[in]  ParentBlockDevNo  The parent block device

  @retval TRUE              New partitions are detected and logical block devices
                            are added to block device array
  @retval FALSE             No new partitions are added

**/
BOOLEAN
FatFindGptPartitions (
  IN  PEI_FAT_PRIVATE_DATA  *PrivateData,
  IN  UINTN                 ParentBlockDevNo
  )
{
  BOOLEAN               Found;
  PEI_FAT_BLOCK_DEVICE  *ParentBlockDev;

  if (ParentBlockDevNo > PEI_FAT_MAX_BLOCK_DEVICE - 1) {
    return FALSE;
  }

  ParentBlockDev = &(PrivateData->BlockDevice[ParentBlockDevNo]);
  if (ParentBlockDev->BlockSize > PEI_FAT_MAX_BLOCK_SIZE) {
    DEBUG ((DEBUG_ERROR, "Device BlockSize %x exceed FAT_MAX_BLOCK_SIZE\n", ParentBlockDev->BlockSize));
    return FALSE;
  }

  if (!PartitionCheckProtectiveMbr (PrivateData, ParentBlockDevNo)) {
    return FALSE;
  }

  Found = PartitionCheckGptStructure (PrivateData, ParentBlockDevNo, TRUE);
  if (!Found) {
    DEBUG ((DEBUG_ERROR, "Primary GPT Header Error, Try to Check Backup GPT Header!\n"));
    Found = PartitionCheckGptStructure (PrivateData, ParentBlockDevNo, FALSE);
  }

  if (Found) {
    ParentBlockDev->PartitionChecked = TRUE;
  }

  return Found;
}
