/** @file
  function declarations for shell environment functions.

  Copyright (c) 2009 - 2018, Intel Corporation. All rights reserved.<BR>
  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include "Shell.h"

#define INIT_NAME_BUFFER_SIZE  128
#define INIT_DATA_BUFFER_SIZE  1024

//
// The list is used to cache the environment variables.
//
ENV_VAR_LIST  gShellEnvVarList;

/**
  Reports whether an environment variable is Volatile or Non-Volatile.

  @param EnvVarName             The name of the environment variable in question
  @param Volatile               Return TRUE if the environment variable is volatile

  @retval EFI_SUCCESS           The volatile attribute is returned successfully
  @retval others                Some errors happened.
**/
EFI_STATUS
IsVolatileEnv (
  IN CONST CHAR16  *EnvVarName,
  OUT BOOLEAN      *Volatile
  )
{
  EFI_STATUS  Status;
  UINTN       Size;
  VOID        *Buffer;
  UINT32      Attribs;

  ASSERT (Volatile != NULL);

  Size   = 0;
  Buffer = NULL;

  //
  // get the variable
  //
  Status = gRT->GetVariable (
                  (CHAR16 *)EnvVarName,
                  &gShellVariableGuid,
                  &Attribs,
                  &Size,
                  Buffer
                  );
  if (Status == EFI_BUFFER_TOO_SMALL) {
    Buffer = AllocateZeroPool (Size);
    if (Buffer == NULL) {
      return EFI_OUT_OF_RESOURCES;
    }

    Status = gRT->GetVariable (
                    (CHAR16 *)EnvVarName,
                    &gShellVariableGuid,
                    &Attribs,
                    &Size,
                    Buffer
                    );
    FreePool (Buffer);
  }

  //
  // not found means volatile
  //
  if (Status == EFI_NOT_FOUND) {
    *Volatile = TRUE;
    return EFI_SUCCESS;
  }

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

  //
  // check for the Non Volatile bit
  //
  *Volatile = !(BOOLEAN)((Attribs & EFI_VARIABLE_NON_VOLATILE) == EFI_VARIABLE_NON_VOLATILE);
  return EFI_SUCCESS;
}

/**
  free function for ENV_VAR_LIST objects.

  @param[in] List               The pointer to pointer to list.
**/
VOID
FreeEnvironmentVariableList (
  IN LIST_ENTRY  *List
  )
{
  ENV_VAR_LIST  *Node;

  ASSERT (List != NULL);
  if (List == NULL) {
    return;
  }

  for ( Node = (ENV_VAR_LIST *)GetFirstNode (List)
        ; !IsListEmpty (List)
        ; Node = (ENV_VAR_LIST *)GetFirstNode (List)
        )
  {
    ASSERT (Node != NULL);
    RemoveEntryList (&Node->Link);
    if (Node->Key != NULL) {
      FreePool (Node->Key);
    }

    if (Node->Val != NULL) {
      FreePool (Node->Val);
    }

    FreePool (Node);
  }
}

/**
  Creates a list of all Shell-Guid-based environment variables.

  @param[in, out] ListHead       The pointer to pointer to LIST ENTRY object for
                                 storing this list.

  @retval EFI_SUCCESS           the list was created successfully.
**/
EFI_STATUS
GetEnvironmentVariableList (
  IN OUT LIST_ENTRY  *ListHead
  )
{
  CHAR16        *VariableName;
  UINTN         NameSize;
  UINTN         NameBufferSize;
  EFI_STATUS    Status;
  EFI_GUID      Guid;
  UINTN         ValSize;
  UINTN         ValBufferSize;
  ENV_VAR_LIST  *VarList;

  if (ListHead == NULL) {
    return (EFI_INVALID_PARAMETER);
  }

  Status = EFI_SUCCESS;

  ValBufferSize  = INIT_DATA_BUFFER_SIZE;
  NameBufferSize = INIT_NAME_BUFFER_SIZE;
  VariableName   = AllocateZeroPool (NameBufferSize);
  if (VariableName == NULL) {
    return (EFI_OUT_OF_RESOURCES);
  }

  *VariableName = CHAR_NULL;

  while (!EFI_ERROR (Status)) {
    NameSize = NameBufferSize;
    Status   = gRT->GetNextVariableName (&NameSize, VariableName, &Guid);
    if (Status == EFI_NOT_FOUND) {
      Status = EFI_SUCCESS;
      break;
    } else if (Status == EFI_BUFFER_TOO_SMALL) {
      NameBufferSize = NameSize > NameBufferSize * 2 ? NameSize : NameBufferSize * 2;
      SHELL_FREE_NON_NULL (VariableName);
      VariableName = AllocateZeroPool (NameBufferSize);
      if (VariableName == NULL) {
        Status = EFI_OUT_OF_RESOURCES;
        break;
      }

      NameSize = NameBufferSize;
      Status   = gRT->GetNextVariableName (&NameSize, VariableName, &Guid);
    }

    if (!EFI_ERROR (Status) && CompareGuid (&Guid, &gShellVariableGuid)) {
      VarList = AllocateZeroPool (sizeof (ENV_VAR_LIST));
      if (VarList == NULL) {
        Status = EFI_OUT_OF_RESOURCES;
      } else {
        ValSize = ValBufferSize;
        //
        // We need another CHAR16 to save '\0' in VarList->Val.
        //
        VarList->Val = AllocateZeroPool (ValSize + sizeof (CHAR16));
        if (VarList->Val == NULL) {
          SHELL_FREE_NON_NULL (VarList);
          Status = EFI_OUT_OF_RESOURCES;
          break;
        }

        Status = SHELL_GET_ENVIRONMENT_VARIABLE_AND_ATTRIBUTES (VariableName, &VarList->Atts, &ValSize, VarList->Val);
        if (Status == EFI_BUFFER_TOO_SMALL) {
          ValBufferSize = ValSize > ValBufferSize * 2 ? ValSize : ValBufferSize * 2;
          SHELL_FREE_NON_NULL (VarList->Val);
          //
          // We need another CHAR16 to save '\0' in VarList->Val.
          //
          VarList->Val = AllocateZeroPool (ValBufferSize + sizeof (CHAR16));
          if (VarList->Val == NULL) {
            SHELL_FREE_NON_NULL (VarList);
            Status = EFI_OUT_OF_RESOURCES;
            break;
          }

          ValSize = ValBufferSize;
          Status  = SHELL_GET_ENVIRONMENT_VARIABLE_AND_ATTRIBUTES (VariableName, &VarList->Atts, &ValSize, VarList->Val);
        }

        if (!EFI_ERROR (Status)) {
          VarList->Key = AllocateCopyPool (StrSize (VariableName), VariableName);
          if (VarList->Key == NULL) {
            SHELL_FREE_NON_NULL (VarList->Val);
            SHELL_FREE_NON_NULL (VarList);
            Status = EFI_OUT_OF_RESOURCES;
          } else {
            InsertTailList (ListHead, &VarList->Link);
          }
        } else {
          SHELL_FREE_NON_NULL (VarList->Val);
          SHELL_FREE_NON_NULL (VarList);
        }
      } // if (VarList == NULL) ... else ...
    } // compare guid
  } // while

  SHELL_FREE_NON_NULL (VariableName);

  if (EFI_ERROR (Status)) {
    FreeEnvironmentVariableList (ListHead);
  }

  return (Status);
}

/**
  Sets a list of all Shell-Guid-based environment variables.  this will
  also eliminate all existing shell environment variables (even if they
  are not on the list).

  This function will also deallocate the memory from List.

  @param[in] ListHead           The pointer to LIST_ENTRY from
                                GetShellEnvVarList().

  @retval EFI_SUCCESS           the list was Set successfully.
**/
EFI_STATUS
SetEnvironmentVariableList (
  IN LIST_ENTRY  *ListHead
  )
{
  ENV_VAR_LIST  VarList;
  ENV_VAR_LIST  *Node;
  EFI_STATUS    Status;
  UINTN         Size;

  InitializeListHead (&VarList.Link);

  //
  // Delete all the current environment variables
  //
  Status = GetEnvironmentVariableList (&VarList.Link);
  ASSERT_EFI_ERROR (Status);

  for ( Node = (ENV_VAR_LIST *)GetFirstNode (&VarList.Link)
        ; !IsNull (&VarList.Link, &Node->Link)
        ; Node = (ENV_VAR_LIST *)GetNextNode (&VarList.Link, &Node->Link)
        )
  {
    if (Node->Key != NULL) {
      Status = SHELL_DELETE_ENVIRONMENT_VARIABLE (Node->Key);
    }

    ASSERT_EFI_ERROR (Status);
  }

  FreeEnvironmentVariableList (&VarList.Link);

  //
  // set all the variables from the list
  //
  if ((ListHead != NULL) && !IsListEmpty (ListHead)) {
    for ( Node = (ENV_VAR_LIST *)GetFirstNode (ListHead)
          ; !IsNull (ListHead, &Node->Link)
          ; Node = (ENV_VAR_LIST *)GetNextNode (ListHead, &Node->Link)
          )
    {
      if ((Node->Key != NULL) && (Node->Val != NULL)) {
        Size = StrSize (Node->Val) - sizeof (CHAR16);
        if (Node->Atts & EFI_VARIABLE_NON_VOLATILE) {
          Status = SHELL_SET_ENVIRONMENT_VARIABLE_NV (Node->Key, Size, Node->Val);
        } else {
          Status = SHELL_SET_ENVIRONMENT_VARIABLE_V (Node->Key, Size, Node->Val);
        }

        ASSERT_EFI_ERROR (Status);
      }
    }
  }

  FreeEnvironmentVariableList (ListHead);

  return (Status);
}

/**
  sets a list of all Shell-Guid-based environment variables.

  @param Environment        Points to a NULL-terminated array of environment
                            variables with the format 'x=y', where x is the
                            environment variable name and y is the value.

  @retval EFI_SUCCESS       The command executed successfully.
  @retval EFI_INVALID_PARAMETER The parameter is invalid.
  @retval EFI_OUT_OF_RESOURCES Out of resources.

  @sa SetEnvironmentVariableList
**/
EFI_STATUS
SetEnvironmentVariables (
  IN CONST CHAR16  **Environment
  )
{
  CONST CHAR16  *CurrentString;
  UINTN         CurrentCount;
  ENV_VAR_LIST  *VarList;
  ENV_VAR_LIST  *Node;

  VarList = NULL;

  if (Environment == NULL) {
    return (EFI_INVALID_PARAMETER);
  }

  //
  // Build a list identical to the ones used for get/set list functions above
  //
  for ( CurrentCount = 0
        ;
        ; CurrentCount++
        )
  {
    CurrentString = Environment[CurrentCount];
    if (CurrentString == NULL) {
      break;
    }

    ASSERT (StrStr (CurrentString, L"=") != NULL);
    Node = AllocateZeroPool (sizeof (ENV_VAR_LIST));
    if (Node == NULL) {
      SetEnvironmentVariableList (&VarList->Link);
      return (EFI_OUT_OF_RESOURCES);
    }

    Node->Key = AllocateZeroPool ((StrStr (CurrentString, L"=") - CurrentString + 1) * sizeof (CHAR16));
    if (Node->Key == NULL) {
      SHELL_FREE_NON_NULL (Node);
      SetEnvironmentVariableList (&VarList->Link);
      return (EFI_OUT_OF_RESOURCES);
    }

    //
    // Copy the string into the Key, leaving the last character allocated as NULL to terminate
    //
    StrnCpyS (
      Node->Key,
      StrStr (CurrentString, L"=") - CurrentString + 1,
      CurrentString,
      StrStr (CurrentString, L"=") - CurrentString
      );

    //
    // ValueSize = TotalSize - already removed size - size for '=' + size for terminator (the last 2 items cancel each other)
    //
    Node->Val = AllocateCopyPool (StrSize (CurrentString) - StrSize (Node->Key), CurrentString + StrLen (Node->Key) + 1);
    if (Node->Val == NULL) {
      SHELL_FREE_NON_NULL (Node->Key);
      SHELL_FREE_NON_NULL (Node);
      SetEnvironmentVariableList (&VarList->Link);
      return (EFI_OUT_OF_RESOURCES);
    }

    Node->Atts = EFI_VARIABLE_BOOTSERVICE_ACCESS;

    if (VarList == NULL) {
      VarList = AllocateZeroPool (sizeof (ENV_VAR_LIST));
      if (VarList == NULL) {
        SHELL_FREE_NON_NULL (Node->Key);
        SHELL_FREE_NON_NULL (Node->Val);
        SHELL_FREE_NON_NULL (Node);
        return (EFI_OUT_OF_RESOURCES);
      }

      InitializeListHead (&VarList->Link);
    }

    InsertTailList (&VarList->Link, &Node->Link);
  } // for loop

  //
  // set this new list as the set of all environment variables.
  // this function also frees the memory and deletes all pre-existing
  // shell-guid based environment variables.
  //
  return (SetEnvironmentVariableList (&VarList->Link));
}

/**
  Find an environment variable in the gShellEnvVarList.

  @param Key        The name of the environment variable.
  @param Value      The value of the environment variable, the buffer
                    shoule be freed by the caller.
  @param ValueSize  The size in bytes of the environment variable
                    including the tailing CHAR_NELL.
  @param Atts       The attributes of the variable.

  @retval EFI_SUCCESS       The command executed successfully.
  @retval EFI_NOT_FOUND     The environment variable is not found in
                            gShellEnvVarList.

**/
EFI_STATUS
ShellFindEnvVarInList (
  IN  CONST CHAR16  *Key,
  OUT CHAR16        **Value,
  OUT UINTN         *ValueSize,
  OUT UINT32        *Atts OPTIONAL
  )
{
  ENV_VAR_LIST  *Node;

  if ((Key == NULL) || (Value == NULL) || (ValueSize == NULL)) {
    return SHELL_INVALID_PARAMETER;
  }

  for ( Node = (ENV_VAR_LIST *)GetFirstNode (&gShellEnvVarList.Link)
        ; !IsNull (&gShellEnvVarList.Link, &Node->Link)
        ; Node = (ENV_VAR_LIST *)GetNextNode (&gShellEnvVarList.Link, &Node->Link)
        )
  {
    if ((Node->Key != NULL) && (StrCmp (Key, Node->Key) == 0) && (Node->Val != NULL)) {
      *Value     = AllocateCopyPool (StrSize (Node->Val), Node->Val);
      *ValueSize = StrSize (Node->Val);
      if (Atts != NULL) {
        *Atts = Node->Atts;
      }

      return EFI_SUCCESS;
    }
  }

  return EFI_NOT_FOUND;
}

/**
  Add an environment variable into gShellEnvVarList.

  @param Key        The name of the environment variable.
  @param Value      The value of environment variable.
  @param ValueSize  The size in bytes of the environment variable
                    including the tailing CHAR_NULL
  @param Atts       The attributes of the variable.

  @retval EFI_SUCCESS  The environment variable was added to list successfully.
  @retval others       Some errors happened.

**/
EFI_STATUS
ShellAddEnvVarToList (
  IN CONST CHAR16  *Key,
  IN CONST CHAR16  *Value,
  IN UINTN         ValueSize,
  IN UINT32        Atts
  )
{
  ENV_VAR_LIST  *Node;
  CHAR16        *LocalKey;
  CHAR16        *LocalValue;

  if ((Key == NULL) || (Value == NULL) || (ValueSize == 0)) {
    return EFI_INVALID_PARAMETER;
  }

  LocalValue = AllocateCopyPool (ValueSize, Value);
  if (LocalValue == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  //
  // Update the variable value if it exists in gShellEnvVarList.
  //
  for ( Node = (ENV_VAR_LIST *)GetFirstNode (&gShellEnvVarList.Link)
        ; !IsNull (&gShellEnvVarList.Link, &Node->Link)
        ; Node = (ENV_VAR_LIST *)GetNextNode (&gShellEnvVarList.Link, &Node->Link)
        )
  {
    if ((Node->Key != NULL) && (StrCmp (Key, Node->Key) == 0)) {
      Node->Atts = Atts;
      SHELL_FREE_NON_NULL (Node->Val);
      Node->Val = LocalValue;
      return EFI_SUCCESS;
    }
  }

  //
  // If the environment variable key doesn't exist in list just insert
  // a new node.
  //
  LocalKey = AllocateCopyPool (StrSize (Key), Key);
  if (LocalKey == NULL) {
    FreePool (LocalValue);
    return EFI_OUT_OF_RESOURCES;
  }

  Node = (ENV_VAR_LIST *)AllocateZeroPool (sizeof (ENV_VAR_LIST));
  if (Node == NULL) {
    FreePool (LocalKey);
    FreePool (LocalValue);
    return EFI_OUT_OF_RESOURCES;
  }

  Node->Key  = LocalKey;
  Node->Val  = LocalValue;
  Node->Atts = Atts;
  InsertTailList (&gShellEnvVarList.Link, &Node->Link);

  return EFI_SUCCESS;
}

/**
  Remove a specified environment variable in gShellEnvVarList.

  @param Key        The name of the environment variable.

  @retval EFI_SUCCESS       The command executed successfully.
  @retval EFI_NOT_FOUND     The environment variable is not found in
                            gShellEnvVarList.
**/
EFI_STATUS
ShellRemvoeEnvVarFromList (
  IN CONST CHAR16  *Key
  )
{
  ENV_VAR_LIST  *Node;

  if (Key == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  for ( Node = (ENV_VAR_LIST *)GetFirstNode (&gShellEnvVarList.Link)
        ; !IsNull (&gShellEnvVarList.Link, &Node->Link)
        ; Node = (ENV_VAR_LIST *)GetNextNode (&gShellEnvVarList.Link, &Node->Link)
        )
  {
    if ((Node->Key != NULL) && (StrCmp (Key, Node->Key) == 0)) {
      SHELL_FREE_NON_NULL (Node->Key);
      SHELL_FREE_NON_NULL (Node->Val);
      RemoveEntryList (&Node->Link);
      SHELL_FREE_NON_NULL (Node);
      return EFI_SUCCESS;
    }
  }

  return EFI_NOT_FOUND;
}

/**
  Initialize the gShellEnvVarList and cache all Shell-Guid-based environment
  variables.

**/
EFI_STATUS
ShellInitEnvVarList (
  VOID
  )
{
  EFI_STATUS  Status;

  InitializeListHead (&gShellEnvVarList.Link);
  Status = GetEnvironmentVariableList (&gShellEnvVarList.Link);

  return Status;
}

/**
  Destructe the gShellEnvVarList.

**/
VOID
ShellFreeEnvVarList (
  VOID
  )
{
  FreeEnvironmentVariableList (&gShellEnvVarList.Link);
  InitializeListHead (&gShellEnvVarList.Link);

  return;
}
