/** @file
  Superblock managing routines

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

#include "Ext4Dxe.h"

STATIC CONST UINT32  gSupportedCompatFeat = EXT4_FEATURE_COMPAT_EXT_ATTR;

STATIC CONST UINT32  gSupportedRoCompatFeat =
  EXT4_FEATURE_RO_COMPAT_DIR_NLINK | EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE |
  EXT4_FEATURE_RO_COMPAT_HUGE_FILE | EXT4_FEATURE_RO_COMPAT_LARGE_FILE |
  EXT4_FEATURE_RO_COMPAT_GDT_CSUM | EXT4_FEATURE_RO_COMPAT_METADATA_CSUM | EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER;

STATIC CONST UINT32  gSupportedIncompatFeat =
  EXT4_FEATURE_INCOMPAT_64BIT | EXT4_FEATURE_INCOMPAT_DIRDATA |
  EXT4_FEATURE_INCOMPAT_FLEX_BG | EXT4_FEATURE_INCOMPAT_FILETYPE |
  EXT4_FEATURE_INCOMPAT_EXTENTS | EXT4_FEATURE_INCOMPAT_LARGEDIR |
  EXT4_FEATURE_INCOMPAT_MMP | EXT4_FEATURE_INCOMPAT_RECOVER;

// Future features that may be nice additions in the future:
// 1) Btree support: Required for write support and would speed up lookups in large directories.
// 2) meta_bg: Required to mount meta_bg-enabled partitions.

// Note: We ignore MMP because it's impossible that it's mapped elsewhere,
// I think (unless there's some sort of network setup where we're accessing a remote partition).

// Note on corruption signaling:
// We (Ext4Dxe) could signal corruption by setting s_state to |= EXT4_FS_STATE_ERRORS_DETECTED.
// I've decided against that, because right now the driver is read-only, and
// that would mean we would need to writeback the superblock. If something like
// this is desired, it's fairly trivial to look for EFI_VOLUME_CORRUPTED
// references and add some Ext4SignalCorruption function + function call.

/**
   Checks the superblock's magic value.

   @param[in] DiskIo      Pointer to the DiskIo.
   @param[in] BlockIo     Pointer to the BlockIo.

   @returns Whether the partition has a valid EXT4 superblock magic value.
**/
BOOLEAN
Ext4SuperblockCheckMagic (
  IN EFI_DISK_IO_PROTOCOL   *DiskIo,
  IN EFI_BLOCK_IO_PROTOCOL  *BlockIo
  )
{
  UINT16      Magic;
  EFI_STATUS  Status;

  Status = DiskIo->ReadDisk (
                     DiskIo,
                     BlockIo->Media->MediaId,
                     EXT4_SUPERBLOCK_OFFSET + OFFSET_OF (EXT4_SUPERBLOCK, s_magic),
                     sizeof (Magic),
                     &Magic
                     );
  if (EFI_ERROR (Status)) {
    return FALSE;
  }

  if (Magic != EXT4_SIGNATURE) {
    return FALSE;
  }

  return TRUE;
}

/**
   Does brief validation of the ext4 superblock.

   @param[in] Sb     Pointer to the read superblock.

   @return TRUE if a valid ext4 superblock, else FALSE.
**/
BOOLEAN
Ext4SuperblockValidate (
  CONST EXT4_SUPERBLOCK  *Sb
  )
{
  if (Sb->s_magic != EXT4_SIGNATURE) {
    return FALSE;
  }

  if ((Sb->s_rev_level != EXT4_DYNAMIC_REV) && (Sb->s_rev_level != EXT4_GOOD_OLD_REV)) {
    return FALSE;
  }

  if ((Sb->s_state & EXT4_FS_STATE_UNMOUNTED) == 0) {
    DEBUG ((DEBUG_WARN, "[ext4] Filesystem was not unmounted cleanly\n"));
  }

  return TRUE;
}

/**
   Calculates the superblock's checksum.

   @param[in] Partition    Pointer to the opened partition.
   @param[in] Sb           Pointer to the superblock.

   @return The superblock's checksum.
**/
STATIC
UINT32
Ext4CalculateSuperblockChecksum (
  EXT4_PARTITION         *Partition,
  CONST EXT4_SUPERBLOCK  *Sb
  )
{
  // Most checksums require us to go through a dummy 0 as part of the requirement
  // that the checksum is done over a structure with its checksum field = 0.
  UINT32  Checksum;

  Checksum = Ext4CalculateChecksum (
               Partition,
               Sb,
               OFFSET_OF (EXT4_SUPERBLOCK, s_checksum),
               ~0U
               );

  return Checksum;
}

/**
   Verifies that the superblock's checksum is valid.

   @param[in] Partition    Pointer to the opened partition.
   @param[in] Sb           Pointer to the superblock.

   @return The superblock's checksum.
**/
STATIC
BOOLEAN
Ext4VerifySuperblockChecksum (
  EXT4_PARTITION         *Partition,
  CONST EXT4_SUPERBLOCK  *Sb
  )
{
  if (!EXT4_HAS_METADATA_CSUM (Partition)) {
    return TRUE;
  }

  return Sb->s_checksum == Ext4CalculateSuperblockChecksum (Partition, Sb);
}

/**
   Opens and parses the superblock.

   @param[out]     Partition Partition structure to fill with filesystem details.
   @retval EFI_SUCCESS       Parsing was successful and the partition is a
                             valid ext4 partition.
**/
EFI_STATUS
Ext4OpenSuperblock (
  OUT EXT4_PARTITION  *Partition
  )
{
  UINT32                 Index;
  EFI_STATUS             Status;
  EXT4_SUPERBLOCK        *Sb;
  UINT32                 NrBlocksRem;
  UINTN                  NrBlocks;
  UINT32                 UnsupportedRoCompat;
  EXT4_BLOCK_GROUP_DESC  *Desc;

  Status = Ext4ReadDiskIo (
             Partition,
             &Partition->SuperBlock,
             sizeof (EXT4_SUPERBLOCK),
             EXT4_SUPERBLOCK_OFFSET
             );

  if (EFI_ERROR (Status)) {
    return Status;
  }

  Sb = &Partition->SuperBlock;

  if (!Ext4SuperblockValidate (Sb)) {
    return EFI_VOLUME_CORRUPTED;
  }

  if (Sb->s_rev_level == EXT4_DYNAMIC_REV) {
    Partition->FeaturesCompat   = Sb->s_feature_compat;
    Partition->FeaturesIncompat = Sb->s_feature_incompat;
    Partition->FeaturesRoCompat = Sb->s_feature_ro_compat;
    Partition->InodeSize        = Sb->s_inode_size;

    // Check for proper alignment of InodeSize and that InodeSize is indeed larger than
    // the minimum size, 128 bytes.
    if (((Partition->InodeSize % 4) != 0) || (Partition->InodeSize < EXT4_GOOD_OLD_INODE_SIZE)) {
      return EFI_VOLUME_CORRUPTED;
    }
  } else {
    // GOOD_OLD_REV
    Partition->FeaturesCompat = Partition->FeaturesIncompat = Partition->FeaturesRoCompat = 0;
    Partition->InodeSize      = EXT4_GOOD_OLD_INODE_SIZE;
  }

  // Now, check for the feature set of the filesystem
  // It's essential to check for this to avoid filesystem corruption and to avoid
  // accidentally opening an ext2/3/4 filesystem we don't understand, which would be disastrous.

  if (Partition->FeaturesIncompat & ~gSupportedIncompatFeat) {
    DEBUG ((
      DEBUG_ERROR,
      "[ext4] Unsupported features %lx\n",
      Partition->FeaturesIncompat & ~gSupportedIncompatFeat
      ));
    return EFI_UNSUPPORTED;
  }

  if (EXT4_HAS_INCOMPAT (Partition, EXT4_FEATURE_INCOMPAT_RECOVER)) {
    DEBUG ((DEBUG_WARN, "[ext4] Needs journal recovery, mounting read-only\n"));
    Partition->ReadOnly = TRUE;
  }

  // At the time of writing, it's the only supported checksum.
  if (EXT4_HAS_METADATA_CSUM (Partition) && (Sb->s_checksum_type != EXT4_CHECKSUM_CRC32C)) {
    return EFI_UNSUPPORTED;
  }

  if (EXT4_HAS_INCOMPAT (Partition, EXT4_FEATURE_INCOMPAT_CSUM_SEED)) {
    Partition->InitialSeed = Sb->s_checksum_seed;
  } else {
    Partition->InitialSeed = Ext4CalculateChecksum (Partition, Sb->s_uuid, 16, ~0U);
  }

  UnsupportedRoCompat = Partition->FeaturesRoCompat & ~gSupportedRoCompatFeat;

  if (UnsupportedRoCompat != 0) {
    DEBUG ((DEBUG_WARN, "[ext4] Unsupported ro compat %x\n", UnsupportedRoCompat));
    Partition->ReadOnly = TRUE;
  }

  // gSupportedCompatFeat is documentation-only since we never need to access it.
  // The line below avoids unused variable warnings.
  (VOID)gSupportedCompatFeat;

  DEBUG ((DEBUG_FS, "Read only = %u\n", Partition->ReadOnly));

  if (Sb->s_inodes_per_group == 0) {
    DEBUG ((DEBUG_ERROR, "[ext4] Inodes per group can not be zero\n"));
    return EFI_VOLUME_CORRUPTED;
  }

  if (Sb->s_log_block_size > EXT4_LOG_BLOCK_SIZE_MAX) {
    DEBUG ((DEBUG_ERROR, "[ext4] SuperBlock s_log_block_size %lu is too big\n", Sb->s_log_block_size));
    return EFI_UNSUPPORTED;
  }

  Partition->BlockSize = (UINT32)LShiftU64 (1024, Sb->s_log_block_size);

  // The size of a block group can also be calculated as 8 * Partition->BlockSize
  if (Sb->s_blocks_per_group != 8 * Partition->BlockSize) {
    return EFI_UNSUPPORTED;
  }

  Partition->NumberBlocks      = EXT4_BLOCK_NR_FROM_HALFS (Partition, Sb->s_blocks_count, Sb->s_blocks_count_hi);
  Partition->NumberBlockGroups = DivU64x32 (Partition->NumberBlocks, Sb->s_blocks_per_group);

  DEBUG ((
    DEBUG_FS,
    "[ext4] Number of blocks = %lu\n[ext4] Number of block groups: %lu\n",
    Partition->NumberBlocks,
    Partition->NumberBlockGroups
    ));

  if (EXT4_IS_64_BIT (Partition)) {
    // s_desc_size should be 4 byte aligned and
    // 64 bit filesystems need DescSize to be 64 bytes
    if (((Sb->s_desc_size % 4) != 0) || (Sb->s_desc_size < EXT4_64BIT_BLOCK_DESC_SIZE)) {
      return EFI_VOLUME_CORRUPTED;
    }

    Partition->DescSize = Sb->s_desc_size;
  } else {
    Partition->DescSize = EXT4_OLD_BLOCK_DESC_SIZE;
  }

  if (!Ext4VerifySuperblockChecksum (Partition, Sb)) {
    DEBUG ((DEBUG_ERROR, "[ext4] Bad superblock checksum %lx\n", Ext4CalculateSuperblockChecksum (Partition, Sb)));
    return EFI_VOLUME_CORRUPTED;
  }

  NrBlocks = (UINTN)DivU64x32Remainder (
                      MultU64x32 (Partition->NumberBlockGroups, Partition->DescSize),
                      Partition->BlockSize,
                      &NrBlocksRem
                      );

  if (NrBlocksRem != 0) {
    NrBlocks++;
  }

  Partition->BlockGroups = Ext4AllocAndReadBlocks (Partition, NrBlocks, Partition->BlockSize == 1024 ? 2 : 1);

  if (Partition->BlockGroups == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  for (Index = 0; Index < Partition->NumberBlockGroups; Index++) {
    Desc = Ext4GetBlockGroupDesc (Partition, Index);
    if (!Ext4VerifyBlockGroupDescChecksum (Partition, Desc, Index)) {
      DEBUG ((DEBUG_ERROR, "[ext4] Block group descriptor %u has an invalid checksum\n", Index));
      FreePool (Partition->BlockGroups);
      return EFI_VOLUME_CORRUPTED;
    }
  }

  // RootDentry will serve as the basis of our directory entry tree.
  Partition->RootDentry = Ext4CreateDentry (L"\\", NULL);

  if (Partition->RootDentry == NULL) {
    FreePool (Partition->BlockGroups);
    return EFI_OUT_OF_RESOURCES;
  }

  // Note that the cast below is completely safe, because EXT4_FILE is a specialization of EFI_FILE_PROTOCOL
  Status = Ext4OpenVolume (&Partition->Interface, (EFI_FILE_PROTOCOL **)&Partition->Root);

  if (EFI_ERROR (Status)) {
    Ext4UnrefDentry (Partition->RootDentry);
    FreePool (Partition->BlockGroups);
  }

  return Status;
}

/**
   Calculates the checksum of the given buffer.
   @param[in]      Partition     Pointer to the opened EXT4 partition.
   @param[in]      Buffer        Pointer to the buffer.
   @param[in]      Length        Length of the buffer, in bytes.
   @param[in]      InitialValue  Initial value of the CRC.

   @return The checksum.
**/
UINT32
Ext4CalculateChecksum (
  IN CONST EXT4_PARTITION  *Partition,
  IN CONST VOID            *Buffer,
  IN UINTN                 Length,
  IN UINT32                InitialValue
  )
{
  if (!EXT4_HAS_METADATA_CSUM (Partition)) {
    return 0;
  }

  switch (Partition->SuperBlock.s_checksum_type) {
    case EXT4_CHECKSUM_CRC32C:
      // For some reason, EXT4 really likes non-inverted CRC32C checksums, so we stick to that here.
      return ~CalculateCrc32c(Buffer, Length, ~InitialValue);
    default:
      ASSERT (FALSE);
      return 0;
  }
}
