/** @file
  Copyright (c) 2021, Marvin Häuser. All rights reserved.
  Copyright (c) 2022, Mikhail Krichanov. All rights reserved.
  SPDX-License-Identifier: BSD-3-Clause
**/

#include "ImageTool.h"

static
bool
CheckToolImageSegment (
  const image_tool_segment_info_t *SegmentInfo,
  const image_tool_segment_t      *Segment,
  uint32_t                        *PreviousEndAddress
  )
{
  bool Overflow;

  // LCOV_EXCL_START
  if (!IS_ALIGNED (Segment->ImageSize, SegmentInfo->SegmentAlignment)) {
    assert (false);
    return false;
  }
  // LCOV_EXCL_STOP

  if (Segment->Write && Segment->Execute) {
    DEBUG_RAISE ();
    return false;
  }

  if (Segment->ImageAddress != *PreviousEndAddress) {
    DEBUG_RAISE ();
    return false;
  }

  Overflow = BaseOverflowAddU32 (
               Segment->ImageAddress,
               Segment->ImageSize,
               PreviousEndAddress
               );
  // LCOV_EXCL_START
  if (Overflow) {
    assert (false);
    return false;
  }
  // LCOV_EXCL_STOP

  return true;
}

static
bool
CheckToolImageSegmentInfo (
  const image_tool_segment_info_t *SegmentInfo,
  uint32_t                        *ImageSize
  )
{
  uint32_t Index;
  bool     Result;

  // LCOV_EXCL_START
  if (!IS_POW2 (SegmentInfo->SegmentAlignment)) {
    assert (false);
    return false;
  }
  // LCOV_EXCL_STOP

  if (SegmentInfo->NumSegments == 0) {
    DEBUG_RAISE ();
    return false;
  }

  // LCOV_EXCL_START
  if (!IS_ALIGNED (SegmentInfo->Segments[0].ImageAddress, SegmentInfo->SegmentAlignment)) {
    assert (false);
    return false;
  }
  // LCOV_EXCL_STOP

  *ImageSize = SegmentInfo->Segments[0].ImageAddress;
  for (Index = 0; Index < SegmentInfo->NumSegments; ++Index) {
    Result = CheckToolImageSegment (
               SegmentInfo,
               &SegmentInfo->Segments[Index],
               ImageSize
               );
    if (!Result) {
      DEBUG_RAISE ();
      return false;
    }
  }

  return true;
}

static
bool
CheckToolImageHeaderInfo (
  const image_tool_header_info_t   *HeaderInfo,
  const image_tool_segment_info_t  *SegmentInfo,
  uint32_t                         ImageSize
  )
{
  if (SegmentInfo->Segments[0].ImageAddress > HeaderInfo->EntryPointAddress ||
      HeaderInfo->EntryPointAddress > ImageSize) {
    DEBUG_RAISE ();
    return false;
  }

  // LCOV_EXCL_START
  if (!IS_ALIGNED (HeaderInfo->BaseAddress, SegmentInfo->SegmentAlignment)) {
    assert (false);
    return false;
  }
  // LCOV_EXCL_STOP

  // LCOV_EXCL_START
  if (HeaderInfo->BaseAddress + ImageSize < HeaderInfo->BaseAddress) {
    assert (false);
    return false;
  }
  // LCOV_EXCL_STOP

  return true;
}

const image_tool_segment_t *
ImageGetSegmentByAddress (
  uint32_t                        *Address,
  uint32_t                        *RemainingSize,
  const image_tool_segment_info_t *SegmentInfo
  )
{
  uint32_t Index;

  for (Index = 0; Index < SegmentInfo->NumSegments; ++Index) {
    if ((SegmentInfo->Segments[Index].ImageAddress <= *Address)
     && (*Address < SegmentInfo->Segments[Index].ImageAddress + SegmentInfo->Segments[Index].ImageSize)) {
      *Address      -= SegmentInfo->Segments[Index].ImageAddress;
      *RemainingSize = SegmentInfo->Segments[Index].ImageSize - *Address;
      return &SegmentInfo->Segments[Index];
    }
  }

  return NULL;
}

uint8_t
ToolImageGetRelocSize (
  uint8_t  Type
  )
{
  // LCOV_EXCL_START
  switch (Type) {
  // LCOV_EXCL_STOP
    case EFI_IMAGE_REL_BASED_HIGHLOW:
    {
      return sizeof (UINT32);
    }

    case EFI_IMAGE_REL_BASED_DIR64:
    {
      return sizeof (UINT64);
    }

#if 0
    case EFI_IMAGE_REL_BASED_ARM_MOV32T:
    {
      return sizeof (UINT32);
    }
#endif

    // LCOV_EXCL_START
    default:
    {
      break;
    }
    // LCOV_EXCL_STOP
  }

  // LCOV_EXCL_START
  assert (false);
  return 0;
  // LCOV_EXCL_STOP
}

static
bool
CheckToolImageReloc (
  const image_tool_image_info_t *Image,
  const image_tool_reloc_t      *Reloc,
  uint8_t                       RelocSize
  )
{
  uint32_t                   RelocOffset;
  uint32_t                   RemainingSize;
  const image_tool_segment_t *Segment;

#if 0
  uint16_t                   MovHigh;
  uint16_t                   MovLow;
#endif

  RelocOffset = Reloc->Target;
  Segment     = ImageGetSegmentByAddress (
                  &RelocOffset,
                  &RemainingSize,
                  &Image->SegmentInfo
                  );
  if (Segment == NULL) {
    DEBUG_RAISE ();
    return false;
  }

  if (RelocSize > RemainingSize) {
    DEBUG_RAISE ();
    return false;
  }

#if 0
  if (Reloc->Type == EFI_IMAGE_REL_BASED_ARM_MOV32T) {
    if (!IS_ALIGNED (Reloc->Target, ALIGNOF (UINT16))) {
      DEBUG_RAISE ();
      return false;
    }

    MovHigh = *(const uint16_t *)&Segment->Data[RelocOffset];
    MovLow  = *(const uint16_t *)&Segment->Data[RelocOffset + 2];
    if (((MovHigh & 0xFBF0U) != 0xF200U && (MovHigh & 0xFBF0U) != 0xF2C0U) ||
      (MovLow & 0x8000U) != 0) {
      DEBUG_RAISE ();
      return false;
    }
  }
#endif

  // FIXME: Update drivers?
  if (Image->HeaderInfo.Subsystem == EFI_IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER &&
       Segment->Write) {
    printf("!!! writable reloc at %x !!!\n", Reloc->Target);
    //DEBUG_RAISE ();
    //return false;
  }

  return true;
}

static
bool
CheckToolImageRelocInfo (
  const image_tool_image_info_t *Image
  )
{
  const image_tool_reloc_info_t *RelocInfo;
  uint8_t                       RelocSize;
  uint32_t                      MinRelocTarget;
  uint32_t                      Index;
  bool                          Result;

  RelocInfo = &Image->RelocInfo;

  if (RelocInfo->NumRelocs == 0) {
    return true;
  }

  // LCOV_EXCL_START
  if (RelocInfo->RelocsStripped) {
    assert (false);
    return false;
  }
  // LCOV_EXCL_STOP

  if (RelocInfo->NumRelocs > (MAX_UINT32 / sizeof (UINT16))) {
    DEBUG_RAISE ();
    return false;
  }

  MinRelocTarget = 0;

  for (Index = 0; Index < RelocInfo->NumRelocs; ++Index) {
    if (RelocInfo->Relocs[Index].Target < MinRelocTarget) {
      DEBUG_RAISE ();
      return false;
    }

    RelocSize = ToolImageGetRelocSize (RelocInfo->Relocs[Index].Type);
    // LCOV_EXCL_START
    if (RelocSize == 0) {
      assert (false);
      return false;
    }
    // LCOV_EXCL_STOP

    Result = CheckToolImageReloc (Image, &RelocInfo->Relocs[Index], RelocSize);
    if (!Result) {
      DEBUG_RAISE ();
      return false;
    }

    MinRelocTarget = RelocInfo->Relocs[Index].Target + RelocSize;
  }

  return true;
}

static
bool
CheckToolImageDebugInfo (
  const image_tool_debug_info_t *DebugInfo
  )
{
  if (DebugInfo->SymbolsPathLen > MAX_UINT8) {
    DEBUG_RAISE ();
    return false;
  }

  return true;
}

bool
CheckToolImage (
  const image_tool_image_info_t *Image
  )
{
  bool     Result;
  uint32_t ImageSize;

  Result = CheckToolImageSegmentInfo (&Image->SegmentInfo, &ImageSize);
  if (!Result) {
    DEBUG_RAISE ();
    return false;
  }

  Result = CheckToolImageHeaderInfo (
             &Image->HeaderInfo,
             &Image->SegmentInfo,
             ImageSize
             );
  if (!Result) {
    DEBUG_RAISE ();
    return false;
  }

  Result = CheckToolImageRelocInfo (Image);
  if (!Result) {
    DEBUG_RAISE ();
    return false;
  }

  Result = CheckToolImageDebugInfo (&Image->DebugInfo);
  if (!Result) {
    DEBUG_RAISE ();
    return false;
  }

  return true;
}

void
ImageInitUnpaddedSize (
  const image_tool_image_info_t *Image
  )
{
  uint32_t              Index;
  image_tool_segment_t  *Segment;

  for (Index = 0; Index < Image->SegmentInfo.NumSegments; ++Index) {
    Segment = &Image->SegmentInfo.Segments[Index];
    Segment->UnpaddedSize = Segment->ImageSize;

    for (; Segment->UnpaddedSize > 0; --Segment->UnpaddedSize) {
      if (Segment->Data[Segment->UnpaddedSize - 1] != 0) {
        break;
      }
    }
  }
}

void
ToolImageDestruct (
  image_tool_image_info_t *Image
  )
{
  uint16_t Index;

  // LCOV_EXCL_START
  if (Image->SegmentInfo.Segments != NULL) {
  // LCOV_EXCL_STOP
    for (Index = 0; Index < Image->SegmentInfo.NumSegments; ++Index) {
      // LCOV_EXCL_START
      if (Image->SegmentInfo.Segments[Index].Name != NULL) {
      // LCOV_EXCL_STOP
        FreePool (Image->SegmentInfo.Segments[Index].Name);
      }

      // LCOV_EXCL_START
      if (Image->SegmentInfo.Segments[Index].Data != NULL) {
      // LCOV_EXCL_STOP
        FreePool (Image->SegmentInfo.Segments[Index].Data);
      }
    }

    // LCOV_EXCL_START
    if (Image->SegmentInfo.Segments != NULL) {
    // LCOV_EXCL_STOP
      FreePool (Image->SegmentInfo.Segments);
    }
  }

  // LCOV_EXCL_START
  if (Image->HiiInfo.Data != NULL) {
  // LCOV_EXCL_STOP
    FreePool (Image->HiiInfo.Data);
  }

  // LCOV_EXCL_START
  if (Image->RelocInfo.Relocs != NULL) {
  // LCOV_EXCL_STOP
    FreePool (Image->RelocInfo.Relocs);
  }

  // LCOV_EXCL_START
  if (Image->DebugInfo.SymbolsPath != NULL) {
  // LCOV_EXCL_STOP
    FreePool (Image->DebugInfo.SymbolsPath);
  }

  memset (Image, 0, sizeof (*Image));
}

bool
ToolImageRelocate (
  image_tool_image_info_t *Image,
  uint64_t                BaseAddress,
  uint32_t                IgnorePrefix
  )
{
  const image_tool_segment_t *LastSegment;
  uint32_t                   ImageSize;
  uint64_t                   Adjust;
  const image_tool_reloc_t   *Reloc;
  uint32_t                   RelocOffset;
  uint32_t                   RemainingSize;
  const image_tool_segment_t *Segment;
  uint32_t                   Index;
  uint32_t                   RelocTarget32;
  uint64_t                   RelocTarget64;

  if (!IS_ALIGNED (BaseAddress, Image->SegmentInfo.SegmentAlignment)) {
    DEBUG_RAISE ();
    return false;
  }

  Adjust = BaseAddress - Image->HeaderInfo.BaseAddress;

  if (Adjust == 0) {
    return true;
  }

  LastSegment = &Image->SegmentInfo.Segments[Image->SegmentInfo.NumSegments - 1];
  ImageSize   = LastSegment->ImageAddress + LastSegment->ImageSize;

  //
  // When removing the image header prefix, BaseAddress + ImageSize may indeed
  // overflow. The important part is that the address starting from the first
  // image segment does not.
  //
  if (BaseAddress + ImageSize < BaseAddress + IgnorePrefix) {
    DEBUG_RAISE ();
    return false;
  }

  for (Index = 0; Index < Image->RelocInfo.NumRelocs; ++Index) {
    Reloc = &Image->RelocInfo.Relocs[Index];

    RelocOffset = Reloc->Target;
    Segment = ImageGetSegmentByAddress (
                &RelocOffset,
                &RemainingSize,
                &Image->SegmentInfo
                );
    assert (Segment != NULL);

    // LCOV_EXCL_START
    switch (Reloc->Type) {
    // LCOV_EXCL_STOP
      case EFI_IMAGE_REL_BASED_HIGHLOW:
      {
        assert (RemainingSize >= sizeof (UINT32));

        RelocTarget32  = ReadUnaligned32 ((CONST VOID *)&Segment->Data[RelocOffset]);
        RelocTarget32 += (uint32_t)Adjust;
        WriteUnaligned32 ((VOID *)&Segment->Data[RelocOffset], RelocTarget32);
        break;
      }

      case EFI_IMAGE_REL_BASED_DIR64:
      {
        assert (RemainingSize >= sizeof (UINT64));

        RelocTarget64  = ReadUnaligned64 ((CONST VOID *)&Segment->Data[RelocOffset]);
        RelocTarget64 += Adjust;
        WriteUnaligned64 ((VOID *)&Segment->Data[RelocOffset], RelocTarget64);
        break;
      }

#if 0
      case EFI_IMAGE_REL_BASED_ARM_MOV32T:
      {
        assert (RemainingSize >= sizeof (UINT32));
        assert (IS_ALIGNED (Reloc->Target, ALIGNOF (UINT16)));

        PeCoffThumbMovwMovtImmediateFixup (&Segment->Data[RelocOffset], Adjust);
        break;
      }
#endif

      // LCOV_EXCL_START
      default:
      {
        assert (false);
        return false;
      }
      // LCOV_EXCL_STOP
    }
  }

  Image->HeaderInfo.BaseAddress = BaseAddress;

  return true;
}

static
int
ToolImageRelocCompare (
  IN const void  *Buffer1,
  IN const void  *Buffer2
  )
{
  const image_tool_reloc_t  *Reloc1;
  const image_tool_reloc_t  *Reloc2;

  Reloc1 = (const image_tool_reloc_t *)Buffer1;
  Reloc2 = (const image_tool_reloc_t *)Buffer2;

  if (Reloc1->Target < Reloc2->Target) {
    return -1;
  }

  if (Reloc1->Target > Reloc2->Target) {
    return 1;
  }

  return 0;
}

void
ToolImageSortRelocs (
  image_tool_image_info_t  *Image
  )
{
  if (Image->RelocInfo.Relocs == NULL) {
    return;
  }

  qsort (
    Image->RelocInfo.Relocs,
    Image->RelocInfo.NumRelocs,
    sizeof (*Image->RelocInfo.Relocs),
    ToolImageRelocCompare
    );
}

bool
ToolImageCompare (
  const image_tool_image_info_t  *DestImage,
  const image_tool_image_info_t  *SourceImage
  )
{
  int         CmpResult;
  uint32_t    SegIndex;
  const char  *DestName;
  const char  *SourceName;
  uint32_t    NameIndex;

  //
  // Compare HeaderInfo.
  //

  CmpResult = memcmp (
                &DestImage->HeaderInfo,
                &SourceImage->HeaderInfo,
                sizeof (DestImage->HeaderInfo)
                );
  // LCOV_EXCL_START
  if (CmpResult != 0) {
    DEBUG_RAISE ();
    return false;
  }
  // LCOV_EXCL_STOP

  //
  // Compare SegmentInfo.
  // UnpaddedSize is deliberately omitted, as it's implicit by the equality of
  // ImageSize and Data.
  //

  CmpResult = memcmp (
                &DestImage->SegmentInfo,
                &SourceImage->SegmentInfo,
                OFFSET_OF (image_tool_segment_info_t, Segments)
                );
  // LCOV_EXCL_START
  if (CmpResult != 0) {
    DEBUG_RAISE ();
    return false;
  }
  // LCOV_EXCL_STOP

  for (SegIndex = 0; SegIndex < DestImage->SegmentInfo.NumSegments; ++SegIndex) {
    CmpResult = memcmp (
                  &DestImage->SegmentInfo.Segments[SegIndex],
                  &SourceImage->SegmentInfo.Segments[SegIndex],
                  OFFSET_OF (image_tool_segment_t, Name)
                  );
    // LCOV_EXCL_START
    if (CmpResult != 0) {
      DEBUG_RAISE ();
      return false;
    }
    // LCOV_EXCL_STOP

    //
    // Don't assume images generally support arbitrarily long names or names in
    // general. Check prefix equiality as a best effort.
    //
    DestName   = DestImage->SegmentInfo.Segments[SegIndex].Name;
    SourceName = SourceImage->SegmentInfo.Segments[SegIndex].Name;

    // LCOV_EXCL_START
    if (DestName != NULL && SourceName == NULL) {
      assert (false);
      return false;
    }
    // LCOV_EXCL_STOP

    //
    // When omitting the debug info, some file formats (e.g., UE) may not
    // contain segment names.
    //
    if (DestName != NULL) {
      for (
        NameIndex = 0;
        DestName[NameIndex] != '\0' &&
        // LCOV_EXCL_START
        SourceName[NameIndex] != '\0';
        // LCOV_EXCL_STOP
        ++NameIndex
        ) {
        // LCOV_EXCL_START
        if (DestName[NameIndex] != SourceName[NameIndex]) {
          DEBUG_RAISE ();
          return false;
        }
        // LCOV_EXCL_STOP
      }
    }

    if (DestImage->SegmentInfo.Segments[SegIndex].ImageSize != 0) {
      CmpResult = memcmp (
                    DestImage->SegmentInfo.Segments[SegIndex].Data,
                    SourceImage->SegmentInfo.Segments[SegIndex].Data,
                    DestImage->SegmentInfo.Segments[SegIndex].ImageSize
                    );
      // LCOV_EXCL_START
      if (CmpResult != 0) {
        DEBUG_RAISE ();
        return false;
      }
      // LCOV_EXCL_STOP
    }
  }

  //
  // Compare RelocInfo.
  //

  CmpResult = memcmp (
                &DestImage->RelocInfo,
                &SourceImage->RelocInfo,
                OFFSET_OF (image_tool_reloc_info_t, Relocs)
                );
  // LCOV_EXCL_START
  if (CmpResult != 0) {
    DEBUG_RAISE ();
    return false;
  }
  // LCOV_EXCL_STOP

  if (DestImage->RelocInfo.NumRelocs != 0) {
    CmpResult = memcmp (
                  DestImage->RelocInfo.Relocs,
                  SourceImage->RelocInfo.Relocs,
                  DestImage->RelocInfo.NumRelocs * sizeof (*DestImage->RelocInfo.Relocs)
                  );
    // LCOV_EXCL_START
    if (CmpResult != 0) {
      DEBUG_RAISE ();
      return false;
    }
    // LCOV_EXCL_STOP
  }

  //
  // Compare HiiInfo.
  //

  CmpResult = memcmp (
                &DestImage->HiiInfo,
                &SourceImage->HiiInfo,
                OFFSET_OF (image_tool_hii_info_t, Data)
                );
  // LCOV_EXCL_START
  if (CmpResult != 0) {
    DEBUG_RAISE ();
    return false;
  }
  // LCOV_EXCL_STOP

  // LCOV_EXCL_START
  if (DestImage->HiiInfo.DataSize != 0) {
    CmpResult = memcmp (
                  DestImage->HiiInfo.Data,
                  SourceImage->HiiInfo.Data,
                  DestImage->HiiInfo.DataSize
                  );
    if (CmpResult != 0) {
      DEBUG_RAISE ();
      return false;
    }
  }
  // LCOV_EXCL_STOP

  //
  // Compare DebugInfo.
  //

  CmpResult = memcmp (
                &DestImage->DebugInfo,
                &SourceImage->DebugInfo,
                OFFSET_OF (image_tool_debug_info_t, SymbolsPath)
                );
  // LCOV_EXCL_START
  if (CmpResult != 0) {
    DEBUG_RAISE ();
    return false;
  }
  // LCOV_EXCL_STOP

  // LCOV_EXCL_START
  if ((DestImage->DebugInfo.SymbolsPath != NULL) != (SourceImage->DebugInfo.SymbolsPath != NULL)) {
    DEBUG_RAISE ();
    return false;
  }
  // LCOV_EXCL_STOP

  if (DestImage->DebugInfo.SymbolsPath != NULL) {
    CmpResult = strcmp (
                  DestImage->DebugInfo.SymbolsPath,
                  SourceImage->DebugInfo.SymbolsPath
                  );
    // LCOV_EXCL_START
    if (CmpResult != 0) {
      DEBUG_RAISE ();
      return false;
    }
    // LCOV_EXCL_STOP
  }

  return true;
}

void
ToolImageStripRelocs (
  image_tool_image_info_t  *Image
  )
{
  Image->RelocInfo.NumRelocs = 0;

  if (Image->RelocInfo.Relocs != NULL) {
    FreePool (Image->RelocInfo.Relocs);
  }

  Image->RelocInfo.Relocs = NULL;

  Image->RelocInfo.RelocsStripped = TRUE;
}
