/** @file
  EFI_FILE_PROTOCOL implementation for EXT4

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

#include "Ext4Dxe.h"

#include <Library/BaseUcs2Utf8Lib.h>

/**
  Reads a symlink file.

  @param[in]      Partition   Pointer to the ext4 partition.
  @param[in]      File        Pointer to the open symlink file.
  @param[out]     Symlink     Pointer to the output unicode symlink string.

  @retval EFI_SUCCESS           Symlink was read.
  @retval EFI_ACCESS_DENIED     Symlink is encrypted.
  @retval EFI_OUT_OF_RESOURCES  Memory allocation error.
  @retval EFI_INVALID_PARAMETER Symlink path has incorrect length
  @retval EFI_VOLUME_CORRUPTED  Symlink read block size differ from inode value
**/
EFI_STATUS
Ext4ReadSymlink (
  IN     EXT4_PARTITION  *Partition,
  IN     EXT4_FILE       *File,
  OUT    CHAR16          **Symlink
  );

/**
   Duplicates a file structure.

   @param[in]        Original    Pointer to the original file.

   @return Pointer to the new file structure.
**/
STATIC
EXT4_FILE *
Ext4DuplicateFile (
  IN CONST EXT4_FILE  *Original
  );

/**
   Gets the next path segment.

   @param[in]        Path        Pointer to the rest of the path.
   @param[out]       PathSegment Pointer to the buffer that will hold the path segment.
                                 Note: It's necessarily EXT4_NAME_MAX +1 long.
   @param[out]       Length      Pointer to the UINTN that will hold the length of the path segment.

   @retval !EFI_SUCCESS          The path segment is too large(> EXT4_NAME_MAX).
**/
STATIC
EFI_STATUS
GetPathSegment (
  IN CONST CHAR16  *Path,
  OUT CHAR16       *PathSegment,
  OUT UINTN        *Length
  )
{
  CONST CHAR16  *Start;
  CONST CHAR16  *End;

  Start = Path;
  End   = Path;

  // The path segment ends on a backslash or a null terminator
  for ( ; *End != L'\0' && *End != L'\\'; End++) {
  }

  *Length = End - Start;

  return StrnCpyS (PathSegment, EXT4_NAME_MAX, Start, End - Start);
}

/**
   Detects if we have more path segments on the path.

   @param[in] Path   Pointer to the rest of the path.
   @return           True if we're on the last segment, false if there are more
                     segments.
**/
STATIC
BOOLEAN
Ext4IsLastPathSegment (
  IN CONST CHAR16  *Path
  )
{
  while (Path[0] == L'\\') {
    Path++;
  }

  return Path[0] == '\0';
}

#define EXT4_INO_PERM_READ_OWNER   0400
#define EXT4_INO_PERM_WRITE_OWNER  0200
#define EXT4_INO_PERM_EXEC_OWNER   0100

/**
   Detects if we have permissions to open the file on the desired mode.

   @param[in out] File         Pointer to the file we're opening.
   @param[in]     OpenMode     Mode in which to open the file.

   @return           True if the open was successful, false if we don't have
                     enough permissions.
**/
STATIC
BOOLEAN
Ext4ApplyPermissions (
  IN OUT EXT4_FILE  *File,
  IN UINT64         OpenMode
  )
{
  UINT16  NeededPerms;

  NeededPerms = 0;

  if ((OpenMode & EFI_FILE_MODE_READ) != 0) {
    NeededPerms |= EXT4_INO_PERM_READ_OWNER;
  }

  if ((OpenMode & EFI_FILE_MODE_WRITE) != 0) {
    NeededPerms |= EXT4_INO_PERM_WRITE_OWNER;
  }

  if ((File->Inode->i_mode & NeededPerms) != NeededPerms) {
    return FALSE;
  }

  File->OpenMode = OpenMode;

  return TRUE;
}

/**
   Detects if we have permissions to search on the directory.

   @param[in out] File         Pointer to the open directory.

   @return           True if we have permission to search, else false.
**/
STATIC
BOOLEAN
Ext4DirCanLookup (
  IN CONST EXT4_FILE  *File
  )
{
  // In UNIX, executable permission on directories means that we have permission to look up
  // files in a directory.
  return (File->Inode->i_mode & EXT4_INO_PERM_EXEC_OWNER) == EXT4_INO_PERM_EXEC_OWNER;
}

/**
  Opens a new file relative to the source file's location.

  @param[out] FoundFile  A pointer to the location to return the opened handle for the new
                         file.
  @param[in]  Source     A pointer to the EXT4_FILE instance that is the file
                         handle to the source location. This would typically be an open
                         handle to a directory.
  @param[in]  FileName   The Null-terminated string of the name of the file to be opened.
                         The file name may contain the following path modifiers: "\", ".",
                         and "..".
  @param[in]  OpenMode   The mode to open the file. The only valid combinations that the
                         file may be opened with are: Read, Read/Write, or Create/Read/Write.
  @param[in]  Attributes Only valid for EFI_FILE_MODE_CREATE, in which case these are the
                         attribute bits for the newly created file.

  @retval EFI_SUCCESS          The file was opened.
  @retval EFI_NOT_FOUND        The specified file could not be found on the device.
  @retval EFI_NO_MEDIA         The device has no medium.
  @retval EFI_MEDIA_CHANGED    The device has a different medium in it or the medium is no
                               longer supported.
  @retval EFI_DEVICE_ERROR     The device reported an error.
  @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted.
  @retval EFI_WRITE_PROTECTED  An attempt was made to create a file, or open a file for write
                               when the media is write-protected.
  @retval EFI_ACCESS_DENIED    The service denied access to the file.
  @retval EFI_OUT_OF_RESOURCES Not enough resources were available to open the file.
  @retval EFI_VOLUME_FULL      The volume is full.

**/
EFI_STATUS
Ext4OpenInternal (
  OUT EXT4_FILE  **FoundFile,
  IN  EXT4_FILE  *Source,
  IN  CHAR16     *FileName,
  IN  UINT64     OpenMode,
  IN  UINT64     Attributes
  )
{
  EXT4_FILE       *Current;
  EXT4_PARTITION  *Partition;
  UINTN           Level;
  CHAR16          PathSegment[EXT4_NAME_MAX + 1];
  UINTN           Length;
  EXT4_FILE       *File;
  CHAR16          *Symlink;
  EFI_STATUS      Status;

  Current   = Source;
  Partition = Current->Partition;
  Level     = 0;

  DEBUG ((DEBUG_FS, "[ext4] Ext4OpenInternal %s\n", FileName));

  if (!Ext4FileIsDir (Current)) {
    return EFI_INVALID_PARAMETER;
  }

  // If the path starts with a backslash, we treat the root directory as the base directory
  if (FileName[0] == L'\\') {
    FileName++;
    Current = Partition->Root;
  }

  while (FileName[0] != L'\0') {
    if (Partition->Root->SymLoops > SYMLOOP_MAX) {
      DEBUG ((DEBUG_FS, "[ext4] Symloop limit is hit !\n"));
      return EFI_ACCESS_DENIED;
    }

    if (!Ext4FileIsDir (Current)) {
      return EFI_INVALID_PARAMETER;
    }

    // Discard leading path separators
    while (FileName[0] == L'\\') {
      FileName++;
    }

    if (GetPathSegment (FileName, PathSegment, &Length) != EFI_SUCCESS) {
      return EFI_BUFFER_TOO_SMALL;
    }

    // Reached the end of the path
    if (Length == 0) {
      break;
    }

    FileName += Length;

    if (StrCmp (PathSegment, L".") == 0) {
      // Opens of "." are a no-op
      continue;
    }

    DEBUG ((DEBUG_FS, "[ext4] Opening %s\n", PathSegment));

    if (!Ext4IsLastPathSegment (FileName)) {
      if (!Ext4DirCanLookup (Current)) {
        return EFI_ACCESS_DENIED;
      }
    }

    Status = Ext4OpenFile (Current, PathSegment, Partition, EFI_FILE_MODE_READ, &File);

    if (EFI_ERROR (Status) && (Status != EFI_NOT_FOUND)) {
      return Status;
    } else if (Status == EFI_NOT_FOUND) {
      // We explicitly ignore the EFI_FILE_MODE_CREATE flag, since we don't have write support
      /// If/ we add write support, this should be changed.
      return Status;
    }

    // Check if this is a valid file to open in EFI
    if (!Ext4FileIsOpenable (File)) {
      Ext4CloseInternal (File);
      // This looks like an /okay/ status to return.
      return EFI_ACCESS_DENIED;
    }

    //
    // Reading symlink and then trying to follow it
    //
    if (Ext4FileIsSymlink (File)) {
      Partition->Root->SymLoops++;
      DEBUG ((DEBUG_FS, "[ext4] File %s is symlink, trying to read it\n", PathSegment));
      Status = Ext4ReadSymlink (Partition, File, &Symlink);
      if (EFI_ERROR (Status)) {
        DEBUG ((DEBUG_FS, "[ext4] Error reading %s symlink!\n", PathSegment));
        return Status;
      }

      DEBUG ((DEBUG_FS, "[ext4] File %s is linked to %s\n", PathSegment, Symlink));
      //
      // Close symlink file
      //
      Ext4CloseInternal (File);
      //
      // Open linked file by recursive call of Ext4OpenFile
      //
      Status = Ext4OpenInternal (FoundFile, Current, Symlink, OpenMode, Attributes);
      FreePool (Symlink);
      if (EFI_ERROR (Status)) {
        DEBUG ((DEBUG_FS, "[ext4] Error opening linked file %s\n", Symlink));
        return Status;
      }

      //
      // Set File to newly opened
      //
      File = *FoundFile;
    }

    if (Level != 0) {
      // Careful not to close the base directory
      Ext4CloseInternal (Current);
    }

    Level++;

    Current = File;
  }

  if (Level == 0) {
    // We opened the base directory again, so we need to duplicate the file structure
    Current = Ext4DuplicateFile (Current);
    if (Current == NULL) {
      return EFI_OUT_OF_RESOURCES;
    }
  }

  if (!Ext4ApplyPermissions (Current, OpenMode)) {
    Ext4CloseInternal (Current);
    return EFI_ACCESS_DENIED;
  }

  *FoundFile = Current;

  DEBUG ((DEBUG_FS, "[ext4] Opened filename %s\n", Current->Dentry->Name));
  return EFI_SUCCESS;
}

/**
  Opens a new file relative to the source file's location.
  @param[in]  This       A pointer to the EFI_FILE_PROTOCOL instance that is the file
                         handle to the source location. This would typically be an open
                         handle to a directory.
  @param[out] NewHandle  A pointer to the location to return the opened handle for the new
                         file.
  @param[in]  FileName   The Null-terminated string of the name of the file to be opened.
                         The file name may contain the following path modifiers: "\", ".",
                         and "..".
  @param[in]  OpenMode   The mode to open the file. The only valid combinations that the
                         file may be opened with are: Read, Read/Write, or Create/Read/Write.
  @param[in]  Attributes Only valid for EFI_FILE_MODE_CREATE, in which case these are the
                         attribute bits for the newly created file.
  @retval EFI_SUCCESS          The file was opened.
  @retval EFI_NOT_FOUND        The specified file could not be found on the device.
  @retval EFI_NO_MEDIA         The device has no medium.
  @retval EFI_MEDIA_CHANGED    The device has a different medium in it or the medium is no
                               longer supported.
  @retval EFI_DEVICE_ERROR     The device reported an error.
  @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted.
  @retval EFI_WRITE_PROTECTED  An attempt was made to create a file, or open a file for write
                               when the media is write-protected.
  @retval EFI_ACCESS_DENIED    The service denied access to the file.
  @retval EFI_OUT_OF_RESOURCES Not enough resources were available to open the file.
  @retval EFI_VOLUME_FULL      The volume is full.
**/
EFI_STATUS
EFIAPI
Ext4Open (
  IN EFI_FILE_PROTOCOL   *This,
  OUT EFI_FILE_PROTOCOL  **NewHandle,
  IN CHAR16              *FileName,
  IN UINT64              OpenMode,
  IN UINT64              Attributes
  )
{
  EFI_STATUS  Status;
  EXT4_FILE   *FoundFile;
  EXT4_FILE   *Source;

  Source = EXT4_FILE_FROM_THIS (This);

  //
  // Reset SymLoops counter
  //
  Source->Partition->Root->SymLoops = 0;

  Status = Ext4OpenInternal (
             &FoundFile,
             Source,
             FileName,
             OpenMode,
             Attributes
             );

  if (!EFI_ERROR (Status)) {
    *NewHandle = &FoundFile->Protocol;
  }

  return Status;
}

/**
  Closes a specified file handle.

  @param[in]  This          A pointer to the EFI_FILE_PROTOCOL instance that is the file
                            handle to close.

  @retval EFI_SUCCESS   The file was closed.

**/
EFI_STATUS
EFIAPI
Ext4Close (
  IN EFI_FILE_PROTOCOL  *This
  )
{
  return Ext4CloseInternal (EXT4_FILE_FROM_THIS (This));
}

/**
   Closes a file.

   @param[in]        File        Pointer to the file.

   @return Status of the closing of the file.
**/
EFI_STATUS
Ext4CloseInternal (
  IN EXT4_FILE  *File
  )
{
  if ((File == File->Partition->Root) && !File->Partition->Unmounting) {
    return EFI_SUCCESS;
  }

  DEBUG ((DEBUG_FS, "[ext4] Closed file %p (inode %lu)\n", File, File->InodeNum));
  RemoveEntryList (&File->OpenFilesListNode);
  FreePool (File->Inode);
  Ext4FreeExtentsMap (File);
  Ext4UnrefDentry (File->Dentry);
  FreePool (File);
  return EFI_SUCCESS;
}

/**
  Close and delete the file handle.

  @param[in]  This                     A pointer to the EFI_FILE_PROTOCOL instance that is the
                                       handle to the file to delete.

  @retval EFI_SUCCESS              The file was closed and deleted, and the handle was closed.
  @retval EFI_WARN_DELETE_FAILURE  The handle was closed, but the file was not deleted.

**/
EFI_STATUS
EFIAPI
Ext4Delete (
  IN EFI_FILE_PROTOCOL  *This
  )
{
  // We do a regular close here since we don't have write support.
  Ext4Close (This);
  return EFI_WARN_DELETE_FAILURE;
}

/**
  Reads data from a file.

  @param[in]      This             A pointer to the EFI_FILE_PROTOCOL instance that is the file
                                   handle to read data from.
  @param[in out]  BufferSize       On input, the size of the Buffer. On output, the amount of data
                                   returned in Buffer. In both cases, the size is measured in bytes.
  @param[out]     Buffer           The buffer into which the data is read.

  @retval EFI_SUCCESS          Data was read.
  @retval EFI_NO_MEDIA         The device has no medium.
  @retval EFI_DEVICE_ERROR     The device reported an error.
  @retval EFI_DEVICE_ERROR     An attempt was made to read from a deleted file.
  @retval EFI_DEVICE_ERROR     On entry, the current file position is beyond the end of the file.
  @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted.
  @retval EFI_BUFFER_TOO_SMALL The BufferSize is too small to read the current directory
                               entry. BufferSize has been updated with the size
                               needed to complete the request.

**/
EFI_STATUS
EFIAPI
Ext4ReadFile (
  IN EFI_FILE_PROTOCOL  *This,
  IN OUT UINTN          *BufferSize,
  OUT VOID              *Buffer
  )
{
  EXT4_FILE       *File;
  EXT4_PARTITION  *Partition;
  EFI_STATUS      Status;

  File      = EXT4_FILE_FROM_THIS (This);
  Partition = File->Partition;

  ASSERT (Ext4FileIsOpenable (File));

  if (Ext4FileIsReg (File)) {
    Status = Ext4Read (Partition, File, Buffer, File->Position, BufferSize);
    if (Status == EFI_SUCCESS) {
      File->Position += *BufferSize;
    }

    return Status;
  } else if (Ext4FileIsDir (File)) {
    Status = Ext4ReadDir (Partition, File, Buffer, File->Position, BufferSize);

    return Status;
  }

  return EFI_SUCCESS;
}

/**
  Writes data to a file.

  @param[in]      This        A pointer to the EFI_FILE_PROTOCOL instance that is the file
                              handle to write data to.
  @param[in out]  BufferSize  On input, the size of the Buffer. On output, the amount of data
                              actually written. In both cases, the size is measured in bytes.
  @param[in]      Buffer      The buffer of data to write.

  @retval EFI_SUCCESS          Data was written.
  @retval EFI_UNSUPPORTED      Writes to open directory files are not supported.
  @retval EFI_NO_MEDIA         The device has no medium.
  @retval EFI_DEVICE_ERROR     The device reported an error.
  @retval EFI_DEVICE_ERROR     An attempt was made to write to a deleted file.
  @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted.
  @retval EFI_WRITE_PROTECTED  The file or medium is write-protected.
  @retval EFI_ACCESS_DENIED    The file was opened read only.
  @retval EFI_VOLUME_FULL      The volume is full.

**/
EFI_STATUS
EFIAPI
Ext4WriteFile (
  IN EFI_FILE_PROTOCOL  *This,
  IN OUT UINTN          *BufferSize,
  IN VOID               *Buffer
  )
{
  EXT4_FILE  *File;

  File = EXT4_FILE_FROM_THIS (This);

  if (!(File->OpenMode & EFI_FILE_MODE_WRITE)) {
    return EFI_ACCESS_DENIED;
  }

  return EFI_WRITE_PROTECTED;
}

/**
  Returns a file's current position.

  @param[in]   This            A pointer to the EFI_FILE_PROTOCOL instance that is the file
                               handle to get the current position on.
  @param[out]  Position        The address to return the file's current position value.

  @retval EFI_SUCCESS      The position was returned.
  @retval EFI_UNSUPPORTED  The request is not valid on open directories.
  @retval EFI_DEVICE_ERROR An attempt was made to get the position from a deleted file.

**/
EFI_STATUS
EFIAPI
Ext4GetPosition (
  IN EFI_FILE_PROTOCOL  *This,
  OUT UINT64            *Position
  )
{
  EXT4_FILE  *File;

  File = EXT4_FILE_FROM_THIS (This);

  if (Ext4FileIsDir (File)) {
    return EFI_UNSUPPORTED;
  }

  *Position = File->Position;

  return EFI_SUCCESS;
}

/**
  Sets a file's current position.

  @param[in]  This            A pointer to the EFI_FILE_PROTOCOL instance that is the
                              file handle to set the requested position on.
  @param[in] Position        The byte position from the start of the file to set.

  @retval EFI_SUCCESS      The position was set.
  @retval EFI_UNSUPPORTED  The seek request for nonzero is not valid on open
                           directories.
  @retval EFI_DEVICE_ERROR An attempt was made to set the position of a deleted file.

**/
EFI_STATUS
EFIAPI
Ext4SetPosition (
  IN EFI_FILE_PROTOCOL  *This,
  IN UINT64             Position
  )
{
  EXT4_FILE  *File;

  File = EXT4_FILE_FROM_THIS (This);

  // Only seeks to 0 (so it resets the ReadDir operation) are allowed
  if (Ext4FileIsDir (File) && (Position != 0)) {
    return EFI_UNSUPPORTED;
  }

  // -1 (0xffffff.......) seeks to the end of the file
  if (Position == (UINT64)-1) {
    Position = EXT4_INODE_SIZE (File->Inode);
  }

  File->Position = Position;

  return EFI_SUCCESS;
}

/**
   Retrieves information about the file and stores it in the EFI_FILE_INFO format.

   @param[in]      File           Pointer to an opened file.
   @param[out]     Info           Pointer to a EFI_FILE_INFO.
   @param[in out]  BufferSize     Pointer to the buffer size

   @return Status of the file information request.
**/
EFI_STATUS
Ext4GetFileInfo (
  IN EXT4_FILE       *File,
  OUT EFI_FILE_INFO  *Info,
  IN OUT UINTN       *BufferSize
  )
{
  UINTN         FileNameLen;
  UINTN         FileNameSize;
  UINTN         NeededLength;
  CONST CHAR16  *FileName;

  if (File->InodeNum == EXT4_ROOT_INODE_NR) {
    // Root inode gets a filename of "", regardless of how it was opened.
    FileName = L"";
  } else {
    FileName = File->Dentry->Name;
  }

  FileNameLen  = StrLen (FileName);
  FileNameSize = StrSize (FileName);

  NeededLength = SIZE_OF_EFI_FILE_INFO + FileNameSize;

  if (*BufferSize < NeededLength) {
    *BufferSize = NeededLength;
    return EFI_BUFFER_TOO_SMALL;
  }

  Info->FileSize     = EXT4_INODE_SIZE (File->Inode);
  Info->PhysicalSize = Ext4FilePhysicalSpace (File);
  Ext4FileATime (File, &Info->LastAccessTime);
  Ext4FileMTime (File, &Info->ModificationTime);
  Ext4FileCreateTime (File, &Info->LastAccessTime);
  Info->Attribute = 0;
  Info->Size      = NeededLength;

  if (Ext4FileIsDir (File)) {
    Info->Attribute |= EFI_FILE_DIRECTORY;
  }

  *BufferSize = NeededLength;

  return StrCpyS (Info->FileName, FileNameLen + 1, FileName);
}

/**
   Retrieves the volume name.

   @param[in]      Part           Pointer to the opened partition.
   @param[out]     Info           Pointer to a CHAR16*.
   @param[out]     BufferSize     Pointer to a UINTN, where the string length
                                  of the name will be put.

   @return Status of the volume name request.
**/
EFI_STATUS
Ext4GetVolumeName (
  IN EXT4_PARTITION  *Partition,
  OUT CHAR16         **OutVolName,
  OUT UINTN          *VolNameLen
  )
{
  CHAR8       TempVolName[16 + 1];
  CHAR16      *VolumeName;
  UINTN       VolNameLength;
  EFI_STATUS  Status;

  VolNameLength = 0;
  VolumeName    = NULL;

  // s_volume_name is only valid on dynamic revision; old filesystems don't support this
  if (Partition->SuperBlock.s_rev_level == EXT4_DYNAMIC_REV) {
    CopyMem (TempVolName, Partition->SuperBlock.s_volume_name, 16);
    TempVolName[16] = '\0';

    Status = UTF8StrToUCS2 (TempVolName, &VolumeName);

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

    VolNameLength = StrLen (VolumeName);
  } else {
    VolumeName = AllocateZeroPool (sizeof (CHAR16));
    if (VolumeName == NULL) {
      return EFI_OUT_OF_RESOURCES;
    }

    VolNameLength = 0;
  }

  *OutVolName = VolumeName;
  *VolNameLen = VolNameLength;

  return EFI_SUCCESS;
}

/**
   Retrieves information about the filesystem and stores it in the EFI_FILE_SYSTEM_INFO format.

   @param[in]      Part           Pointer to the opened partition.
   @param[out]     Info           Pointer to a EFI_FILE_SYSTEM_INFO.
   @param[in out]  BufferSize     Pointer to the buffer size

   @return Status of the file information request.
**/
STATIC
EFI_STATUS
Ext4GetFilesystemInfo (
  IN EXT4_PARTITION         *Part,
  OUT EFI_FILE_SYSTEM_INFO  *Info,
  IN OUT UINTN              *BufferSize
  )
{
  // Length of s_volume_name + null terminator
  EFI_STATUS     Status;
  UINTN          NeededLength;
  EXT4_BLOCK_NR  TotalBlocks;
  EXT4_BLOCK_NR  FreeBlocks;
  CHAR16         *VolumeName;
  UINTN          VolNameLength;

  Status = Ext4GetVolumeName (Part, &VolumeName, &VolNameLength);

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

  NeededLength = SIZE_OF_EFI_FILE_SYSTEM_INFO;

  NeededLength += StrSize (VolumeName);

  if (*BufferSize < NeededLength) {
    *BufferSize = NeededLength;

    FreePool (VolumeName);

    return EFI_BUFFER_TOO_SMALL;
  }

  TotalBlocks = Part->NumberBlocks;

  FreeBlocks = EXT4_BLOCK_NR_FROM_HALFS (
                 Part,
                 Part->SuperBlock.s_free_blocks_count,
                 Part->SuperBlock.s_free_blocks_count_hi
                 );

  Info->BlockSize  = Part->BlockSize;
  Info->Size       = NeededLength;
  Info->ReadOnly   = Part->ReadOnly;
  Info->VolumeSize = MultU64x32 (TotalBlocks, Part->BlockSize);
  Info->FreeSpace  = MultU64x32 (FreeBlocks, Part->BlockSize);

  Status = StrCpyS (Info->VolumeLabel, VolNameLength + 1, VolumeName);

  ASSERT_EFI_ERROR (Status);

  FreePool (VolumeName);

  *BufferSize = NeededLength;

  return EFI_SUCCESS;
}

/**
   Retrieves the volume label and stores it in the EFI_FILE_SYSTEM_VOLUME_LABEL format.

   @param[in]      Part           Pointer to the opened partition.
   @param[out]     Info           Pointer to a EFI_FILE_SYSTEM_VOLUME_LABEL.
   @param[in out]  BufferSize     Pointer to the buffer size

   @return Status of the file information request.
**/
STATIC
EFI_STATUS
Ext4GetVolumeLabelInfo (
  IN EXT4_PARTITION                 *Part,
  OUT EFI_FILE_SYSTEM_VOLUME_LABEL  *Info,
  IN OUT UINTN                      *BufferSize
  )
{
  // Length of s_volume_name + null terminator
  CHAR16      *VolumeName;
  UINTN       VolNameLength;
  EFI_STATUS  Status;
  UINTN       NeededLength;

  Status = Ext4GetVolumeName (Part, &VolumeName, &VolNameLength);

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

  NeededLength = (VolNameLength + 1) * sizeof (CHAR16);

  if (NeededLength > *BufferSize) {
    *BufferSize = NeededLength;

    FreePool (VolumeName);

    return EFI_BUFFER_TOO_SMALL;
  }

  Status = StrCpyS (Info->VolumeLabel, VolNameLength + 1, VolumeName);

  ASSERT_EFI_ERROR (Status);

  FreePool (VolumeName);

  *BufferSize = NeededLength;

  return EFI_SUCCESS;
}

/**
  Returns information about a file.

  @param[in]      This            A pointer to the EFI_FILE_PROTOCOL instance that is the file
                                  handle the requested information is for.
  @param[in]      InformationType The type identifier for the information being requested.
  @param[in out]  BufferSize      On input, the size of Buffer. On output, the amount of data
                                  returned in Buffer. In both cases, the size is measured in bytes.
  @param[out]     Buffer          A pointer to the data buffer to return. The buffer's type is
                                  indicated by InformationType.

  @retval EFI_SUCCESS          The information was returned.
  @retval EFI_UNSUPPORTED      The InformationType is not known.
  @retval EFI_NO_MEDIA         The device has no medium.
  @retval EFI_DEVICE_ERROR     The device reported an error.
  @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted.
  @retval EFI_BUFFER_TOO_SMALL The BufferSize is too small to read the current directory entry.
                               BufferSize has been updated with the size needed to complete
                               the request.
**/
EFI_STATUS
EFIAPI
Ext4GetInfo (
  IN EFI_FILE_PROTOCOL  *This,
  IN EFI_GUID           *InformationType,
  IN OUT UINTN          *BufferSize,
  OUT VOID              *Buffer
  )
{
  EXT4_FILE       *File;
  EXT4_PARTITION  *Partition;

  File      = EXT4_FILE_FROM_THIS (This);
  Partition = File->Partition;

  if (CompareGuid (InformationType, &gEfiFileInfoGuid)) {
    return Ext4GetFileInfo (File, Buffer, BufferSize);
  }

  if (CompareGuid (InformationType, &gEfiFileSystemInfoGuid)) {
    return Ext4GetFilesystemInfo (Partition, Buffer, BufferSize);
  }

  if (CompareGuid (InformationType, &gEfiFileSystemVolumeLabelInfoIdGuid)) {
    return Ext4GetVolumeLabelInfo (Partition, Buffer, BufferSize);
  }

  return EFI_UNSUPPORTED;
}

/**
   Duplicates a file structure.

   @param[in]        Original    Pointer to the original file.

   @return Pointer to the new file structure.
**/
STATIC
EXT4_FILE *
Ext4DuplicateFile (
  IN CONST EXT4_FILE  *Original
  )
{
  EXT4_PARTITION  *Partition;
  EXT4_FILE       *File;
  EFI_STATUS      Status;

  Partition = Original->Partition;
  File      = AllocateZeroPool (sizeof (EXT4_FILE));

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

  File->Inode = Ext4AllocateInode (Partition);
  if (File->Inode == NULL) {
    FreePool (File);
    return NULL;
  }

  CopyMem (File->Inode, Original->Inode, Partition->InodeSize);

  File->Position = 0;
  Ext4SetupFile (File, Partition);
  File->InodeNum = Original->InodeNum;
  File->OpenMode = 0; // Will be filled by other code

  Status = Ext4InitExtentsMap (File);
  if (EFI_ERROR (Status)) {
    FreePool (File->Inode);
    FreePool (File);
    return NULL;
  }

  File->Dentry = Original->Dentry;

  Ext4RefDentry (File->Dentry);

  InsertTailList (&Partition->OpenFiles, &File->OpenFilesListNode);

  return File;
}

/**
  Sets information about a file.

  @param[in]  This            A pointer to the EFI_FILE_PROTOCOL instance that is the file
                              handle the information is for.
  @param[in]  InformationType The type identifier for the information being set.
  @param[in]  BufferSize      The size, in bytes, of Buffer.
  @param[in]  Buffer          A pointer to the data buffer to write. The buffer's type is
                              indicated by InformationType.

  @retval EFI_SUCCESS          The information was set.
  @retval EFI_UNSUPPORTED      The InformationType is not known.
  @retval EFI_NO_MEDIA         The device has no medium.
  @retval EFI_DEVICE_ERROR     The device reported an error.
  @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted.
  @retval EFI_WRITE_PROTECTED  InformationType is EFI_FILE_INFO_ID and the media is
                               read-only.
  @retval EFI_WRITE_PROTECTED  InformationType is EFI_FILE_PROTOCOL_SYSTEM_INFO_ID
                               and the media is read only.
  @retval EFI_WRITE_PROTECTED  InformationType is EFI_FILE_SYSTEM_VOLUME_LABEL_ID
                               and the media is read-only.
  @retval EFI_ACCESS_DENIED    An attempt is made to change the name of a file to a
                               file that is already present.
  @retval EFI_ACCESS_DENIED    An attempt is being made to change the EFI_FILE_DIRECTORY
                               Attribute.
  @retval EFI_ACCESS_DENIED    An attempt is being made to change the size of a directory.
  @retval EFI_ACCESS_DENIED    InformationType is EFI_FILE_INFO_ID and the file was opened
                               read-only and an attempt is being made to modify a field
                               other than Attribute.
  @retval EFI_VOLUME_FULL      The volume is full.
  @retval EFI_BAD_BUFFER_SIZE  BufferSize is smaller than the size of the type indicated
                               by InformationType.

**/
EFI_STATUS
EFIAPI
Ext4SetInfo (
  IN EFI_FILE_PROTOCOL  *This,
  IN EFI_GUID           *InformationType,
  IN UINTN              BufferSize,
  IN VOID               *Buffer
  )
{
  EXT4_FILE       *File;
  EXT4_PARTITION  *Partition;

  File      = EXT4_FILE_FROM_THIS (This);
  Partition = File->Partition;

  if (Partition->ReadOnly) {
    return EFI_WRITE_PROTECTED;
  }

  // There's no write support just yet.
  return EFI_UNSUPPORTED;
}
