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

  Copyright (C) 2020, Red Hat, Inc.

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

#include <Guid/FileSystemInfo.h>            // gEfiFileSystemInfoGuid
#include <Guid/FileSystemVolumeLabelInfo.h> // gEfiFileSystemVolumeLabelInfo...
#include <Library/BaseLib.h>                // StrCmp()
#include <Library/BaseMemoryLib.h>          // CompareGuid()
#include <Library/MemoryAllocationLib.h>    // FreePool()

#include "VirtioFsDxe.h"

/**
  Validate a buffer that the EFI_FILE_PROTOCOL.SetInfo() caller passes in for a
  particular InformationType GUID.

  The structure to be validated is supposed to end with a variable-length,
  NUL-terminated CHAR16 Name string.

  @param[in] SizeByProtocolCaller  The BufferSize parameter as provided by the
                                   EFI_FILE_PROTOCOL.SetInfo() caller.

  @param[in] MinimumStructSize     The minimum structure size that is required
                                   for the given InformationType GUID,
                                   including a single CHAR16 element from the
                                   trailing Name field.

  @param[in] IsSizeByInfoPresent   TRUE if and only if the expected structure
                                   starts with a UINT64 Size field that reports
                                   the actual structure size.

  @param[in] Buffer                The Buffer parameter as provided by the
                                   EFI_FILE_PROTOCOL.SetInfo() caller.

  @retval EFI_SUCCESS            Validation successful, Buffer is well-formed.

  @retval EFI_BAD_BUFFER_SIZE    The EFI_FILE_PROTOCOL.SetInfo()
                                 caller provided a BufferSize that is smaller
                                 than the minimum structure size required for
                                 the given InformationType GUID.

  @retval EFI_INVALID_PARAMETER  IsSizeByInfoPresent is TRUE, and the leading
                                 UINT64 Size field does not match the
                                 EFI_FILE_PROTOCOL.SetInfo() caller-provided
                                 BufferSize.

  @retval EFI_INVALID_PARAMETER  The trailing Name field does not consist of a
                                 whole multiple of CHAR16 elements.

  @retval EFI_INVALID_PARAMETER  The trailing Name field is not NUL-terminated.
**/
STATIC
EFI_STATUS
ValidateInfoStructure (
  IN UINTN   SizeByProtocolCaller,
  IN UINTN   MinimumStructSize,
  IN BOOLEAN IsSizeByInfoPresent,
  IN VOID    *Buffer
  )
{
  UINTN  NameFieldByteOffset;
  UINTN  NameFieldBytes;
  UINTN  NameFieldChar16s;
  CHAR16 *NameField;

  //
  // Make sure the internal function asking for validation passes in sane
  // values.
  //
  ASSERT (MinimumStructSize >= sizeof (CHAR16));
  NameFieldByteOffset = MinimumStructSize - sizeof (CHAR16);

  if (IsSizeByInfoPresent) {
    ASSERT (MinimumStructSize >= sizeof (UINT64) + sizeof (CHAR16));
    ASSERT (NameFieldByteOffset >= sizeof (UINT64));
  }

  //
  // Check whether the protocol caller provided enough bytes for the minimum
  // size of this info structure.
  //
  if (SizeByProtocolCaller < MinimumStructSize) {
    return EFI_BAD_BUFFER_SIZE;
  }

  //
  // If the info structure starts with a UINT64 Size field, check if that
  // agrees with the protocol caller-provided size.
  //
  if (IsSizeByInfoPresent) {
    UINT64 *SizeByInfo;

    SizeByInfo = Buffer;
    if (*SizeByInfo != SizeByProtocolCaller) {
      return EFI_INVALID_PARAMETER;
    }
  }

  //
  // The CHAR16 Name field at the end of the structure must have an even number
  // of bytes.
  //
  // The subtraction below cannot underflow, and yields at least
  // sizeof(CHAR16).
  //
  ASSERT (SizeByProtocolCaller >= NameFieldByteOffset);
  NameFieldBytes = SizeByProtocolCaller - NameFieldByteOffset;
  ASSERT (NameFieldBytes >= sizeof (CHAR16));
  if (NameFieldBytes % sizeof (CHAR16) != 0) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // The CHAR16 Name field at the end of the structure must be NUL-terminated.
  //
  NameFieldChar16s = NameFieldBytes / sizeof (CHAR16);
  ASSERT (NameFieldChar16s >= 1);

  NameField = (CHAR16 *)((UINT8 *)Buffer + NameFieldByteOffset);
  if (NameField[NameFieldChar16s - 1] != L'\0') {
    return EFI_INVALID_PARAMETER;
  }

  return EFI_SUCCESS;
}

/**
  Rename a VIRTIO_FS_FILE as requested in EFI_FILE_INFO.FileName.

  @param[in,out] VirtioFsFile  The VIRTIO_FS_FILE to rename.

  @param[in] NewFileName       The new file name requested by
                               EFI_FILE_PROTOCOL.SetInfo().

  @retval EFI_SUCCESS        The canonical format destination path that is
                             determined from the input value of
                             VirtioFsFile->CanonicalPathname and from
                             NewFileName is identical to the input value of
                             VirtioFsFile->CanonicalPathname. This means that
                             EFI_FILE_INFO does not constitute a rename
                             request. VirtioFsFile has not been changed.

  @retval EFI_SUCCESS        VirtioFsFile has been renamed.
                             VirtioFsFile->CanonicalPathname has assumed the
                             destination pathname in canonical format.

  @retval EFI_ACCESS_DENIED  VirtioFsFile refers to the root directory, and
                             NewFileName expresses an actual rename/move
                             request.

  @retval EFI_ACCESS_DENIED  VirtioFsFile is the (possibly indirect) parent
                             directory of at least one other VIRTIO_FS_FILE
                             that is open for the same Virtio Filesystem
                             (identified by VirtioFsFile->OwnerFs). Renaming
                             VirtioFsFile would invalidate the canonical
                             pathnames of those VIRTIO_FS_FILE instances;
                             therefore the request has been rejected.

  @retval EFI_ACCESS_DENIED  VirtioFsFile is not open for writing, but
                             NewFileName expresses an actual rename/move
                             request.

  @retval EFI_NOT_FOUND      At least one dot-dot component in NewFileName
                             attempted to escape the root directory.

  @return                    Error codes propagated from underlying functions.
**/
STATIC
EFI_STATUS
Rename (
  IN OUT VIRTIO_FS_FILE *VirtioFsFile,
  IN     CHAR16         *NewFileName
  )
{

  VIRTIO_FS  *VirtioFs;
  EFI_STATUS Status;
  CHAR8      *Destination;
  BOOLEAN    RootEscape;
  UINT64     OldParentDirNodeId;
  CHAR8      *OldLastComponent;
  UINT64     NewParentDirNodeId;
  CHAR8      *NewLastComponent;

  VirtioFs = VirtioFsFile->OwnerFs;

  //
  // The root directory cannot be renamed.
  //
  if (AsciiStrCmp (VirtioFsFile->CanonicalPathname, "/") == 0) {
    if (StrCmp (NewFileName, L"") == 0) {
      //
      // Not a rename request anyway.
      //
      return EFI_SUCCESS;
    }
    return EFI_ACCESS_DENIED;
  }

  //
  // Compose the canonical pathname for the destination.
  //
  Status = VirtioFsComposeRenameDestination (VirtioFsFile->CanonicalPathname,
             NewFileName, &Destination, &RootEscape);
  if (EFI_ERROR (Status)) {
    return Status;
  }
  if (RootEscape) {
    Status = EFI_NOT_FOUND;
    goto FreeDestination;
  }
  //
  // If the rename would leave VirtioFsFile->CanonicalPathname unchanged, then
  // EFI_FILE_PROTOCOL.SetInfo() isn't asking for a rename actually.
  //
  if (AsciiStrCmp (VirtioFsFile->CanonicalPathname, Destination) == 0) {
    Status = EFI_SUCCESS;
    goto FreeDestination;
  }
  //
  // Check if the rename would break the canonical pathnames of other
  // VIRTIO_FS_FILE instances of the same VIRTIO_FS.
  //
  if (VirtioFsFile->IsDirectory) {
    UINTN      PathLen;
    LIST_ENTRY *OpenFilesEntry;

    PathLen = AsciiStrLen (VirtioFsFile->CanonicalPathname);
    BASE_LIST_FOR_EACH (OpenFilesEntry, &VirtioFs->OpenFiles) {
      VIRTIO_FS_FILE *OtherFile;

      OtherFile = VIRTIO_FS_FILE_FROM_OPEN_FILES_ENTRY (OpenFilesEntry);
      if (OtherFile != VirtioFsFile &&
          AsciiStrnCmp (VirtioFsFile->CanonicalPathname,
            OtherFile->CanonicalPathname, PathLen) == 0 &&
          (OtherFile->CanonicalPathname[PathLen] == '\0' ||
           OtherFile->CanonicalPathname[PathLen] == '/')) {
        //
        // OtherFile refers to the same directory as VirtioFsFile, or is a
        // (possibly indirect) child of the directory referred to by
        // VirtioFsFile.
        //
        Status = EFI_ACCESS_DENIED;
        goto FreeDestination;
      }
    }
  }
  //
  // From this point on, the file needs to be open for writing.
  //
  if (!VirtioFsFile->IsOpenForWriting) {
    Status = EFI_ACCESS_DENIED;
    goto FreeDestination;
  }
  //
  // Split both source and destination canonical pathnames into (most specific
  // parent directory, last component) pairs.
  //
  Status = VirtioFsLookupMostSpecificParentDir (VirtioFs,
             VirtioFsFile->CanonicalPathname, &OldParentDirNodeId,
             &OldLastComponent);
  if (EFI_ERROR (Status)) {
    goto FreeDestination;
  }
  Status = VirtioFsLookupMostSpecificParentDir (VirtioFs, Destination,
             &NewParentDirNodeId, &NewLastComponent);
  if (EFI_ERROR (Status)) {
    goto ForgetOldParentDirNodeId;
  }
  //
  // Perform the rename. If the destination path exists, the rename will fail.
  //
  Status = VirtioFsFuseRename (VirtioFs, OldParentDirNodeId, OldLastComponent,
             NewParentDirNodeId, NewLastComponent);
  if (EFI_ERROR (Status)) {
    goto ForgetNewParentDirNodeId;
  }

  //
  // Swap in the new canonical pathname.
  //
  FreePool (VirtioFsFile->CanonicalPathname);
  VirtioFsFile->CanonicalPathname = Destination;
  Destination = NULL;
  Status = EFI_SUCCESS;

  //
  // Fall through.
  //
ForgetNewParentDirNodeId:
  if (NewParentDirNodeId != VIRTIO_FS_FUSE_ROOT_DIR_NODE_ID) {
    VirtioFsFuseForget (VirtioFs, NewParentDirNodeId);
  }

ForgetOldParentDirNodeId:
  if (OldParentDirNodeId != VIRTIO_FS_FUSE_ROOT_DIR_NODE_ID) {
    VirtioFsFuseForget (VirtioFs, OldParentDirNodeId);
  }

FreeDestination:
  if (Destination != NULL) {
    FreePool (Destination);
  }
  return Status;
}

/**
  Update the attributes of a VIRTIO_FS_FILE as requested in EFI_FILE_INFO.

  @param[in,out] VirtioFsFile  The VIRTIO_FS_FILE to update the attributes of.

  @param[in] NewFileInfo       The new attributes requested by
                               EFI_FILE_PROTOCOL.SetInfo(). NewFileInfo->Size
                               and NewFileInfo->FileName are ignored.

  @retval EFI_SUCCESS        No attributes had to be updated.

  @retval EFI_SUCCESS        The required set of attribute updates has been
                             determined and performed successfully.

  @retval EFI_ACCESS_DENIED  NewFileInfo requests an update to a property
                             different from the EFI_FILE_READ_ONLY bit in the
                             Attribute field, but VirtioFsFile is not open for
                             writing.

  @return                    Error codes propagated from underlying functions.
**/
STATIC
EFI_STATUS
UpdateAttributes (
  IN OUT VIRTIO_FS_FILE *VirtioFsFile,
  IN     EFI_FILE_INFO  *NewFileInfo
  )
{
  VIRTIO_FS                          *VirtioFs;
  EFI_STATUS                         Status;
  VIRTIO_FS_FUSE_ATTRIBUTES_RESPONSE FuseAttr;
  EFI_FILE_INFO                      FileInfo;
  BOOLEAN                            UpdateFileSize;
  UINT64                             FileSize;
  BOOLEAN                            UpdateAtime;
  BOOLEAN                            UpdateMtime;
  UINT64                             Atime;
  UINT64                             Mtime;
  BOOLEAN                            UpdateMode;
  UINT32                             Mode;

  VirtioFs = VirtioFsFile->OwnerFs;

  //
  // Fetch the current attributes first, so we can build the difference between
  // them and NewFileInfo.
  //
  Status = VirtioFsFuseGetAttr (VirtioFs, VirtioFsFile->NodeId, &FuseAttr);
  if (EFI_ERROR (Status)) {
    return Status;
  }
  Status = VirtioFsFuseAttrToEfiFileInfo (&FuseAttr, &FileInfo);
  if (EFI_ERROR (Status)) {
    return Status;
  }
  //
  // Collect the updates.
  //
  if (VirtioFsFile->IsDirectory) {
    UpdateFileSize = FALSE;
  } else {
    VirtioFsGetFuseSizeUpdate (&FileInfo, NewFileInfo, &UpdateFileSize,
      &FileSize);
  }

  Status = VirtioFsGetFuseTimeUpdates (&FileInfo, NewFileInfo, &UpdateAtime,
             &UpdateMtime, &Atime, &Mtime);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = VirtioFsGetFuseModeUpdate (&FileInfo, NewFileInfo, &UpdateMode,
             &Mode);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // If no attribute updates are necessary, we're done.
  //
  if (!UpdateFileSize && !UpdateAtime && !UpdateMtime && !UpdateMode) {
    return EFI_SUCCESS;
  }
  //
  // If the file is not open for writing, then only Mode may be updated (for
  // toggling EFI_FILE_READ_ONLY).
  //
  if (!VirtioFsFile->IsOpenForWriting &&
      (UpdateFileSize || UpdateAtime || UpdateMtime)) {
    return EFI_ACCESS_DENIED;
  }
  //
  // Send the FUSE_SETATTR request now.
  //
  Status = VirtioFsFuseSetAttr (
             VirtioFs,
             VirtioFsFile->NodeId,
             UpdateFileSize ? &FileSize : NULL,
             UpdateAtime    ? &Atime    : NULL,
             UpdateMtime    ? &Mtime    : NULL,
             UpdateMode     ? &Mode     : NULL
             );
  return Status;
}

/**
  Process an EFI_FILE_INFO setting request.
**/
STATIC
EFI_STATUS
SetFileInfo (
  IN EFI_FILE_PROTOCOL *This,
  IN UINTN             BufferSize,
  IN VOID              *Buffer
  )
{
  VIRTIO_FS_FILE *VirtioFsFile;
  EFI_STATUS     Status;
  EFI_FILE_INFO  *FileInfo;

  VirtioFsFile = VIRTIO_FS_FILE_FROM_SIMPLE_FILE (This);

  //
  // Validate if Buffer passes as EFI_FILE_INFO.
  //
  Status = ValidateInfoStructure (
             BufferSize,                    // SizeByProtocolCaller
             OFFSET_OF (EFI_FILE_INFO,
               FileName) + sizeof (CHAR16), // MinimumStructSize
             TRUE,                          // IsSizeByInfoPresent
             Buffer
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }
  FileInfo = Buffer;

  //
  // Perform the rename/move request, if any.
  //
  Status = Rename (VirtioFsFile, FileInfo->FileName);
  if (EFI_ERROR (Status)) {
    return Status;
  }
  //
  // Update any attributes requested.
  //
  Status = UpdateAttributes (VirtioFsFile, FileInfo);
  //
  // The UEFI spec does not speak about partial failure in
  // EFI_FILE_PROTOCOL.SetInfo(); we won't try to roll back the rename (if
  // there was one) in case the attribute updates fail.
  //
  return Status;
}

/**
  Process an EFI_FILE_SYSTEM_INFO setting request.
**/
STATIC
EFI_STATUS
SetFileSystemInfo (
  IN EFI_FILE_PROTOCOL *This,
  IN UINTN             BufferSize,
  IN VOID              *Buffer
  )
{
  VIRTIO_FS_FILE       *VirtioFsFile;
  VIRTIO_FS            *VirtioFs;
  EFI_STATUS           Status;
  EFI_FILE_SYSTEM_INFO *FileSystemInfo;

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

  //
  // Validate if Buffer passes as EFI_FILE_SYSTEM_INFO.
  //
  Status = ValidateInfoStructure (
             BufferSize,                       // SizeByProtocolCaller
             OFFSET_OF (EFI_FILE_SYSTEM_INFO,
               VolumeLabel) + sizeof (CHAR16), // MinimumStructSize
             TRUE,                             // IsSizeByInfoPresent
             Buffer
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }
  FileSystemInfo = Buffer;

  //
  // EFI_FILE_SYSTEM_INFO fields other than VolumeLabel cannot be changed, per
  // spec.
  //
  // If the label is being changed to its current value, report success;
  // otherwise, reject the request, as the Virtio Filesystem device does not
  // support changing the label.
  //
  if (StrCmp (FileSystemInfo->VolumeLabel, VirtioFs->Label) == 0) {
    return EFI_SUCCESS;
  }
  return EFI_WRITE_PROTECTED;
}

/**
  Process an EFI_FILE_SYSTEM_VOLUME_LABEL setting request.
**/
STATIC
EFI_STATUS
SetFileSystemVolumeLabelInfo (
  IN EFI_FILE_PROTOCOL *This,
  IN UINTN             BufferSize,
  IN VOID              *Buffer
  )
{
  VIRTIO_FS_FILE               *VirtioFsFile;
  VIRTIO_FS                    *VirtioFs;
  EFI_STATUS                   Status;
  EFI_FILE_SYSTEM_VOLUME_LABEL *FileSystemVolumeLabel;

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

  //
  // Validate if Buffer passes as EFI_FILE_SYSTEM_VOLUME_LABEL.
  //
  Status = ValidateInfoStructure (
             BufferSize,                              // SizeByProtocolCaller
             OFFSET_OF (EFI_FILE_SYSTEM_VOLUME_LABEL,
               VolumeLabel) + sizeof (CHAR16),        // MinimumStructSize
             FALSE,                                   // IsSizeByInfoPresent
             Buffer
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }
  FileSystemVolumeLabel = Buffer;

  //
  // If the label is being changed to its current value, report success;
  // otherwise, reject the request, as the Virtio Filesystem device does not
  // support changing the label.
  //
  if (StrCmp (FileSystemVolumeLabel->VolumeLabel, VirtioFs->Label) == 0) {
    return EFI_SUCCESS;
  }
  return EFI_WRITE_PROTECTED;
}

EFI_STATUS
EFIAPI
VirtioFsSimpleFileSetInfo (
  IN EFI_FILE_PROTOCOL *This,
  IN EFI_GUID          *InformationType,
  IN UINTN             BufferSize,
  IN VOID              *Buffer
  )
{
  if (CompareGuid (InformationType, &gEfiFileInfoGuid)) {
    return SetFileInfo (This, BufferSize, Buffer);
  }

  if (CompareGuid (InformationType, &gEfiFileSystemInfoGuid)) {
    return SetFileSystemInfo (This, BufferSize, Buffer);
  }

  if (CompareGuid (InformationType, &gEfiFileSystemVolumeLabelInfoIdGuid)) {
    return SetFileSystemVolumeLabelInfo (This, BufferSize, Buffer);
  }

  return EFI_UNSUPPORTED;
}