/** @file
  Provides 'initrd' dynamic UEFI shell command to load a Linux initrd
  via its GUIDed vendor media path

  Copyright (c) 2020, Arm, Ltd. All rights reserved.<BR>

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

#include <Uefi.h>

#include <Library/DebugLib.h>
#include <Library/DevicePathLib.h>
#include <Library/HiiLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/ShellLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiHiiServicesLib.h>

#include <Guid/LinuxEfiInitrdMedia.h>

#include <Protocol/DevicePath.h>
#include <Protocol/HiiPackageList.h>
#include <Protocol/LoadFile2.h>
#include <Protocol/ShellDynamicCommand.h>

#pragma pack (1)
typedef struct {
  VENDOR_DEVICE_PATH          VenMediaNode;
  EFI_DEVICE_PATH_PROTOCOL    EndNode;
} SINGLE_NODE_VENDOR_MEDIA_DEVPATH;
#pragma pack ()

STATIC EFI_HII_HANDLE         mLinuxInitrdShellCommandHiiHandle;
STATIC EFI_PHYSICAL_ADDRESS   mInitrdFileAddress;
STATIC UINTN                  mInitrdFileSize;
STATIC EFI_HANDLE             mInitrdLoadFile2Handle;

STATIC CONST SHELL_PARAM_ITEM ParamList[] = {
  {L"-u", TypeFlag},
  {NULL, TypeMax}
  };

STATIC CONST SINGLE_NODE_VENDOR_MEDIA_DEVPATH mInitrdDevicePath = {
  {
    {
      MEDIA_DEVICE_PATH, MEDIA_VENDOR_DP, { sizeof (VENDOR_DEVICE_PATH) }
    },
    LINUX_EFI_INITRD_MEDIA_GUID
  }, {
    END_DEVICE_PATH_TYPE, END_ENTIRE_DEVICE_PATH_SUBTYPE,
    { sizeof (EFI_DEVICE_PATH_PROTOCOL) }
  }
};

STATIC
BOOLEAN
IsOtherInitrdDevicePathAlreadyInstalled (
  VOID
  )
{
  EFI_STATUS                  Status;
  EFI_DEVICE_PATH_PROTOCOL    *DevicePath;
  EFI_HANDLE                  Handle;

  DevicePath = (EFI_DEVICE_PATH_PROTOCOL *)&mInitrdDevicePath;
  Status = gBS->LocateDevicePath (&gEfiLoadFile2ProtocolGuid, &DevicePath,
                  &Handle);
  if (EFI_ERROR (Status)) {
    return FALSE;
  }

  //
  // Check whether the existing instance is one that we installed during
  // a previous invocation.
  //
  if (Handle == mInitrdLoadFile2Handle) {
    return FALSE;
  }
  return TRUE;
}

STATIC
EFI_STATUS
EFIAPI
InitrdLoadFile2 (
  IN      EFI_LOAD_FILE2_PROTOCOL       *This,
  IN      EFI_DEVICE_PATH_PROTOCOL      *FilePath,
  IN      BOOLEAN                       BootPolicy,
  IN  OUT UINTN                         *BufferSize,
  OUT     VOID                          *Buffer     OPTIONAL
  )
{
  if (BootPolicy) {
    return EFI_UNSUPPORTED;
  }

  if (BufferSize == NULL || !IsDevicePathValid (FilePath, 0)) {
    return EFI_INVALID_PARAMETER;
  }

  if (FilePath->Type != END_DEVICE_PATH_TYPE ||
      FilePath->SubType != END_ENTIRE_DEVICE_PATH_SUBTYPE ||
      mInitrdFileSize == 0) {
    return EFI_NOT_FOUND;
  }

  if (Buffer == NULL || *BufferSize < mInitrdFileSize) {
    *BufferSize = mInitrdFileSize;
    return EFI_BUFFER_TOO_SMALL;
  }

  ASSERT (mInitrdFileAddress != 0);

  gBS->CopyMem (Buffer, (VOID *)(UINTN)mInitrdFileAddress, mInitrdFileSize);
  *BufferSize = mInitrdFileSize;
  return EFI_SUCCESS;
}

STATIC CONST EFI_LOAD_FILE2_PROTOCOL     mInitrdLoadFile2 = {
  InitrdLoadFile2,
};

STATIC
EFI_STATUS
UninstallLoadFile2Protocol (
  VOID
  )
{
  EFI_STATUS    Status;

  if (mInitrdLoadFile2Handle != NULL) {
    Status = gBS->UninstallMultipleProtocolInterfaces (mInitrdLoadFile2Handle,
                    &gEfiDevicePathProtocolGuid,    &mInitrdDevicePath,
                    &gEfiLoadFile2ProtocolGuid,     &mInitrdLoadFile2,
                    NULL);
    if (!EFI_ERROR (Status)) {
      mInitrdLoadFile2Handle = NULL;
    }
    return Status;
  }
  return EFI_SUCCESS;
}

STATIC
VOID
FreeInitrdFile (
  VOID
  )
{
  if (mInitrdFileSize != 0) {
    gBS->FreePages (mInitrdFileAddress, EFI_SIZE_TO_PAGES (mInitrdFileSize));
    mInitrdFileSize = 0;
  }
}

STATIC
EFI_STATUS
CacheInitrdFile (
  IN  SHELL_FILE_HANDLE   FileHandle
  )
{
  EFI_STATUS              Status;
  UINT64                  FileSize;
  UINTN                   ReadSize;

  Status = gEfiShellProtocol->GetFileSize (FileHandle, &FileSize);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  if (FileSize == 0 || FileSize > MAX_UINTN) {
    return EFI_UNSUPPORTED;
  }

  Status = gBS->AllocatePages (AllocateAnyPages, EfiLoaderData,
                  EFI_SIZE_TO_PAGES ((UINTN)FileSize), &mInitrdFileAddress);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  ReadSize = (UINTN)FileSize;
  Status = gEfiShellProtocol->ReadFile (FileHandle, &ReadSize,
                                (VOID *)(UINTN)mInitrdFileAddress);
  if (EFI_ERROR (Status) || ReadSize < FileSize) {
    DEBUG ((DEBUG_WARN, "%a: failed to read initrd file - %r 0x%lx 0x%lx\n",
      __FUNCTION__, Status, (UINT64)ReadSize, FileSize));
    goto FreeMemory;
  }

  if (mInitrdLoadFile2Handle == NULL) {
    Status = gBS->InstallMultipleProtocolInterfaces (&mInitrdLoadFile2Handle,
                    &gEfiDevicePathProtocolGuid,  &mInitrdDevicePath,
                    &gEfiLoadFile2ProtocolGuid,   &mInitrdLoadFile2,
                    NULL);
    ASSERT_EFI_ERROR (Status);
  }

  mInitrdFileSize = (UINTN)FileSize;
  return EFI_SUCCESS;

FreeMemory:
  gBS->FreePages (mInitrdFileAddress, EFI_SIZE_TO_PAGES ((UINTN)FileSize));
  return Status;
}

/**
  Function for 'initrd' command.

  @param[in] ImageHandle  Handle to the Image (NULL if Internal).
  @param[in] SystemTable  Pointer to the System Table (NULL if Internal).
**/
STATIC
SHELL_STATUS
EFIAPI
RunInitrd (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS            Status;
  LIST_ENTRY            *Package;
  CHAR16                *ProblemParam;
  CONST CHAR16          *Param;
  CHAR16                *Filename;
  SHELL_STATUS          ShellStatus;
  SHELL_FILE_HANDLE     FileHandle;

  ProblemParam        = NULL;
  ShellStatus         = SHELL_SUCCESS;

  Status = ShellInitialize ();
  ASSERT_EFI_ERROR (Status);

  //
  // parse the command line
  //
  Status = ShellCommandLineParse (ParamList, &Package, &ProblemParam, TRUE);
  if (EFI_ERROR (Status)) {
    if (Status == EFI_VOLUME_CORRUPTED && ProblemParam != NULL) {
      ShellPrintHiiEx (-1, -1, NULL, STRING_TOKEN (STR_GEN_PROBLEM),
        mLinuxInitrdShellCommandHiiHandle, L"initrd", ProblemParam);
      FreePool (ProblemParam);
      ShellStatus = SHELL_INVALID_PARAMETER;
    } else {
      ASSERT(FALSE);
    }
  } else if (IsOtherInitrdDevicePathAlreadyInstalled ()) {
    ShellPrintHiiEx (-1, -1, NULL, STRING_TOKEN (STR_GEN_ALREADY_INSTALLED),
      mLinuxInitrdShellCommandHiiHandle, L"initrd");
    ShellStatus = SHELL_UNSUPPORTED;
  } else {
    if (ShellCommandLineGetCount (Package) > 2) {
      ShellPrintHiiEx (-1, -1, NULL, STRING_TOKEN (STR_GEN_TOO_MANY),
        mLinuxInitrdShellCommandHiiHandle, L"initrd");
      ShellStatus = SHELL_INVALID_PARAMETER;
    } else if (ShellCommandLineGetCount (Package) < 2) {
      if (ShellCommandLineGetFlag (Package, L"-u")) {
        FreeInitrdFile ();
        UninstallLoadFile2Protocol ();
      } else {
        ShellPrintHiiEx (-1, -1, NULL, STRING_TOKEN (STR_GEN_TOO_FEW),
          mLinuxInitrdShellCommandHiiHandle, L"initrd");
        ShellStatus = SHELL_INVALID_PARAMETER;
      }
    } else {
      Param = ShellCommandLineGetRawValue (Package, 1);
      ASSERT (Param != NULL);

      Filename = ShellFindFilePath (Param);
      if (Filename == NULL) {
        ShellPrintHiiEx (-1, -1, NULL, STRING_TOKEN (STR_GEN_FIND_FAIL),
          mLinuxInitrdShellCommandHiiHandle, L"initrd", Param);
        ShellStatus = SHELL_NOT_FOUND;
      } else {
        Status = ShellOpenFileByName (Filename, &FileHandle,
                   EFI_FILE_MODE_READ, 0);
        if (!EFI_ERROR (Status)) {
          FreeInitrdFile ();
          Status = CacheInitrdFile (FileHandle);
          ShellCloseFile (&FileHandle);
        }
        if (EFI_ERROR (Status)) {
          ShellPrintHiiEx (-1, -1, NULL, STRING_TOKEN (STR_GEN_FILE_OPEN_FAIL),
            mLinuxInitrdShellCommandHiiHandle, L"initrd", Param);
          ShellStatus = SHELL_NOT_FOUND;
        }
        FreePool (Filename);
      }
    }
  }
  return ShellStatus;
}


/**
  This is the shell command handler function pointer callback type.  This
  function handles the command when it is invoked in the shell.

  @param[in] This                   The instance of the
                                    EFI_SHELL_DYNAMIC_COMMAND_PROTOCOL.
  @param[in] SystemTable            The pointer to the system table.
  @param[in] ShellParameters        The parameters associated with the command.
  @param[in] Shell                  The instance of the shell protocol used in
                                    the context of processing this command.

  @return EFI_SUCCESS               the operation was successful
  @return other                     the operation failed.
**/
SHELL_STATUS
EFIAPI
LinuxInitrdCommandHandler (
  IN EFI_SHELL_DYNAMIC_COMMAND_PROTOCOL    *This,
  IN EFI_SYSTEM_TABLE                      *SystemTable,
  IN EFI_SHELL_PARAMETERS_PROTOCOL         *ShellParameters,
  IN EFI_SHELL_PROTOCOL                    *Shell
  )
{
  gEfiShellParametersProtocol = ShellParameters;
  gEfiShellProtocol           = Shell;

  return RunInitrd (gImageHandle, SystemTable);
}

/**
  This is the command help handler function pointer callback type.  This
  function is responsible for displaying help information for the associated
  command.

  @param[in] This                   The instance of the
                                    EFI_SHELL_DYNAMIC_COMMAND_PROTOCOL.
  @param[in] Language               The pointer to the language string to use.

  @return string                    Pool allocated help string, must be freed
                                    by caller
**/
STATIC
CHAR16 *
EFIAPI
LinuxInitrdGetHelp (
  IN EFI_SHELL_DYNAMIC_COMMAND_PROTOCOL    *This,
  IN CONST CHAR8                           *Language
  )
{
  return HiiGetString (mLinuxInitrdShellCommandHiiHandle,
           STRING_TOKEN (STR_GET_HELP_INITRD), Language);
}

STATIC EFI_SHELL_DYNAMIC_COMMAND_PROTOCOL mLinuxInitrdDynamicCommand = {
  L"initrd",
  LinuxInitrdCommandHandler,
  LinuxInitrdGetHelp
};

/**
  Retrieve HII package list from ImageHandle and publish to HII database.

  @param ImageHandle            The image handle of the process.

  @return HII handle.
**/
STATIC
EFI_HII_HANDLE
InitializeHiiPackage (
  EFI_HANDLE                  ImageHandle
  )
{
  EFI_STATUS                  Status;
  EFI_HII_PACKAGE_LIST_HEADER *PackageList;
  EFI_HII_HANDLE              HiiHandle;

  //
  // Retrieve HII package list from ImageHandle
  //
  Status = gBS->OpenProtocol (ImageHandle, &gEfiHiiPackageListProtocolGuid,
                  (VOID **)&PackageList, ImageHandle, NULL,
                  EFI_OPEN_PROTOCOL_GET_PROTOCOL);
  ASSERT_EFI_ERROR (Status);
  if (EFI_ERROR (Status)) {
    return NULL;
  }

  //
  // Publish HII package list to HII Database.
  //
  Status = gHiiDatabase->NewPackageList (gHiiDatabase, PackageList, NULL,
                           &HiiHandle);
  ASSERT_EFI_ERROR (Status);
  if (EFI_ERROR (Status)) {
    return NULL;
  }
  return HiiHandle;
}

/**
  Entry point of Linux Initrd dynamic UEFI Shell command.

  Produce the DynamicCommand protocol to handle "initrd" command.

  @param ImageHandle            The image handle of the process.
  @param SystemTable            The EFI System Table pointer.

  @retval EFI_SUCCESS           Initrd command is executed successfully.
  @retval EFI_ABORTED           HII package was failed to initialize.
  @retval others                Other errors when executing Initrd command.
**/
EFI_STATUS
EFIAPI
LinuxInitrdDynamicShellCommandEntryPoint (
  IN EFI_HANDLE               ImageHandle,
  IN EFI_SYSTEM_TABLE         *SystemTable
  )
{
  EFI_STATUS                  Status;

  mLinuxInitrdShellCommandHiiHandle = InitializeHiiPackage (ImageHandle);
  if (mLinuxInitrdShellCommandHiiHandle == NULL) {
    return EFI_ABORTED;
  }

  Status = gBS->InstallProtocolInterface (&ImageHandle,
                  &gEfiShellDynamicCommandProtocolGuid,
                  EFI_NATIVE_INTERFACE,
                  &mLinuxInitrdDynamicCommand);
  ASSERT_EFI_ERROR (Status);
  return Status;
}

/**
  Unload the dynamic UEFI Shell command.

  @param ImageHandle            The image handle of the process.

  @retval EFI_SUCCESS           The image is unloaded.
  @retval Others                Failed to unload the image.
**/
EFI_STATUS
EFIAPI
LinuxInitrdDynamicShellCommandUnload (
  IN EFI_HANDLE               ImageHandle
)
{
  EFI_STATUS                  Status;

  FreeInitrdFile ();

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

  Status = gBS->UninstallProtocolInterface (ImageHandle,
                  &gEfiShellDynamicCommandProtocolGuid,
                  &mLinuxInitrdDynamicCommand);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  HiiRemovePackages (mLinuxInitrdShellCommandHiiHandle);
  return EFI_SUCCESS;
}
