/** @file
  Inode related routines

  Copyright (c) 2021 - 2022 Pedro Falcato All rights reserved.
  SPDX-License-Identifier: BSD-2-Clause-Patent

  EpochToEfiTime copied from EmbeddedPkg/Library/TimeBaseLib.c
  Copyright (c) 2016, Hisilicon Limited. All rights reserved.
  Copyright (c) 2016-2019, Linaro Limited. All rights reserved.
  Copyright (c) 2021, Ampere Computing LLC. All rights reserved.
**/

#include "Ext4Dxe.h"

/**
   Calculates the checksum of the given inode.
   @param[in]      Partition     Pointer to the opened EXT4 partition.
   @param[in]      Inode         Pointer to the inode.
   @param[in]      InodeNum      Inode number.

   @return The checksum.
**/
UINT32
Ext4CalculateInodeChecksum (
  IN CONST EXT4_PARTITION  *Partition,
  IN CONST EXT4_INODE      *Inode,
  IN EXT4_INO_NR           InodeNum
  )
{
  UINT32      Crc;
  UINT16      Dummy;
  BOOLEAN     HasSecondChecksumField;
  CONST VOID  *RestOfInode;
  UINTN       RestOfInodeLength;
  UINTN       Length;

  HasSecondChecksumField = EXT4_INODE_HAS_FIELD (Inode, i_checksum_hi);

  Dummy = 0;

  Crc = Ext4CalculateChecksum (Partition, &InodeNum, sizeof (InodeNum), Partition->InitialSeed);
  Crc = Ext4CalculateChecksum (Partition, &Inode->i_generation, sizeof (Inode->i_generation), Crc);

  Crc = Ext4CalculateChecksum (
          Partition,
          Inode,
          OFFSET_OF (EXT4_INODE, i_osd2.data_linux.l_i_checksum_lo),
          Crc
          );

  Crc = Ext4CalculateChecksum (Partition, &Dummy, sizeof (Dummy), Crc);

  RestOfInode       = &Inode->i_osd2.data_linux.l_i_reserved;
  RestOfInodeLength = Partition->InodeSize - OFFSET_OF (EXT4_INODE, i_osd2.data_linux.l_i_reserved);

  if (HasSecondChecksumField) {
    Length = OFFSET_OF (EXT4_INODE, i_checksum_hi) - OFFSET_OF (EXT4_INODE, i_osd2.data_linux.l_i_reserved);

    Crc = Ext4CalculateChecksum (Partition, &Inode->i_osd2.data_linux.l_i_reserved, Length, Crc);
    Crc = Ext4CalculateChecksum (Partition, &Dummy, sizeof (Dummy), Crc);

    // 4 is the size of the i_extra_size field + the size of i_checksum_hi
    RestOfInodeLength = Partition->InodeSize - EXT4_GOOD_OLD_INODE_SIZE - 4;
    RestOfInode       = &Inode->i_ctime_extra;
  }

  Crc = Ext4CalculateChecksum (Partition, RestOfInode, RestOfInodeLength, Crc);

  return Crc;
}

/**
   Reads from an EXT4 inode.
   @param[in]      Partition     Pointer to the opened EXT4 partition.
   @param[in]      File          Pointer to the opened file.
   @param[out]     Buffer        Pointer to the buffer.
   @param[in]      Offset        Offset of the read.
   @param[in out]  Length        Pointer to the length of the buffer, in bytes.
                                 After a successful read, it's updated to the number of read bytes.

   @return Status of the read operation.
**/
EFI_STATUS
Ext4Read (
  IN     EXT4_PARTITION  *Partition,
  IN     EXT4_FILE       *File,
  OUT    VOID            *Buffer,
  IN     UINT64          Offset,
  IN OUT UINTN           *Length
  )
{
  EXT4_INODE   *Inode;
  UINT64       InodeSize;
  UINT64       CurrentSeek;
  UINTN        RemainingRead;
  UINTN        BeenRead;
  UINTN        WasRead;
  EXT4_EXTENT  Extent;
  UINT32       BlockOff;
  EFI_STATUS   Status;
  BOOLEAN      HasBackingExtent;
  UINT32       HoleOff;
  UINT64       HoleLen;
  UINT64       ExtentStartBytes;
  UINT64       ExtentLengthBytes;
  UINT64       ExtentLogicalBytes;

  // Our extent offset is the difference between CurrentSeek and ExtentLogicalBytes
  UINT64  ExtentOffset;
  UINTN   ExtentMayRead;

  Inode         = File->Inode;
  InodeSize     = EXT4_INODE_SIZE (Inode);
  CurrentSeek   = Offset;
  RemainingRead = *Length;
  BeenRead      = 0;

  DEBUG ((DEBUG_FS, "[ext4] Ext4Read(%s, Offset %lu, Length %lu)\n", File->Dentry->Name, Offset, *Length));

  if (Offset > InodeSize) {
    return EFI_DEVICE_ERROR;
  }

  if (RemainingRead > InodeSize - Offset) {
    RemainingRead = (UINTN)(InodeSize - Offset);
  }

  while (RemainingRead != 0) {
    WasRead = 0;

    // The algorithm here is to get the extent corresponding to the current block
    // and then read as much as we can from the current extent.

    Status = Ext4GetExtent (
               Partition,
               File,
               DivU64x32Remainder (CurrentSeek, Partition->BlockSize, &BlockOff),
               &Extent
               );

    if ((Status != EFI_SUCCESS) && (Status != EFI_NO_MAPPING)) {
      return Status;
    }

    HasBackingExtent = Status != EFI_NO_MAPPING;

    if (!HasBackingExtent || EXT4_EXTENT_IS_UNINITIALIZED (&Extent)) {
      HoleOff = BlockOff;

      if (!HasBackingExtent) {
        HoleLen = Partition->BlockSize - HoleOff;
      } else {
        // Uninitialized extents behave exactly the same as file holes, except they have
        // blocks already allocated to them.
        HoleLen = MultU64x32 (Ext4GetExtentLength (&Extent), Partition->BlockSize) - HoleOff;
      }

      WasRead = HoleLen > RemainingRead ? RemainingRead : (UINTN)HoleLen;
      // Potential improvement: In the future, we could get the file hole's total
      // size and memset all that
      ZeroMem (Buffer, WasRead);
    } else {
      ExtentStartBytes = MultU64x32 (
                           LShiftU64 (Extent.ee_start_hi, 32) |
                           Extent.ee_start_lo,
                           Partition->BlockSize
                           );
      ExtentLengthBytes  = Extent.ee_len * Partition->BlockSize;
      ExtentLogicalBytes = MultU64x32 ((UINT64)Extent.ee_block, Partition->BlockSize);
      ExtentOffset       = CurrentSeek - ExtentLogicalBytes;
      ExtentMayRead      = (UINTN)(ExtentLengthBytes - ExtentOffset);

      WasRead = ExtentMayRead > RemainingRead ? RemainingRead : ExtentMayRead;

      Status = Ext4ReadDiskIo (Partition, Buffer, WasRead, ExtentStartBytes + ExtentOffset);

      if (EFI_ERROR (Status)) {
        DEBUG ((
          DEBUG_ERROR,
          "[ext4] Error %r reading [%lu, %lu]\n",
          Status,
          ExtentStartBytes + ExtentOffset,
          ExtentStartBytes + ExtentOffset + WasRead - 1
          ));
        return Status;
      }
    }

    RemainingRead -= WasRead;
    Buffer         = (VOID *)((CHAR8 *)Buffer + WasRead);
    BeenRead      += WasRead;
    CurrentSeek   += WasRead;
  }

  *Length = BeenRead;

  return EFI_SUCCESS;
}

/**
   Allocates a zeroed inode structure.
   @param[in]      Partition     Pointer to the opened EXT4 partition.

   @return Pointer to the allocated structure, from the pool,
           with size Partition->InodeSize.
**/
EXT4_INODE *
Ext4AllocateInode (
  IN EXT4_PARTITION  *Partition
  )
{
  BOOLEAN     NeedsToZeroRest;
  UINT32      InodeSize;
  EXT4_INODE  *Inode;

  NeedsToZeroRest = FALSE;
  InodeSize       = Partition->InodeSize;

  // We allocate a structure of at least sizeof(EXT4_INODE), but in the future, when
  // write support is added and we need to flush inodes to disk, we could have a bit better
  // distinction between the on-disk inode and a separate, nicer to work with inode struct.
  // It's important to note that EXT4_INODE includes fields that may not exist in an actual
  // filesystem (the minimum inode size is 128 byte and at the moment the size of EXT4_INODE
  // is 160 bytes).

  if (InodeSize < sizeof (EXT4_INODE)) {
    InodeSize       = sizeof (EXT4_INODE);
    NeedsToZeroRest = TRUE;
  }

  Inode = AllocateZeroPool (InodeSize);

  if (Inode == NULL) {
    return NULL;
  }

  if (NeedsToZeroRest) {
    Inode->i_extra_isize = 0;
  }

  return Inode;
}

/**
   Checks if a file is a directory.
   @param[in]      File          Pointer to the opened file.

   @return TRUE if file is a directory.
**/
BOOLEAN
Ext4FileIsDir (
  IN CONST EXT4_FILE  *File
  )
{
  return (File->Inode->i_mode & EXT4_INO_TYPE_DIR) == EXT4_INO_TYPE_DIR;
}

/**
   Checks if a file is a symlink.

   @param[in]      File          Pointer to the opened file.

   @return BOOLEAN         Whether file is a symlink
**/
BOOLEAN
Ext4FileIsSymlink (
  IN CONST EXT4_FILE  *File
  )
{
  return (File->Inode->i_mode & EXT4_INO_TYPE_SYMLINK) == EXT4_INO_TYPE_SYMLINK;
}

/**
   Checks if a file is a regular file.
   @param[in]      File          Pointer to the opened file.

   @return BOOLEAN         TRUE if file is a regular file.
**/
BOOLEAN
Ext4FileIsReg (
  IN CONST EXT4_FILE  *File
  )
{
  return (File->Inode->i_mode & EXT4_INO_TYPE_REGFILE) == EXT4_INO_TYPE_REGFILE;
}

/**
   Calculates the physical space used by a file.
   @param[in]      File          Pointer to the opened file.

   @return Physical space used by a file, in bytes.
**/
UINT64
Ext4FilePhysicalSpace (
  IN EXT4_FILE  *File
  )
{
  BOOLEAN  HugeFile;
  UINT64   Blocks;

  HugeFile = EXT4_HAS_RO_COMPAT (File->Partition, EXT4_FEATURE_RO_COMPAT_HUGE_FILE);
  Blocks   = File->Inode->i_blocks;

  if (HugeFile) {
    Blocks |= LShiftU64 (File->Inode->i_osd2.data_linux.l_i_blocks_high, 32);

    // If HUGE_FILE is enabled and EXT4_HUGE_FILE_FL is set in the inode's flags, each unit
    // in i_blocks corresponds to an actual filesystem block
    if ((File->Inode->i_flags & EXT4_HUGE_FILE_FL) != 0) {
      return MultU64x32 (Blocks, File->Partition->BlockSize);
    }
  }

  // Else, each i_blocks unit corresponds to 512 bytes
  return MultU64x32 (Blocks, 512);
}

// Copied from EmbeddedPkg at my mentor's request.
// The lack of comments and good variable names is frightening...

/**
  Converts Epoch seconds (elapsed since 1970 JANUARY 01, 00:00:00 UTC) to EFI_TIME.

  @param[in]   EpochSeconds   Epoch seconds.
  @param[out]  Time           The time converted to UEFI format.

**/
STATIC
VOID
EFIAPI
EpochToEfiTime (
  IN  UINTN     EpochSeconds,
  OUT EFI_TIME  *Time
  )
{
  UINTN  a;
  UINTN  b;
  UINTN  c;
  UINTN  d;
  UINTN  g;
  UINTN  j;
  UINTN  m;
  UINTN  y;
  UINTN  da;
  UINTN  db;
  UINTN  dc;
  UINTN  dg;
  UINTN  hh;
  UINTN  mm;
  UINTN  ss;
  UINTN  J;

  J  = (EpochSeconds / 86400) + 2440588;
  j  = J + 32044;
  g  = j / 146097;
  dg = j % 146097;
  c  = (((dg / 36524) + 1) * 3) / 4;
  dc = dg - (c * 36524);
  b  = dc / 1461;
  db = dc % 1461;
  a  = (((db / 365) + 1) * 3) / 4;
  da = db - (a * 365);
  y  = (g * 400) + (c * 100) + (b * 4) + a;
  m  = (((da * 5) + 308) / 153) - 2;
  d  = da - (((m + 4) * 153) / 5) + 122;

  Time->Year  = (UINT16)(y - 4800 + ((m + 2) / 12));
  Time->Month = ((m + 2) % 12) + 1;
  Time->Day   = (UINT8)(d + 1);

  ss = EpochSeconds % 60;
  a  = (EpochSeconds - ss) / 60;
  mm = a % 60;
  b  = (a - mm) / 60;
  hh = b % 24;

  Time->Hour       = (UINT8)hh;
  Time->Minute     = (UINT8)mm;
  Time->Second     = (UINT8)ss;
  Time->Nanosecond = 0;
}

// The time format used to (de/en)code timestamp and timestamp_extra is documented on
// the ext4 docs page in kernel.org
#define EXT4_EXTRA_TIMESTAMP_MASK  ((1 << 2) - 1)

#define EXT4_FILE_GET_TIME_GENERIC(Name, Field)            \
  VOID \
  Ext4File ## Name (IN EXT4_FILE  *File, OUT EFI_TIME  *Time) \
  {                                                          \
    EXT4_INODE  *Inode = File->Inode;                       \
    UINT64      SecondsEpoch = Inode->Field;                   \
    UINT32      Nanoseconds  = 0;                                \
                                                           \
    if (EXT4_INODE_HAS_FIELD (Inode, Field ## _extra)) {          \
      SecondsEpoch |= LShiftU64 ((UINT64)(Inode->Field ## _extra & EXT4_EXTRA_TIMESTAMP_MASK), 32); \
      Nanoseconds   = Inode->Field ## _extra >> 2;                                            \
    }                                                                                       \
    EpochToEfiTime ((UINTN)SecondsEpoch, Time);                                                     \
    Time->Nanosecond = Nanoseconds;                                                         \
  }

// Note: EpochToEfiTime should be adjusted to take in a UINT64 instead of a UINTN, in order to avoid Y2038
// on 32-bit systems.

/**
   Gets the file's last access time.
   @param[in]      File   Pointer to the opened file.
   @param[out]     Time   Pointer to an EFI_TIME structure.
**/
EXT4_FILE_GET_TIME_GENERIC (ATime, i_atime);

/**
   Gets the file's last (data) modification time.
   @param[in]      File   Pointer to the opened file.
   @param[out]     Time   Pointer to an EFI_TIME structure.
**/
EXT4_FILE_GET_TIME_GENERIC (MTime, i_mtime);

/**
   Gets the file's creation time.
   @param[in]      File   Pointer to the opened file.
   @param[out]     Time   Pointer to an EFI_TIME structure.
**/
STATIC
EXT4_FILE_GET_TIME_GENERIC (
  CrTime,
  i_crtime
  );

/**
   Gets the file's creation time, if possible.
   @param[in]      File   Pointer to the opened file.
   @param[out]     Time   Pointer to an EFI_TIME structure.
                          In the case where the the creation time isn't recorded,
                          Time is zeroed.
**/
VOID
Ext4FileCreateTime (
  IN EXT4_FILE  *File,
  OUT EFI_TIME  *Time
  )
{
  EXT4_INODE  *Inode;

  Inode = File->Inode;

  if (!EXT4_INODE_HAS_FIELD (Inode, i_crtime)) {
    ZeroMem (Time, sizeof (EFI_TIME));
    return;
  }

  Ext4FileCrTime (File, Time);
}

/**
   Checks if the checksum of the inode is correct.
   @param[in]      Partition     Pointer to the opened EXT4 partition.
   @param[in]      Inode         Pointer to the inode.
   @param[in]      InodeNum      Inode number.

   @return TRUE if checksum is correct, FALSE if there is corruption.
**/
BOOLEAN
Ext4CheckInodeChecksum (
  IN CONST EXT4_PARTITION  *Partition,
  IN CONST EXT4_INODE      *Inode,
  IN EXT4_INO_NR           InodeNum
  )
{
  UINT32  Csum;
  UINT32  DiskCsum;

  if (!EXT4_HAS_METADATA_CSUM (Partition)) {
    return TRUE;
  }

  Csum = Ext4CalculateInodeChecksum (Partition, Inode, InodeNum);

  DiskCsum = Inode->i_osd2.data_linux.l_i_checksum_lo;

  if (EXT4_INODE_HAS_FIELD (Inode, i_checksum_hi)) {
    DiskCsum |= ((UINT32)Inode->i_checksum_hi) << 16;
  } else {
    // Only keep the lower bits for the comparison if the checksum is 16 bits.
    Csum &= 0xffff;
  }

  return Csum == DiskCsum;
}
