/** @file
  EFI_FILE_PROTOCOL.Open() member function for the Virtio Filesystem driver.

  Copyright (C) 2020, Red Hat, Inc.

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

#include <Library/BaseLib.h>             // AsciiStrCmp()
#include <Library/MemoryAllocationLib.h> // AllocatePool()

#include "VirtioFsDxe.h"

/**
  Open the root directory, possibly for writing.

  @param[in,out] VirtioFs    The Virtio Filesystem device whose root directory
                             should be opened.

  @param[out] NewHandle      The new EFI_FILE_PROTOCOL instance through which
                             the root directory can be accessed.

  @param[in] OpenForWriting  TRUE if the root directory should be opened for
                             read-write access. FALSE if the root directory
                             should be opened for read-only access. Opening the
                             root directory for read-write access is useful for
                             calling EFI_FILE_PROTOCOL.Flush() or
                             EFI_FILE_PROTOCOL.SetInfo() later, for syncing or
                             touching the root directory, respectively.

  @retval EFI_SUCCESS        The root directory has been opened successfully.

  @retval EFI_ACCESS_DENIED  OpenForWriting is TRUE, but the root directory is
                             marked as read-only.

  @return                    Error codes propagated from underlying functions.
**/
STATIC
EFI_STATUS
OpenRootDirectory (
  IN OUT VIRTIO_FS       *VirtioFs,
  OUT EFI_FILE_PROTOCOL  **NewHandle,
  IN     BOOLEAN         OpenForWriting
  )
{
  EFI_STATUS      Status;
  VIRTIO_FS_FILE  *NewVirtioFsFile;

  //
  // VirtioFsOpenVolume() opens the root directory for read-only access. If the
  // current request is to open the root directory for read-write access, so
  // that EFI_FILE_PROTOCOL.Flush() or EFI_FILE_PROTOCOL.SetInfo()+timestamps
  // can be used on the root directory later, then we have to check for write
  // permission first.
  //
  if (OpenForWriting) {
    VIRTIO_FS_FUSE_ATTRIBUTES_RESPONSE  FuseAttr;
    EFI_FILE_INFO                       FileInfo;

    Status = VirtioFsFuseGetAttr (
               VirtioFs,
               VIRTIO_FS_FUSE_ROOT_DIR_NODE_ID,
               &FuseAttr
               );
    if (EFI_ERROR (Status)) {
      return Status;
    }

    Status = VirtioFsFuseAttrToEfiFileInfo (&FuseAttr, &FileInfo);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    if ((FileInfo.Attribute & EFI_FILE_READ_ONLY) != 0) {
      return EFI_ACCESS_DENIED;
    }
  }

  Status = VirtioFsOpenVolume (&VirtioFs->SimpleFs, NewHandle);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  NewVirtioFsFile                   = VIRTIO_FS_FILE_FROM_SIMPLE_FILE (*NewHandle);
  NewVirtioFsFile->IsOpenForWriting = OpenForWriting;
  return EFI_SUCCESS;
}

/**
  Open an existent regular file or non-root directory.

  @param[in,out] VirtioFs      The Virtio Filesystem device on which the
                               regular file or directory should be opened.

  @param[in] DirNodeId         The inode number of the immediate parent
                               directory of the regular file or directory to
                               open.

  @param[in] Name              The single-component filename of the regular
                               file or directory to open, under the immediate
                               parent directory identified by DirNodeId.

  @param[in] OpenForWriting    TRUE if the regular file or directory should be
                               opened for read-write access. FALSE if the
                               regular file or directory should be opened for
                               read-only access. Opening a directory for
                               read-write access is useful for deleting,
                               renaming, syncing or touching the directory
                               later.

  @param[out] NodeId           The inode number of the regular file or
                               directory, returned by the Virtio Filesystem
                               device.

  @param[out] FuseHandle       The open handle to the regular file or
                               directory, returned by the Virtio Filesystem
                               device.

  @param[out] NodeIsDirectory  Set to TRUE on output if Name was found to refer
                               to a directory. Set to FALSE if Name was found
                               to refer to a regular file.

  @retval EFI_SUCCESS        The regular file or directory has been looked up
                             and opened successfully.

  @retval EFI_ACCESS_DENIED  OpenForWriting is TRUE, but the regular file or
                             directory is marked read-only.

  @retval EFI_NOT_FOUND      A directory entry called Name was not found in the
                             directory identified by DirNodeId. (EFI_NOT_FOUND
                             is not returned for any other condition.)

  @return                    Errors propagated from underlying functions. If
                             the error code to propagate were EFI_NOT_FOUND, it
                             is remapped to EFI_DEVICE_ERROR.
**/
STATIC
EFI_STATUS
OpenExistentFileOrDirectory (
  IN OUT VIRTIO_FS  *VirtioFs,
  IN     UINT64     DirNodeId,
  IN     CHAR8      *Name,
  IN     BOOLEAN    OpenForWriting,
  OUT UINT64        *NodeId,
  OUT UINT64        *FuseHandle,
  OUT BOOLEAN       *NodeIsDirectory
  )
{
  EFI_STATUS                          Status;
  UINT64                              ResolvedNodeId;
  VIRTIO_FS_FUSE_ATTRIBUTES_RESPONSE  FuseAttr;
  EFI_FILE_INFO                       FileInfo;
  BOOLEAN                             IsDirectory;
  UINT64                              NewFuseHandle;

  Status = VirtioFsFuseLookup (
             VirtioFs,
             DirNodeId,
             Name,
             &ResolvedNodeId,
             &FuseAttr
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = VirtioFsFuseAttrToEfiFileInfo (&FuseAttr, &FileInfo);
  if (EFI_ERROR (Status)) {
    goto ForgetResolvedNodeId;
  }

  if (OpenForWriting && ((FileInfo.Attribute & EFI_FILE_READ_ONLY) != 0)) {
    Status = EFI_ACCESS_DENIED;
    goto ForgetResolvedNodeId;
  }

  IsDirectory = (BOOLEAN)((FileInfo.Attribute & EFI_FILE_DIRECTORY) != 0);
  if (IsDirectory) {
    //
    // If OpenForWriting is TRUE here, that's not passed to
    // VirtioFsFuseOpenDir(); it does not affect the FUSE_OPENDIR request we
    // send. OpenForWriting=TRUE will only permit attempts to delete, rename,
    // flush (sync), and touch the directory.
    //
    Status = VirtioFsFuseOpenDir (VirtioFs, ResolvedNodeId, &NewFuseHandle);
  } else {
    Status = VirtioFsFuseOpen (
               VirtioFs,
               ResolvedNodeId,
               OpenForWriting,
               &NewFuseHandle
               );
  }

  if (EFI_ERROR (Status)) {
    goto ForgetResolvedNodeId;
  }

  *NodeId          = ResolvedNodeId;
  *FuseHandle      = NewFuseHandle;
  *NodeIsDirectory = IsDirectory;
  return EFI_SUCCESS;

ForgetResolvedNodeId:
  VirtioFsFuseForget (VirtioFs, ResolvedNodeId);
  return (Status == EFI_NOT_FOUND) ? EFI_DEVICE_ERROR : Status;
}

/**
  Create a directory.

  @param[in,out] VirtioFs  The Virtio Filesystem device on which the directory
                           should be created.

  @param[in] DirNodeId     The inode number of the immediate parent directory
                           of the directory to create.

  @param[in] Name          The single-component filename of the directory to
                           create, under the immediate parent directory
                           identified by DirNodeId.

  @param[out] NodeId       The inode number of the directory created, returned
                           by the Virtio Filesystem device.

  @param[out] FuseHandle   The open handle to the directory created, returned
                           by the Virtio Filesystem device.

  @retval EFI_SUCCESS  The directory has been created successfully.

  @return              Errors propagated from underlying functions.
**/
STATIC
EFI_STATUS
CreateDirectory (
  IN OUT VIRTIO_FS  *VirtioFs,
  IN     UINT64     DirNodeId,
  IN     CHAR8      *Name,
  OUT UINT64        *NodeId,
  OUT UINT64        *FuseHandle
  )
{
  EFI_STATUS  Status;
  UINT64      NewChildDirNodeId;
  UINT64      NewFuseHandle;

  Status = VirtioFsFuseMkDir (VirtioFs, DirNodeId, Name, &NewChildDirNodeId);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = VirtioFsFuseOpenDir (VirtioFs, NewChildDirNodeId, &NewFuseHandle);
  if (EFI_ERROR (Status)) {
    goto RemoveNewChildDir;
  }

  *NodeId     = NewChildDirNodeId;
  *FuseHandle = NewFuseHandle;
  return EFI_SUCCESS;

RemoveNewChildDir:
  VirtioFsFuseRemoveFileOrDir (VirtioFs, DirNodeId, Name, TRUE /* IsDir */);
  VirtioFsFuseForget (VirtioFs, NewChildDirNodeId);
  return Status;
}

/**
  Create a regular file.

  @param[in,out] VirtioFs  The Virtio Filesystem device on which the regular
                           file should be created.

  @param[in] DirNodeId     The inode number of the immediate parent directory
                           of the regular file to create.

  @param[in] Name          The single-component filename of the regular file to
                           create, under the immediate parent directory
                           identified by DirNodeId.

  @param[out] NodeId       The inode number of the regular file created,
                           returned by the Virtio Filesystem device.

  @param[out] FuseHandle   The open handle to the regular file created,
                           returned by the Virtio Filesystem device.

  @retval EFI_SUCCESS  The regular file has been created successfully.

  @return              Errors propagated from underlying functions.
**/
STATIC
EFI_STATUS
CreateRegularFile (
  IN OUT VIRTIO_FS  *VirtioFs,
  IN     UINT64     DirNodeId,
  IN     CHAR8      *Name,
  OUT UINT64        *NodeId,
  OUT UINT64        *FuseHandle
  )
{
  return VirtioFsFuseOpenOrCreate (
           VirtioFs,
           DirNodeId,
           Name,
           NodeId,
           FuseHandle
           );
}

EFI_STATUS
EFIAPI
VirtioFsSimpleFileOpen (
  IN     EFI_FILE_PROTOCOL  *This,
  OUT EFI_FILE_PROTOCOL     **NewHandle,
  IN     CHAR16             *FileName,
  IN     UINT64             OpenMode,
  IN     UINT64             Attributes
  )
{
  VIRTIO_FS_FILE  *VirtioFsFile;
  VIRTIO_FS       *VirtioFs;
  BOOLEAN         OpenForWriting;
  BOOLEAN         PermitCreation;
  BOOLEAN         CreateDirectoryIfCreating;
  VIRTIO_FS_FILE  *NewVirtioFsFile;
  EFI_STATUS      Status;
  CHAR8           *NewCanonicalPath;
  BOOLEAN         RootEscape;
  UINT64          DirNodeId;
  CHAR8           *LastComponent;
  UINT64          NewNodeId;
  UINT64          NewFuseHandle;
  BOOLEAN         NewNodeIsDirectory;

  VirtioFsFile = VIRTIO_FS_FILE_FROM_SIMPLE_FILE (This);
  VirtioFs     = VirtioFsFile->OwnerFs;

  //
  // Validate OpenMode.
  //
  switch (OpenMode) {
    case EFI_FILE_MODE_READ:
      OpenForWriting = FALSE;
      PermitCreation = FALSE;
      break;
    case EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE:
      OpenForWriting = TRUE;
      PermitCreation = FALSE;
      break;
    case EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE:
      OpenForWriting = TRUE;
      PermitCreation = TRUE;
      break;
    default:
      return EFI_INVALID_PARAMETER;
  }

  //
  // Set CreateDirectoryIfCreating to suppress incorrect compiler/analyzer
  // warnings.
  //
  CreateDirectoryIfCreating = FALSE;

  //
  // Validate the Attributes requested for the case when the file ends up being
  // created, provided creation is permitted.
  //
  if (PermitCreation) {
    if ((Attributes & ~EFI_FILE_VALID_ATTR) != 0) {
      //
      // Unknown attribute requested.
      //
      return EFI_INVALID_PARAMETER;
    }

    ASSERT (OpenForWriting);
    if ((Attributes & EFI_FILE_READ_ONLY) != 0) {
      DEBUG ((
        DEBUG_ERROR,
        ("%a: Label=\"%s\" CanonicalPathname=\"%a\" FileName=\"%s\" "
         "OpenMode=0x%Lx Attributes=0x%Lx: nonsensical request to possibly "
         "create a file marked read-only, for read-write access\n"),
        __func__,
        VirtioFs->Label,
        VirtioFsFile->CanonicalPathname,
        FileName,
        OpenMode,
        Attributes
        ));
      return EFI_INVALID_PARAMETER;
    }

    CreateDirectoryIfCreating = (BOOLEAN)((Attributes &
                                           EFI_FILE_DIRECTORY) != 0);
  }

  //
  // Referring to a file relative to a regular file makes no sense (or at least
  // it cannot be implemented consistently with how a file is referred to
  // relative to a directory). See USWG Mantis ticket #2367.
  //
  if (!VirtioFsFile->IsDirectory) {
    BOOLEAN  BugCompat;

    //
    // Tolerate this bug in the caller if FileName is absolute. If FileName is
    // absolute, then VirtioFsAppendPath() below will disregard
    // VirtioFsFile->CanonicalPathname.
    //
    BugCompat = (FileName[0] == L'\\');

    DEBUG ((
      BugCompat ? DEBUG_WARN : DEBUG_ERROR,
      ("%a: Label=\"%s\" CanonicalPathname=\"%a\" FileName=\"%s\": "
       "nonsensical request to open a file or directory relative to a regular "
       "file\n"),
      __func__,
      VirtioFs->Label,
      VirtioFsFile->CanonicalPathname,
      FileName
      ));
    if (!BugCompat) {
      return EFI_INVALID_PARAMETER;
    }
  }

  //
  // Allocate the new VIRTIO_FS_FILE object.
  //
  NewVirtioFsFile = AllocatePool (sizeof *NewVirtioFsFile);
  if (NewVirtioFsFile == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  //
  // Create the canonical pathname at which the desired file is expected to
  // exist.
  //
  Status = VirtioFsAppendPath (
             VirtioFsFile->CanonicalPathname,
             FileName,
             &NewCanonicalPath,
             &RootEscape
             );
  if (EFI_ERROR (Status)) {
    goto FreeNewVirtioFsFile;
  }

  if (RootEscape) {
    Status = EFI_ACCESS_DENIED;
    goto FreeNewCanonicalPath;
  }

  //
  // If the desired file is the root directory, just open the volume one more
  // time, without looking up anything.
  //
  if (AsciiStrCmp (NewCanonicalPath, "/") == 0) {
    FreePool (NewCanonicalPath);
    FreePool (NewVirtioFsFile);
    return OpenRootDirectory (VirtioFs, NewHandle, OpenForWriting);
  }

  //
  // Split the new canonical pathname into most specific parent directory
  // (given by DirNodeId) and last pathname component (i.e., immediate child
  // within that parent directory).
  //
  Status = VirtioFsLookupMostSpecificParentDir (
             VirtioFs,
             NewCanonicalPath,
             &DirNodeId,
             &LastComponent
             );
  if (EFI_ERROR (Status)) {
    goto FreeNewCanonicalPath;
  }

  //
  // Set NewNodeIsDirectory to suppress incorrect compiler/analyzer warnings.
  //
  NewNodeIsDirectory = FALSE;

  //
  // Try to open LastComponent directly under DirNodeId, as an existent regular
  // file or directory.
  //
  Status = OpenExistentFileOrDirectory (
             VirtioFs,
             DirNodeId,
             LastComponent,
             OpenForWriting,
             &NewNodeId,
             &NewFuseHandle,
             &NewNodeIsDirectory
             );
  //
  // If LastComponent could not be found under DirNodeId, but the request
  // allows us to create a new entry, attempt creating the requested regular
  // file or directory.
  //
  if ((Status == EFI_NOT_FOUND) && PermitCreation) {
    ASSERT (OpenForWriting);
    if (CreateDirectoryIfCreating) {
      Status = CreateDirectory (
                 VirtioFs,
                 DirNodeId,
                 LastComponent,
                 &NewNodeId,
                 &NewFuseHandle
                 );
    } else {
      Status = CreateRegularFile (
                 VirtioFs,
                 DirNodeId,
                 LastComponent,
                 &NewNodeId,
                 &NewFuseHandle
                 );
    }

    NewNodeIsDirectory = CreateDirectoryIfCreating;
  }

  //
  // Regardless of the branch taken, we're done with DirNodeId.
  //
  if (DirNodeId != VIRTIO_FS_FUSE_ROOT_DIR_NODE_ID) {
    VirtioFsFuseForget (VirtioFs, DirNodeId);
  }

  if (EFI_ERROR (Status)) {
    goto FreeNewCanonicalPath;
  }

  //
  // Populate the new VIRTIO_FS_FILE object.
  //
  NewVirtioFsFile->Signature              = VIRTIO_FS_FILE_SIG;
  NewVirtioFsFile->SimpleFile.Revision    = EFI_FILE_PROTOCOL_REVISION;
  NewVirtioFsFile->SimpleFile.Open        = VirtioFsSimpleFileOpen;
  NewVirtioFsFile->SimpleFile.Close       = VirtioFsSimpleFileClose;
  NewVirtioFsFile->SimpleFile.Delete      = VirtioFsSimpleFileDelete;
  NewVirtioFsFile->SimpleFile.Read        = VirtioFsSimpleFileRead;
  NewVirtioFsFile->SimpleFile.Write       = VirtioFsSimpleFileWrite;
  NewVirtioFsFile->SimpleFile.GetPosition = VirtioFsSimpleFileGetPosition;
  NewVirtioFsFile->SimpleFile.SetPosition = VirtioFsSimpleFileSetPosition;
  NewVirtioFsFile->SimpleFile.GetInfo     = VirtioFsSimpleFileGetInfo;
  NewVirtioFsFile->SimpleFile.SetInfo     = VirtioFsSimpleFileSetInfo;
  NewVirtioFsFile->SimpleFile.Flush       = VirtioFsSimpleFileFlush;
  NewVirtioFsFile->IsDirectory            = NewNodeIsDirectory;
  NewVirtioFsFile->IsOpenForWriting       = OpenForWriting;
  NewVirtioFsFile->OwnerFs                = VirtioFs;
  NewVirtioFsFile->CanonicalPathname      = NewCanonicalPath;
  NewVirtioFsFile->FilePosition           = 0;
  NewVirtioFsFile->NodeId                 = NewNodeId;
  NewVirtioFsFile->FuseHandle             = NewFuseHandle;
  NewVirtioFsFile->FileInfoArray          = NULL;
  NewVirtioFsFile->SingleFileInfoSize     = 0;
  NewVirtioFsFile->NumFileInfo            = 0;
  NewVirtioFsFile->NextFileInfo           = 0;

  //
  // One more file is now open for the filesystem.
  //
  InsertTailList (&VirtioFs->OpenFiles, &NewVirtioFsFile->OpenFilesEntry);

  *NewHandle = &NewVirtioFsFile->SimpleFile;
  return EFI_SUCCESS;

FreeNewCanonicalPath:
  FreePool (NewCanonicalPath);

FreeNewVirtioFsFile:
  FreePool (NewVirtioFsFile);

  return Status;
}
