content: package com.termux.shared.file;
  
  import android.os.Build;
  import android.system.Os;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  
  import com.google.common.io.RecursiveDeleteOption;
  import com.termux.shared.file.filesystem.FileType;
  import com.termux.shared.file.filesystem.FileTypes;
  import com.termux.shared.data.DataUtils;
  import com.termux.shared.logger.Logger;
  import com.termux.shared.errors.Errno;
  import com.termux.shared.errors.Error;
  import com.termux.shared.errors.FunctionErrno;
  
  import org.apache.commons.io.filefilter.AgeFileFilter;
  import org.apache.commons.io.filefilter.IOFileFilter;
  
  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.Closeable;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.ObjectInputStream;
  import java.io.ObjectOutputStream;
  import java.io.OutputStreamWriter;
  import java.io.Serializable;
  import java.nio.charset.Charset;
  import java.nio.file.LinkOption;
  import java.nio.file.StandardCopyOption;
  import java.util.Arrays;
  import java.util.Calendar;
  import java.util.Collections;
  import java.util.Iterator;
  import java.util.List;
  import java.util.regex.Pattern;
  
  public class FileUtils {
  
      /** Required file permissions for the executable file for app usage. Executable file must have read and execute permissions */
      public static final String APP_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
      /** Required file permissions for the working directory for app usage. Working directory must have read and write permissions.
       * Execute permissions should be attempted to be set, but ignored if they are missing */
      public static final String APP_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
  
      private static final String LOG_TAG = "FileUtils";
  
      /**
       * Get canonical path.
       *
       * If path is already an absolute path, then it is used as is to get canonical path.
       * If path is not an absolute path and {code prefixForNonAbsolutePath} is not {@code null}, then
       * {code prefixForNonAbsolutePath} + "/" is prefixed before path before getting canonical path.
       * If path is not an absolute path and {code prefixForNonAbsolutePath} is {@code null}, then
       * "/" is prefixed before path before getting canonical path.
       *
       * If an exception is raised to get the canonical path, then absolute path is returned.
       *
       * @param path The {@code path} to convert.
       * @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
       *                                 can be set to {@code null} if non-absolute paths should
       *                                 be prefixed with "/". The call to {@link File#getCanonicalPath()}
       *                                 will automatically do this anyways.
       * @return Returns the {@code canonical path}.
       */
      public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath) {
          if (path == null) path = "";
  
          String absolutePath;
  
          // If path is already an absolute path
          if (path.startsWith("/")) {
              absolutePath = path;
          } else {
              if (prefixForNonAbsolutePath != null)
                  absolutePath = prefixForNonAbsolutePath + "/" + path;
              else
                  absolutePath = "/" + path;
          }
  
          try {
              return new File(absolutePath).getCanonicalPath();
          } catch(Exception e) {
          }
  
          return absolutePath;
      }
  
      /**
       * Removes one or more forward slashes "//" with single slash "/"
       * Removes "./"
       * Removes trailing forward slash "/"
       *
       * @param path The {@code path} to convert.
       * @return Returns the {@code normalized path}.
       */
      @Nullable
      public static String normalizePath(String path) {
          if (path == null) return null;
  
          path = path.replaceAll("/+", "/");
          path = path.replaceAll("\\./", "");
  
          if (path.endsWith("/")) {
              path = path.replaceAll("/+$", "");
          }
  
          return path;
      }
  
      /**
       * Convert special characters `\/:*?"<>|` to underscore.
       *
       * @param fileName The name to sanitize.
       * @param sanitizeWhitespaces If set to {@code true}, then white space characters ` \t\n` will be
       *                            converted.
       * @param toLower If set to {@code true}, then file name will be converted to lower case.
       * @return Returns the {@code sanitized name}.
       */
      public static String sanitizeFileName(String fileName, boolean sanitizeWhitespaces, boolean toLower) {
          if (fileName == null) return null;
  
          if (sanitizeWhitespaces)
              fileName = fileName.replaceAll("[\\\\/:*?\"<>| \t\n]", "_");
          else
              fileName = fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
  
          if (toLower)
              return fileName.toLowerCase();
          else
              return fileName;
      }
  
      /**
       * Determines whether path is in {@code dirPath}. The {@code dirPath} is not canonicalized and
       * only normalized.
       *
       * @param path The {@code path} to check.
       * @param dirPath The {@code directory path} to check in.
       * @param ensureUnder If set to {@code true}, then it will be ensured that {@code path} is
       *                    under the directory and does not equal it.
       * @return Returns {@code true} if path in {@code dirPath}, otherwise returns {@code false}.
       */
      public static boolean isPathInDirPath(String path, final String dirPath, final boolean ensureUnder) {
          return isPathInDirPaths(path, Collections.singletonList(dirPath), ensureUnder);
      }
  
      /**
       * Determines whether path is in one of the {@code dirPaths}. The {@code dirPaths} are not
       * canonicalized and only normalized.
       *
       * @param path The {@code path} to check.
       * @param dirPaths The {@code directory paths} to check in.
       * @param ensureUnder If set to {@code true}, then it will be ensured that {@code path} is
       *                    under the directories and does not equal it.
       * @return Returns {@code true} if path in {@code dirPaths}, otherwise returns {@code false}.
       */
      public static boolean isPathInDirPaths(String path, final List<String> dirPaths, final boolean ensureUnder) {
          if (path == null || path.isEmpty() || dirPaths == null || dirPaths.size() < 1) return false;
  
          try {
              path = new File(path).getCanonicalPath();
          } catch(Exception e) {
              return false;
          }
  
          boolean isPathInDirPaths;
  
          for (String dirPath : dirPaths) {
              String normalizedDirPath = normalizePath(dirPath);
  
              if (ensureUnder)
                  isPathInDirPaths = !path.equals(normalizedDirPath) && path.startsWith(normalizedDirPath + "/");
              else
                  isPathInDirPaths = path.startsWith(normalizedDirPath + "/");
  
              if (isPathInDirPaths) return true;
          }
  
          return false;
      }
  
  
  
      /**
       * Validate that directory is empty or contains only files in {@code ignoredSubFilePaths}.
       *
       * If parent path of an ignored file exists, but ignored file itself does not exist, then directory
       * is not considered empty.
       *
       * @param label The optional label for directory to check. This can optionally be {@code null}.
       * @param filePath The {@code path} for directory to check.
       * @param ignoredSubFilePaths The list of absolute file paths under {@code filePath} dir.
       *                            Validation is done for the paths.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to be checked doesn't exist.
       * @return Returns {@code null} if directory is empty or contains only files in {@code ignoredSubFilePaths}.
       * Returns {@code FileUtilsErrno#ERRNO_NON_EMPTY_DIRECTORY_FILE} if a file was found that did not
       * exist in the {@code ignoredSubFilePaths}, otherwise returns an appropriate {@code error} if
       * checking was not successful.
       */
      public static Error validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(String label, String filePath,
                                                                                final List<String> ignoredSubFilePaths,
                                                                                final boolean ignoreNonExistentFile) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "isDirectoryFileEmptyOrOnlyContainsSpecificFiles");
  
          try {
              File file = new File(filePath);
              FileType fileType = getFileType(filePath, false);
  
              // If file exists but not a directory file
              if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
                  return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
              }
  
              // If file does not exist
              if (fileType == FileType.NO_EXIST) {
                  // If checking is to be ignored if file does not exist
                  if (ignoreNonExistentFile)
                      return null;
                  else {
                      label += "directory to check if is empty or only contains specific files";
                      return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
                  }
              }
  
              File[] subFiles = file.listFiles();
              if (subFiles == null || subFiles.length == 0)
                  return null;
  
              // If sub files exists but no file should be ignored
              if (ignoredSubFilePaths == null || ignoredSubFilePaths.size() == 0)
                  return FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.getError(label, filePath);
  
              // If a sub file does not exist in ignored file path
              if (nonIgnoredSubFileExists(subFiles, ignoredSubFilePaths)) {
                  return FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.getError(label, filePath);
              }
  
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_VALIDATE_DIRECTORY_EMPTY_OR_ONLY_CONTAINS_SPECIFIC_FILES_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, e.getMessage());
          }
  
          return null;
      }
  
      /**
       * Check if {@code subFiles} contains contains a file not in {@code ignoredSubFilePaths}.
       *
       * If parent path of an ignored file exists, but ignored file itself does not exist, then directory
       * is not considered empty.
       *
       * This function should ideally not be called by itself but through
       * {@link #validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(String, String, List, boolean)}.
       *
       * @param subFiles The list of files of a directory to check.
       * @param ignoredSubFilePaths The list of absolute file paths under {@code filePath} dir.
       *                            Validation is done for the paths.
       * @return Returns {@code true} if a file was found that did not exist in the {@code ignoredSubFilePaths},
       * otherwise  {@code false}.
       */
      public static boolean nonIgnoredSubFileExists(File[] subFiles, @NonNull List<String> ignoredSubFilePaths) {
          if (subFiles == null || subFiles.length == 0) return false;
  
          String subFilePath;
          for (File subFile : subFiles) {
              subFilePath = subFile.getAbsolutePath();
              // If sub file does not exist in ignored sub file paths
              if (!ignoredSubFilePaths.contains(subFilePath)) {
                  boolean isParentPath = false;
                  for (String ignoredSubFilePath : ignoredSubFilePaths) {
                      if (ignoredSubFilePath.startsWith(subFilePath + "/") && fileExists(ignoredSubFilePath, false)) {
                          isParentPath = true;
                          break;
                      }
                  }
                  // If sub file is not a parent of any existing ignored sub file paths
                  if (!isParentPath) {
                      return true;
                  }
              }
                  
              if (getFileType(subFilePath, false) == FileType.DIRECTORY) {
                  // If non ignored sub file found, then early exit, otherwise continue looking
                  if (nonIgnoredSubFileExists(subFile.listFiles(), ignoredSubFilePaths))
                       return true;
              }
          }
  
          return false;
      }
  
  
  
      /**
       * Checks whether a regular file exists at {@code filePath}.
       *
       * @param filePath The {@code path} for regular file to check.
       * @param followLinks The {@code boolean} that decides if symlinks will be followed while
       *                       finding if file exists. Check {@link #getFileType(String, boolean)}
       *                       for details.
       * @return Returns {@code true} if regular file exists, otherwise {@code false}.
       */
      public static boolean regularFileExists(final String filePath, final boolean followLinks) {
          return getFileType(filePath, followLinks) == FileType.REGULAR;
      }
  
      /**
       * Checks whether a directory file exists at {@code filePath}.
       *
       * @param filePath The {@code path} for directory file to check.
       * @param followLinks The {@code boolean} that decides if symlinks will be followed while
       *                       finding if file exists. Check {@link #getFileType(String, boolean)}
       *                       for details.
       * @return Returns {@code true} if directory file exists, otherwise {@code false}.
       */
      public static boolean directoryFileExists(final String filePath, final boolean followLinks) {
          return getFileType(filePath, followLinks) == FileType.DIRECTORY;
      }
  
      /**
       * Checks whether a symlink file exists at {@code filePath}.
       *
       * @param filePath The {@code path} for symlink file to check.
       * @return Returns {@code true} if symlink file exists, otherwise {@code false}.
       */
      public static boolean symlinkFileExists(final String filePath) {
          return getFileType(filePath, false) == FileType.SYMLINK;
      }
  
      /**
       * Checks whether a regular or directory file exists at {@code filePath}.
       *
       * @param filePath The {@code path} for regular file to check.
       * @param followLinks The {@code boolean} that decides if symlinks will be followed while
       *                       finding if file exists. Check {@link #getFileType(String, boolean)}
       *                       for details.
       * @return Returns {@code true} if regular or directory file exists, otherwise {@code false}.
       */
      public static boolean regularOrDirectoryFileExists(final String filePath, final boolean followLinks) {
          FileType fileType = getFileType(filePath, followLinks);
          return fileType == FileType.REGULAR || fileType == FileType.DIRECTORY;
      }
  
      /**
       * Checks whether any file exists at {@code filePath}.
       *
       * @param filePath The {@code path} for file to check.
       * @param followLinks The {@code boolean} that decides if symlinks will be followed while
       *                       finding if file exists. Check {@link #getFileType(String, boolean)}
       *                       for details.
       * @return Returns {@code true} if file exists, otherwise {@code false}.
       */
      public static boolean fileExists(final String filePath, final boolean followLinks) {
          return getFileType(filePath, followLinks) != FileType.NO_EXIST;
      }
  
      /**
       * Get the type of file that exists at {@code filePath}.
       *
       * This function is a wrapper for
       * {@link FileTypes#getFileType(String, boolean)}
       *
       * @param filePath The {@code path} for file to check.
       * @param followLinks The {@code boolean} that decides if symlinks will be followed while
       *                       finding type. If set to {@code true}, then type of symlink target will
       *                       be returned if file at {@code filePath} is a symlink. If set to
       *                       {@code false}, then type of file at {@code filePath} itself will be
       *                       returned.
       * @return Returns the {@link FileType} of file.
       */
      @NonNull
      public static FileType getFileType(final String filePath, final boolean followLinks) {
          return FileTypes.getFileType(filePath, followLinks);
      }
  
  
  
      /**
       * Validate the existence and permissions of regular file at path.
       *
       * If the {@code parentDirPath} is not {@code null}, then setting of missing permissions will
       * only be done if {@code path} is under {@code parentDirPath}.
       *
       * @param label The optional label for the regular file. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to validate. Symlinks will not be followed.
       * @param parentDirPath The optional {@code parent directory path} to restrict operations to.
       *                      This can optionally be {@code null}. It is not canonicalized and only normalized.
       * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
       * @param setPermissions The {@code boolean} that decides if permissions are to be
       *                              automatically set defined by {@code permissionsToCheck}.
       * @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
       *                                  are to be set or if they should be overridden.
       * @param ignoreErrorsIfPathIsUnderParentDirPath The {@code boolean} that decides if permission
       *                                               errors are to be ignored if path is under
       *                                               {@code parentDirPath}.
       * @return Returns the {@code error} if path is not a regular file, or validating permissions
       * failed, otherwise {@code null}.
       */
      public static Error validateRegularFileExistenceAndPermissions(String label, final String filePath, final String parentDirPath,
                                                                     final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly,
                                                                     final boolean ignoreErrorsIfPathIsUnderParentDirPath) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "regular file path", "validateRegularFileExistenceAndPermissions");
  
          try {
              FileType fileType = getFileType(filePath, false);
  
              // If file exists but not a regular file
              if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
                  return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
              }
  
              boolean isPathUnderParentDirPath = false;
              if (parentDirPath != null) {
                  // The path can only be under parent directory path
                  isPathUnderParentDirPath = isPathInDirPath(filePath, parentDirPath, true);
              }
  
              // If setPermissions is enabled and path is a regular file
              if (setPermissions && permissionsToCheck != null && fileType == FileType.REGULAR) {
                  // If there is not parentDirPath restriction or path is under parentDirPath
                  if (parentDirPath == null || (isPathUnderParentDirPath && getFileType(parentDirPath, false) == FileType.DIRECTORY)) {
                      if (setMissingPermissionsOnly)
                          setMissingFilePermissions(label + "file", filePath, permissionsToCheck);
                      else
                          setFilePermissions(label + "file", filePath, permissionsToCheck);
                  }
              }
  
              // If path is not a regular file
              // Regular files cannot be automatically created so we do not ignore if missing
              if (fileType != FileType.REGULAR) {
                  label += "regular file";
                  return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
              }
  
              // If there is not parentDirPath restriction or path is not under parentDirPath or
              // if permission errors must not be ignored for paths under parentDirPath
              if (parentDirPath == null || !isPathUnderParentDirPath || !ignoreErrorsIfPathIsUnderParentDirPath) {
                  if (permissionsToCheck != null) {
                      // Check if permissions are missing
                      return checkMissingFilePermissions(label + "regular", filePath, permissionsToCheck, false);
                  }
              }
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
          }
  
          return null;
  
      }
  
      /**
       * Validate the existence and permissions of directory file at path.
       *
       * If the {@code parentDirPath} is not {@code null}, then creation of missing directory and
       * setting of missing permissions will only be done if {@code path} is under
       * {@code parentDirPath} or equals {@code parentDirPath}.
       *
       * @param label The optional label for the directory file. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to validate or create. Symlinks will not be followed.
       * @param parentDirPath The optional {@code parent directory path} to restrict operations to.
       *                      This can optionally be {@code null}. It is not canonicalized and only normalized.
       * @param createDirectoryIfMissing The {@code boolean} that decides if directory file
       *                                 should be created if its missing.
       * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
       * @param setPermissions The {@code boolean} that decides if permissions are to be
       *                              automatically set defined by {@code permissionsToCheck}.
       * @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
       *                                  are to be set or if they should be overridden.
       * @param ignoreErrorsIfPathIsInParentDirPath The {@code boolean} that decides if existence
       *                                  and permission errors are to be ignored if path is
       *                                  in {@code parentDirPath}.
       * @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
       *                              error is to be ignored. This allows making an attempt to set
       *                              executable permissions, but ignoring if it fails.
       * @return Returns the {@code error} if path is not a directory file, failed to create it,
       * or validating permissions failed, otherwise {@code null}.
       */
      public static Error validateDirectoryFileExistenceAndPermissions(String label, final String filePath, final String parentDirPath, final boolean createDirectoryIfMissing,
                                                                       final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly,
                                                                       final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "directory file path", "validateDirectoryExistenceAndPermissions");
  
          try {
              File file = new File(filePath);
              FileType fileType = getFileType(filePath, false);
  
              // If file exists but not a directory file
              if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
                  return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
              }
  
              boolean isPathInParentDirPath = false;
              if (parentDirPath != null) {
                  // The path can be equal to parent directory path or under it
                  isPathInParentDirPath = isPathInDirPath(filePath, parentDirPath, false);
              }
  
              if (createDirectoryIfMissing || setPermissions) {
                  // If there is not parentDirPath restriction or path is in parentDirPath
                  if (parentDirPath == null || (isPathInParentDirPath && getFileType(parentDirPath, false) == FileType.DIRECTORY)) {
                      // If createDirectoryIfMissing is enabled and no file exists at path, then create directory
                      if (createDirectoryIfMissing && fileType == FileType.NO_EXIST) {
                          Logger.logVerbose(LOG_TAG, "Creating " + label + "directory file at path \"" + filePath + "\"");
                          // Create directory and update fileType if successful, otherwise return with error
                          // It "might" be possible that mkdirs returns false even though directory was created
                          boolean result = file.mkdirs();
                          fileType = getFileType(filePath, false);
                          if (!result && fileType != FileType.DIRECTORY)
                              return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED.getError(label + "directory file", filePath);
                      }
  
                      // If setPermissions is enabled and path is a directory
                      if (setPermissions && permissionsToCheck != null && fileType == FileType.DIRECTORY) {
                          if (setMissingPermissionsOnly)
                              setMissingFilePermissions(label + "directory", filePath, permissionsToCheck);
                          else
                              setFilePermissions(label + "directory", filePath, permissionsToCheck);
                      }
                  }
              }
  
              // If there is not parentDirPath restriction or path is not in parentDirPath or
              // if existence or permission errors must not be ignored for paths in parentDirPath
              if (parentDirPath == null || !isPathInParentDirPath || !ignoreErrorsIfPathIsInParentDirPath) {
                  // If path is not a directory
                  // Directories can be automatically created so we can ignore if missing with above check
                  if (fileType != FileType.DIRECTORY) {
                      label += "directory";
                      return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
                  }
  
                  if (permissionsToCheck != null) {
                      // Check if permissions are missing
                      return checkMissingFilePermissions(label + "directory", filePath, permissionsToCheck, ignoreIfNotExecutable);
                  }
              }
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION.getError(e, label + "directory file", filePath, e.getMessage());
          }
  
          return null;
      }
  
  
  
      /**
       * Create a regular file at path.
       *
       * This function is a wrapper for
       * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
       *
       * @param filePath The {@code path} for regular file to create.
       * @return Returns the {@code error} if path is not a regular file or failed to create it,
       * otherwise {@code null}.
       */
      public static Error createRegularFile(final String filePath) {
          return createRegularFile(null, filePath);
      }
  
      /**
       * Create a regular file at path.
       *
       * This function is a wrapper for
       * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
       *
       * @param label The optional label for the regular file. This can optionally be {@code null}.
       * @param filePath The {@code path} for regular file to create.
       * @return Returns the {@code error} if path is not a regular file or failed to create it,
       * otherwise {@code null}.
       */
      public static Error createRegularFile(final String label, final String filePath) {
          return createRegularFile(label, filePath,
              null, false, false);
      }
  
      /**
       * Create a regular file at path.
       *
       * This function is a wrapper for
       * {@link #validateRegularFileExistenceAndPermissions(String, String, String, String, boolean, boolean, boolean)}.
       *
       * @param label The optional label for the regular file. This can optionally be {@code null}.
       * @param filePath The {@code path} for regular file to create.
       * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
       * @param setPermissions The {@code boolean} that decides if permissions are to be
       *                              automatically set defined by {@code permissionsToCheck}.
       * @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
       *                                  are to be set or if they should be overridden.
       * @return Returns the {@code error} if path is not a regular file, failed to create it,
       * or validating permissions failed, otherwise {@code null}.
       */
      public static Error createRegularFile(String label, final String filePath,
                                            final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "createRegularFile");
  
          Error error;
  
          File file = new File(filePath);
          FileType fileType = getFileType(filePath, false);
  
          // If file exists but not a regular file
          if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
              return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
          }
  
          // If regular file already exists
          if (fileType == FileType.REGULAR) {
              return null;
          }
  
          // Create the file parent directory
          error = createParentDirectoryFile(label + "regular file parent", filePath);
          if (error != null)
              return error;
  
          try {
              Logger.logVerbose(LOG_TAG, "Creating " + label + "regular file at path \"" + filePath + "\"");
  
              if (!file.createNewFile())
                  return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED.getError(label + "regular file", filePath);
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "regular file", filePath, e.getMessage());
          }
  
          return validateRegularFileExistenceAndPermissions(label, filePath,
              null,
              permissionsToCheck, setPermissions, setMissingPermissionsOnly,
              false);
      }
  
  
  
      /**
       * Create parent directory of file at path.
       *
       * This function is a wrapper for
       * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
       *
       * @param label The optional label for the parent directory file. This can optionally be {@code null}.
       * @param filePath The {@code path} for file whose parent needs to be created.
       * @return Returns the {@code error} if parent path is not a directory file or failed to create it,
       * otherwise {@code null}.
       */
      public static Error createParentDirectoryFile(final String label, final String filePath) {
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "createParentDirectoryFile");
  
          File file = new File(filePath);
          String fileParentPath = file.getParent();
  
          if (fileParentPath != null)
              return createDirectoryFile(label, fileParentPath,
                  null, false, false);
          else
              return null;
      }
  
      /**
       * Create a directory file at path.
       *
       * This function is a wrapper for
       * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
       *
       * @param filePath The {@code path} for directory file to create.
       * @return Returns the {@code error} if path is not a directory file or failed to create it,
       * otherwise {@code null}.
       */
      public static Error createDirectoryFile(final String filePath) {
          return createDirectoryFile(null, filePath);
      }
  
      /**
       * Create a directory file at path.
       *
       * This function is a wrapper for
       * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
       *
       * @param label The optional label for the directory file. This can optionally be {@code null}.
       * @param filePath The {@code path} for directory file to create.
       * @return Returns the {@code error} if path is not a directory file or failed to create it,
       * otherwise {@code null}.
       */
      public static Error createDirectoryFile(final String label, final String filePath) {
          return createDirectoryFile(label, filePath,
              null, false, false);
      }
  
      /**
       * Create a directory file at path.
       *
       * This function is a wrapper for
       * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
       *
       * @param label The optional label for the directory file. This can optionally be {@code null}.
       * @param filePath The {@code path} for directory file to create.
       * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
       * @param setPermissions The {@code boolean} that decides if permissions are to be
       *                              automatically set defined by {@code permissionsToCheck}.
       * @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
       *                                  are to be set or if they should be overridden.
       * @return Returns the {@code error} if path is not a directory file, failed to create it,
       * or validating permissions failed, otherwise {@code null}.
       */
      public static Error createDirectoryFile(final String label, final String filePath,
                                              final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly) {
          return validateDirectoryFileExistenceAndPermissions(label, filePath,
              null, true,
              permissionsToCheck, setPermissions, setMissingPermissionsOnly,
              false, false);
      }
  
  
  
      /**
       * Create a symlink file at path.
       *
       * This function is a wrapper for
       * {@link #createSymlinkFile(String, String, String, boolean, boolean, boolean)}.
       *
       * Dangling symlinks will be allowed.
       * Symlink destination will be overwritten if it already exists but only if its a symlink.
       *
       * @param targetFilePath The {@code path} TO which the symlink file will be created.
       * @param destFilePath The {@code path} AT which the symlink file will be created.
       * @return Returns the {@code error} if path is not a symlink file, failed to create it,
       * otherwise {@code null}.
       */
      public static Error createSymlinkFile(final String targetFilePath, final String destFilePath) {
          return createSymlinkFile(null, targetFilePath, destFilePath,
              true, true, true);
      }
  
      /**
       * Create a symlink file at path.
       *
       * This function is a wrapper for
       * {@link #createSymlinkFile(String, String, String, boolean, boolean, boolean)}.
       *
       * Dangling symlinks will be allowed.
       * Symlink destination will be overwritten if it already exists but only if its a symlink.
       *
       * @param label The optional label for the symlink file. This can optionally be {@code null}.
       * @param targetFilePath The {@code path} TO which the symlink file will be created.
       * @param destFilePath The {@code path} AT which the symlink file will be created.
       * @return Returns the {@code error} if path is not a symlink file, failed to create it,
       * otherwise {@code null}.
       */
      public static Error createSymlinkFile(String label, final String targetFilePath, final String destFilePath) {
          return createSymlinkFile(label, targetFilePath, destFilePath,
              true, true, true);
      }
  
      /**
       * Create a symlink file at path.
       *
       * @param label The optional label for the symlink file. This can optionally be {@code null}.
       * @param targetFilePath The {@code path} TO which the symlink file will be created.
       * @param destFilePath The {@code path} AT which the symlink file will be created.
       * @param allowDangling The {@code boolean} that decides if it should be considered an
       *                              error if source file doesn't exist.
       * @param overwrite The {@code boolean} that decides if destination file should be overwritten if
       *                  it already exists. If set to {@code true}, then destination file will be
       *                  deleted before symlink is created.
       * @param overwriteOnlyIfDestIsASymlink The {@code boolean} that decides if overwrite should
       *                                         only be done if destination file is also a symlink.
       * @return Returns the {@code error} if path is not a symlink file, failed to create it,
       * or validating permissions failed, otherwise {@code null}.
       */
      public static Error createSymlinkFile(String label, final String targetFilePath, final String destFilePath,
                                            final boolean allowDangling, final boolean overwrite, final boolean overwriteOnlyIfDestIsASymlink) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (targetFilePath == null || targetFilePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "target file path", "createSymlinkFile");
          if (destFilePath == null || destFilePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "destination file path", "createSymlinkFile");
  
          Error error;
  
          try {
              File destFile = new File(destFilePath);
  
              String targetFileAbsolutePath = targetFilePath;
              // If target path is relative instead of absolute
              if (!targetFilePath.startsWith("/")) {
                  String destFileParentPath = destFile.getParent();
                  if (destFileParentPath != null)
                      targetFileAbsolutePath = destFileParentPath + "/" +  targetFilePath;
              }
  
              FileType targetFileType = getFileType(targetFileAbsolutePath, false);
              FileType destFileType = getFileType(destFilePath, false);
  
              // If target file does not exist
              if (targetFileType == FileType.NO_EXIST) {
                  // If dangling symlink should not be allowed, then return with error
                  if (!allowDangling) {
                      label += "symlink target file";
                      return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, targetFileAbsolutePath).setLabel(label);
                  }
              }
  
              // If destination exists
              if (destFileType != FileType.NO_EXIST) {
                  // If destination must not be overwritten
                  if (!overwrite) {
                      return null;
                  }
  
                  // If overwriteOnlyIfDestIsASymlink is enabled but destination file is not a symlink
                  if (overwriteOnlyIfDestIsASymlink && destFileType != FileType.SYMLINK)
                      return FileUtilsErrno.ERRNO_CANNOT_OVERWRITE_A_NON_SYMLINK_FILE_TYPE.getError(label + " file", destFilePath, targetFilePath, destFileType.getName());
  
                  // Delete the destination file
                  error = deleteFile(label + "symlink destination", destFilePath, true);
                  if (error != null)
                      return error;
              } else {
                  // Create the destination file parent directory
                  error = createParentDirectoryFile(label + "symlink destination file parent", destFilePath);
                  if (error != null)
                      return error;
              }
  
              // create a symlink at destFilePath to targetFilePath
              Logger.logVerbose(LOG_TAG, "Creating " + label + "symlink file at path \"" + destFilePath + "\" to \"" + targetFilePath + "\"");
              Os.symlink(targetFilePath, destFilePath);
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_CREATING_SYMLINK_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "symlink file", destFilePath, targetFilePath, e.getMessage());
          }
  
          return null;
      }
  
  
  
      /**
       * Copy a regular file from {@code sourceFilePath} to {@code destFilePath}.
       *
       * This function is a wrapper for
       * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
       *
       * If destination file already exists, then it will be overwritten, but only if its a regular
       * file, otherwise an error will be returned.
       *
       * @param label The optional label for file to copy. This can optionally be {@code null}.
       * @param srcFilePath The {@code source path} for file to copy.
       * @param destFilePath The {@code destination path} for file to copy.
       * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
       *                              error if source file to copied doesn't exist.
       * @return Returns the {@code error} if copy was not successful, otherwise {@code null}.
       */
      public static Error copyRegularFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
          return copyOrMoveFile(label, srcFilePath, destFilePath,
              false, ignoreNonExistentSrcFile, FileType.REGULAR.getValue(),
              true, true);
      }
  
      /**
       * Move a regular file from {@code sourceFilePath} to {@code destFilePath}.
       *
       * This function is a wrapper for
       * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
       *
       * If destination file already exists, then it will be overwritten, but only if its a regular
       * file, otherwise an error will be returned.
       *
       * @param label The optional label for file to move. This can optionally be {@code null}.
       * @param srcFilePath The {@code source path} for file to move.
       * @param destFilePath The {@code destination path} for file to move.
       * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
       *                              error if source file to moved doesn't exist.
       * @return Returns the {@code error} if move was not successful, otherwise {@code null}.
       */
      public static Error moveRegularFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
          return copyOrMoveFile(label, srcFilePath, destFilePath,
              true, ignoreNonExistentSrcFile, FileType.REGULAR.getValue(),
              true, true);
      }
  
      /**
       * Copy a directory file from {@code sourceFilePath} to {@code destFilePath}.
       *
       * This function is a wrapper for
       * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
       *
       * If destination file already exists, then it will be overwritten, but only if its a directory
       * file, otherwise an error will be returned.
       *
       * @param label The optional label for file to copy. This can optionally be {@code null}.
       * @param srcFilePath The {@code source path} for file to copy.
       * @param destFilePath The {@code destination path} for file to copy.
       * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
       *                              error if source file to copied doesn't exist.
       * @return Returns the {@code error} if copy was not successful, otherwise {@code null}.
       */
      public static Error copyDirectoryFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
          return copyOrMoveFile(label, srcFilePath, destFilePath,
              false, ignoreNonExistentSrcFile, FileType.DIRECTORY.getValue(),
              true, true);
      }
  
      /**
       * Move a directory file from {@code sourceFilePath} to {@code destFilePath}.
       *
       * This function is a wrapper for
       * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
       *
       * If destination file already exists, then it will be overwritten, but only if its a directory
       * file, otherwise an error will be returned.
       *
       * @param label The optional label for file to move. This can optionally be {@code null}.
       * @param srcFilePath The {@code source path} for file to move.
       * @param destFilePath The {@code destination path} for file to move.
       * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
       *                              error if source file to moved doesn't exist.
       * @return Returns the {@code error} if move was not successful, otherwise {@code null}.
       */
      public static Error moveDirectoryFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
          return copyOrMoveFile(label, srcFilePath, destFilePath,
              true, ignoreNonExistentSrcFile, FileType.DIRECTORY.getValue(),
              true, true);
      }
  
      /**
       * Copy a symlink file from {@code sourceFilePath} to {@code destFilePath}.
       *
       * This function is a wrapper for
       * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
       *
       * If destination file already exists, then it will be overwritten, but only if its a symlink
       * file, otherwise an error will be returned.
       *
       * @param label The optional label for file to copy. This can optionally be {@code null}.
       * @param srcFilePath The {@code source path} for file to copy.
       * @param destFilePath The {@code destination path} for file to copy.
       * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
       *                              error if source file to copied doesn't exist.
       * @return Returns the {@code error} if copy was not successful, otherwise {@code null}.
       */
      public static Error copySymlinkFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
          return copyOrMoveFile(label, srcFilePath, destFilePath,
              false, ignoreNonExistentSrcFile, FileType.SYMLINK.getValue(),
              true, true);
      }
  
      /**
       * Move a symlink file from {@code sourceFilePath} to {@code destFilePath}.
       *
       * This function is a wrapper for
       * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
       *
       * If destination file already exists, then it will be overwritten, but only if its a symlink
       * file, otherwise an error will be returned.
       *
       * @param label The optional label for file to move. This can optionally be {@code null}.
       * @param srcFilePath The {@code source path} for file to move.
       * @param destFilePath The {@code destination path} for file to move.
       * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
       *                              error if source file to moved doesn't exist.
       * @return Returns the {@code error} if move was not successful, otherwise {@code null}.
       */
      public static Error moveSymlinkFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
          return copyOrMoveFile(label, srcFilePath, destFilePath,
              true, ignoreNonExistentSrcFile, FileType.SYMLINK.getValue(),
              true, true);
      }
  
      /**
       * Copy a file from {@code sourceFilePath} to {@code destFilePath}.
       *
       * This function is a wrapper for
       * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
       *
       * If destination file already exists, then it will be overwritten, but only if its the same file
       * type as the source, otherwise an error will be returned.
       *
       * @param label The optional label for file to copy. This can optionally be {@code null}.
       * @param srcFilePath The {@code source path} for file to copy.
       * @param destFilePath The {@code destination path} for file to copy.
       * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
       *                              error if source file to copied doesn't exist.
       * @return Returns the {@code error} if copy was not successful, otherwise {@code null}.
       */
      public static Error copyFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
          return copyOrMoveFile(label, srcFilePath, destFilePath,
              false, ignoreNonExistentSrcFile, FileTypes.FILE_TYPE_NORMAL_FLAGS,
              true, true);
      }
  
      /**
       * Move a file from {@code sourceFilePath} to {@code destFilePath}.
       *
       * This function is a wrapper for
       * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
       *
       * If destination file already exists, then it will be overwritten, but only if its the same file
       * type as the source, otherwise an error will be returned.
       *
       * @param label The optional label for file to move. This can optionally be {@code null}.
       * @param srcFilePath The {@code source path} for file to move.
       * @param destFilePath The {@code destination path} for file to move.
       * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
       *                              error if source file to moved doesn't exist.
       * @return Returns the {@code error} if move was not successful, otherwise {@code null}.
       */
      public static Error moveFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
          return copyOrMoveFile(label, srcFilePath, destFilePath,
              true, ignoreNonExistentSrcFile, FileTypes.FILE_TYPE_NORMAL_FLAGS,
              true, true);
      }
  
      /**
       * Copy or move a file from {@code sourceFilePath} to {@code destFilePath}.
       *
       * The {@code sourceFilePath} and {@code destFilePath} must be the canonical path to the source
       * and destination since symlinks will not be followed.
       *
       * If the {@code sourceFilePath} or {@code destFilePath} is a canonical path to a directory,
       * then any symlink files found under the directory will be deleted, but not their targets when
       * deleting source after move and deleting destination before copy/move.
       *
       * @param label The optional label for file to copy or move. This can optionally be {@code null}.
       * @param srcFilePath The {@code source path} for file to copy or move.
       * @param destFilePath The {@code destination path} for file to copy or move.
       * @param moveFile The {@code boolean} that decides if source file needs to be copied or moved.
       *                 If set to {@code true}, then source file will be moved, otherwise it will be
       *                 copied.
       * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
       *                              error if source file to copied or moved doesn't exist.
       * @param allowedFileTypeFlags The flags that are matched against the source file's {@link FileType}
       *                             to see if it should be copied/moved or not. This is a safety measure
       *                             to prevent accidental copy/move/delete of the wrong type of file,
       *                             like a directory instead of a regular file. You can pass
       *                             {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow copy/move of any file type.
       * @param overwrite The {@code boolean} that decides if destination file should be overwritten if
       *                  it already exists. If set to {@code true}, then destination file will be
       *                  deleted before source is copied or moved.
       * @param overwriteOnlyIfDestSameFileTypeAsSrc The {@code boolean} that decides if overwrite should
       *                                         only be done if destination file is also the same file
       *                                          type as the source file.
       * @return Returns the {@code error} if copy or move was not successful, otherwise {@code null}.
       */
      public static Error copyOrMoveFile(String label, final String srcFilePath, final String destFilePath,
                                         final boolean moveFile, final boolean ignoreNonExistentSrcFile, int allowedFileTypeFlags,
                                         final boolean overwrite, final boolean overwriteOnlyIfDestSameFileTypeAsSrc) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (srcFilePath == null || srcFilePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "source file path", "copyOrMoveFile");
          if (destFilePath == null || destFilePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "destination file path", "copyOrMoveFile");
  
          String mode = (moveFile ? "Moving" : "Copying");
          String modePast = (moveFile ? "moved" : "copied");
  
          Error error;
  
          try {
              Logger.logVerbose(LOG_TAG, mode + " " + label + "source file from \"" + srcFilePath + "\" to destination \"" + destFilePath + "\"");
  
              File srcFile = new File(srcFilePath);
              File destFile = new File(destFilePath);
  
              FileType srcFileType = getFileType(srcFilePath, false);
              FileType destFileType = getFileType(destFilePath, false);
  
              String srcFileCanonicalPath = srcFile.getCanonicalPath();
              String destFileCanonicalPath = destFile.getCanonicalPath();
  
              // If source file does not exist
              if (srcFileType == FileType.NO_EXIST) {
                  // If copy or move is to be ignored if source file is not found
                  if (ignoreNonExistentSrcFile)
                      return null;
                      // Else return with error
                  else {
                      label += "source file";
                      return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, srcFilePath).setLabel(label);
                  }
              }
  
              // If the file type of the source file does not exist in the allowedFileTypeFlags, then return with error
              if ((allowedFileTypeFlags & srcFileType.getValue()) <= 0)
                  return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "source file meant to be " + modePast, srcFilePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
  
              // If source and destination file path are the same
              if (srcFileCanonicalPath.equals(destFileCanonicalPath))
                  return FileUtilsErrno.ERRNO_COPYING_OR_MOVING_FILE_TO_SAME_PATH.getError(mode + " " + label + "source file", srcFilePath, destFilePath);
  
              // If destination exists
              if (destFileType != FileType.NO_EXIST) {
                  // If destination must not be overwritten
                  if (!overwrite) {
                      return null;
                  }
  
                  // If overwriteOnlyIfDestSameFileTypeAsSrc is enabled but destination file does not match source file type
                  if (overwriteOnlyIfDestSameFileTypeAsSrc && destFileType != srcFileType)
                      return FileUtilsErrno.ERRNO_CANNOT_OVERWRITE_A_DIFFERENT_FILE_TYPE.getError(label + "source file", mode.toLowerCase(), srcFilePath, destFilePath, destFileType.getName(), srcFileType.getName());
  
                  // Delete the destination file
                  error = deleteFile(label + "destination", destFilePath, true);
                  if (error != null)
                      return error;
              }
  
  
              // Copy or move source file to dest
              boolean copyFile = !moveFile;
  
              // If moveFile is true
              if (moveFile) {
                  // We first try to rename source file to destination file to save a copy operation in case both source and destination are on the same filesystem
                  Logger.logVerbose(LOG_TAG, "Attempting to rename source to destination.");
  
                  // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/io/UnixFileSystem.java;l=358
                  // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/luni/src/main/java/android/system/Os.java;l=512
                  // Uses File.getPath() to get the path of source and destination and not the canonical path
                  if (!srcFile.renameTo(destFile)) {
                      // If destination directory is a subdirectory of the source directory
                      // Copying is still allowed by copyDirectory() by excluding destination directory files
                      if (srcFileType == FileType.DIRECTORY && destFileCanonicalPath.startsWith(srcFileCanonicalPath + File.separator))
                          return FileUtilsErrno.ERRNO_CANNOT_MOVE_DIRECTORY_TO_SUB_DIRECTORY_OF_ITSELF.getError(label + "source directory", srcFilePath, destFilePath);
  
                      // If rename failed, then we copy
                      Logger.logVerbose(LOG_TAG, "Renaming " + label + "source file to destination failed, attempting to copy.");
                      copyFile = true;
                  }
              }
  
              // If moveFile is false or renameTo failed while moving
              if (copyFile) {
                  Logger.logVerbose(LOG_TAG, "Attempting to copy source to destination.");
  
                  // Create the dest file parent directory
                  error = createParentDirectoryFile(label + "dest file parent", destFilePath);
                  if (error != null)
                      return error;
  
                  if (srcFileType == FileType.DIRECTORY) {
                      // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
                      org.apache.commons.io.FileUtils.copyDirectory(srcFile, destFile, true);
                  } else if (srcFileType == FileType.SYMLINK) {
                      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                          java.nio.file.Files.copy(srcFile.toPath(), destFile.toPath(), LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING);
                      } else {
                          // read the target for the source file and create a symlink at dest
                          // source file metadata will be lost
                          error = createSymlinkFile(label + "dest", Os.readlink(srcFilePath), destFilePath);
                          if (error != null)
                              return error;
                      }
                  } else {
                      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                          java.nio.file.Files.copy(srcFile.toPath(), destFile.toPath(), LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING);
                      } else {
                          // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
                          org.apache.commons.io.FileUtils.copyFile(srcFile, destFile, true);
                      }
                  }
              }
  
              // If source file had to be moved
              if (moveFile) {
                  // Delete the source file since copying would have succeeded
                  error = deleteFile(label + "source", srcFilePath, true);
                  if (error != null)
                      return error;
              }
  
              Logger.logVerbose(LOG_TAG, mode + " successful.");
          }
          catch (Exception e) {
              return FileUtilsErrno.ERRNO_COPYING_OR_MOVING_FILE_FAILED_WITH_EXCEPTION.getError(e, mode + " " + label + "file", srcFilePath, destFilePath, e.getMessage());
          }
  
          return null;
      }
  
  
  
      /**
       * Delete regular file at path.
       *
       * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
       *
       * @param label The optional label for file to delete. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to delete.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to deleted doesn't exist.
       * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
       */
      public static Error deleteRegularFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
          return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.REGULAR.getValue());
      }
  
      /**
       * Delete directory file at path.
       *
       * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
       *
       * @param label The optional label for file to delete. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to delete.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to deleted doesn't exist.
       * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
       */
      public static Error deleteDirectoryFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
          return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.DIRECTORY.getValue());
      }
  
      /**
       * Delete symlink file at path.
       *
       * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
       *
       * @param label The optional label for file to delete. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to delete.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to deleted doesn't exist.
       * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
       */
      public static Error deleteSymlinkFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
          return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.SYMLINK.getValue());
      }
  
      /**
       * Delete socket file at path.
       *
       * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
       *
       * @param label The optional label for file to delete. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to delete.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to deleted doesn't exist.
       * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
       */
      public static Error deleteSocketFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
          return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.SOCKET.getValue());
      }
  
      /**
       * Delete regular, directory or symlink file at path.
       *
       * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
       *
       * @param label The optional label for file to delete. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to delete.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to deleted doesn't exist.
       * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
       */
      public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
          return deleteFile(label, filePath, ignoreNonExistentFile, false, FileTypes.FILE_TYPE_NORMAL_FLAGS);
      }
  
      /**
       * Delete file at path.
       *
       * The {@code filePath} must be the canonical path to the file to be deleted since symlinks will
       * not be followed.
       * If the {@code filePath} is a canonical path to a directory, then any symlink files found under
       * the directory will be deleted, but not their targets.
       *
       * @param label The optional label for file to delete. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to delete.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to deleted doesn't exist.
       * @param ignoreWrongFileType The {@code boolean} that decides if it should be considered an
       *                              error if file type is not one from {@code allowedFileTypeFlags}.
       * @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to
       *                             see if it should be deleted or not. This is a safety measure to
       *                             prevent accidental deletion of the wrong type of file, like a
       *                             directory instead of a regular file. You can pass
       *                             {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
       * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
       */
      public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile, final boolean ignoreWrongFileType, int allowedFileTypeFlags) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFile");
  
          try {
              File file = new File(filePath);
              FileType fileType = getFileType(filePath, false);
  
              Logger.logVerbose(LOG_TAG, "Processing delete of " + label + "file at path \"" + filePath + "\" of type \"" + fileType.getName() + "\"");
  
              // If file does not exist
              if (fileType == FileType.NO_EXIST) {
                  // If delete is to be ignored if file does not exist
                  if (ignoreNonExistentFile)
                      return null;
                      // Else return with error
                  else {
                      label += "file meant to be deleted";
                      return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
                  }
              }
  
              // If the file type of the file does not exist in the allowedFileTypeFlags
              if ((allowedFileTypeFlags & fileType.getValue()) <= 0) {
                  // If wrong file type is to be ignored
                  if (ignoreWrongFileType) {
                      Logger.logVerbose(LOG_TAG, "Ignoring deletion of " + label + "file at path \"" + filePath + "\" of type \"" + fileType.getName() + "\" not matching allowed file types: " + FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
                      return null;
                  }
  
                  // Else return with error
                  return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "file meant to be deleted", filePath, fileType.getName(), FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
              }
  
              Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\"");
  
              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                  /*
                   * Try to use {@link SecureDirectoryStream} if available for safer directory
                   * deletion, it should be available for android >= 8.0
                   * https://guava.dev/releases/24.1-jre/api/docs/com/google/common/io/MoreFiles.html#deleteRecursively-java.nio.file.Path-com.google.common.io.RecursiveDeleteOption...-
                   * https://github.com/google/guava/issues/365
                   * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixSecureDirectoryStream.java
                   *
                   * MoreUtils is marked with the @Beta annotation so the API may be removed in
                   * future but has been there for a few years now.
                   *
                   * If an exception is thrown, the exception message might not contain the full errors.
                   * Individual failures get added to suppressed throwables which can be extracted
                   * from the exception object by calling `Throwable[] getSuppressed()`. So just logging
                   * the exception message and stacktrace may not be enough, the suppressed throwables
                   * need to be logged as well, which the Logger class does if they are found in the
                   * exception added to the Error that's returned by this function.
                   * https://github.com/google/guava/blob/v30.1.1/guava/src/com/google/common/io/MoreFiles.java#L775
                   */
                  //noinspection UnstableApiUsage
                  com.google.common.io.MoreFiles.deleteRecursively(file.toPath(), RecursiveDeleteOption.ALLOW_INSECURE);
              } else {
                  if (fileType == FileType.DIRECTORY) {
                      // deleteDirectory() instead of forceDelete() gets the files list first instead of walking directory tree, so seems safer
                      // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
                      org.apache.commons.io.FileUtils.deleteDirectory(file);
                  } else {
                      // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
                      org.apache.commons.io.FileUtils.forceDelete(file);
                  }
              }
  
              // If file still exists after deleting it
              fileType = getFileType(filePath, false);
              if (fileType != FileType.NO_EXIST)
                  return FileUtilsErrno.ERRNO_FILE_STILL_EXISTS_AFTER_DELETING.getError(label + "file meant to be deleted", filePath);
          }
          catch (Exception e) {
              return FileUtilsErrno.ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
          }
  
          return null;
      }
  
  
  
      /**
       * Clear contents of directory at path without deleting the directory. If directory does not exist
       * it will be created automatically.
       *
       * This function is a wrapper for
       * {@link #clearDirectory(String, String)}.
       *
       * @param filePath The {@code path} for directory to clear.
       * @return Returns the {@code error} if clearing was not successful, otherwise {@code null}.
       */
      public static Error clearDirectory(String filePath) {
          return clearDirectory(null, filePath);
      }
  
      /**
       * Clear contents of directory at path without deleting the directory. If directory does not exist
       * it will be created automatically.
       *
       * The {@code filePath} must be the canonical path to a directory since symlinks will not be followed.
       * Any symlink files found under the directory will be deleted, but not their targets.
       *
       * @param label The optional label for directory to clear. This can optionally be {@code null}.
       * @param filePath The {@code path} for directory to clear.
       * @return Returns the {@code error} if clearing was not successful, otherwise {@code null}.
       */
      public static Error clearDirectory(String label, final String filePath) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "clearDirectory");
  
          Error error;
  
          try {
              Logger.logVerbose(LOG_TAG, "Clearing " + label + "directory at path \"" + filePath + "\"");
  
              File file = new File(filePath);
              FileType fileType = getFileType(filePath, false);
  
              // If file exists but not a directory file
              if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
                  return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
              }
  
              // If directory exists, clear its contents
              if (fileType == FileType.DIRECTORY) {
                  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                      /* If an exception is thrown, the exception message might not contain the full errors.
                       * Individual failures get added to suppressed throwables. */
                      //noinspection UnstableApiUsage
                      com.google.common.io.MoreFiles.deleteDirectoryContents(file.toPath(), RecursiveDeleteOption.ALLOW_INSECURE);
                  } else {
                      // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
                      org.apache.commons.io.FileUtils.cleanDirectory(new File(filePath));
                  }
              }
              // Else create it
              else {
                  error = createDirectoryFile(label, filePath);
                  if (error != null)
                      return error;
              }
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, e.getMessage());
          }
  
          return null;
      }
  
      /**
       * Delete files under a directory older than x days.
       *
       * The {@code filePath} must be the canonical path to a directory since symlinks will not be followed.
       * Any symlink files found under the directory will be deleted, but not their targets.
       *
       * @param label The optional label for directory to clear. This can optionally be {@code null}.
       * @param filePath The {@code path} for directory to clear.
       * @param dirFilter  The optional filter to apply when finding subdirectories.
       *                   If this parameter is {@code null}, subdirectories will not be included in the
       *                   search. Use TrueFileFilter.INSTANCE to match all directories.
       * @param days The x amount of days before which files should be deleted. This must be `>=0`.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to deleted doesn't exist.
       * @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to
       *                             see if it should be deleted or not. This is a safety measure to
       *                             prevent accidental deletion of the wrong type of file, like a
       *                             directory instead of a regular file. You can pass
       *                             {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
       * @return Returns the {@code error} if deleting was not successful, otherwise {@code null}.
       */
      public static Error deleteFilesOlderThanXDays(String label, final String filePath, final IOFileFilter dirFilter, int days, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFilesOlderThanXDays");
          if (days < 0) return FunctionErrno.ERRNO_INVALID_PARAMETER.getError(label + "days", "deleteFilesOlderThanXDays", " It must be >= 0.");
  
          Error error;
  
          try {
              Logger.logVerbose(LOG_TAG, "Deleting files under " + label + "directory at path \"" + filePath + "\" older than " + days + " days");
  
              File file = new File(filePath);
              FileType fileType = getFileType(filePath, false);
  
              // If file exists but not a directory file
              if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
                  return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
              }
  
              // If file does not exist
              if (fileType == FileType.NO_EXIST) {
                  // If delete is to be ignored if file does not exist
                  if (ignoreNonExistentFile)
                      return null;
                      // Else return with error
                  else {
                      label += "directory under which files had to be deleted";
                      return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
                  }
              }
  
              // TODO: Use FileAttributes with support for atime (default), mtime, ctime. Add regex for ignoring file and dir absolute paths.
              // FIXME: iterateFiles() does not return subdirectories even with TrueFileFilter for file and dir.
              // FIXME: Empty directories remain
  
              // If directory exists, delete its contents
              Calendar calendar = Calendar.getInstance();
              calendar.add(Calendar.DATE, -(days));
              // AgeFileFilter seems to apply to symlink destination timestamp instead of symlink file itself
              Iterator<File> filesToDelete =
                  org.apache.commons.io.FileUtils.iterateFiles(file, new AgeFileFilter(calendar.getTime()), dirFilter);
              while (filesToDelete.hasNext()) {
                  File subFile = filesToDelete.next();
                  error = deleteFile(label + " directory sub", subFile.getAbsolutePath(), true, true, allowedFileTypeFlags);
                  if (error != null)
                      return error;
              }
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, days, e.getMessage());
          }
  
          return null;
  
      }
  
  
  
  
  
      /**
       * Read a text {@link String} from file at path with a specific {@link Charset} into {@code dataString}.
       *
       * @param label The optional label for file to read. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to read.
       * @param charset The {@link Charset} of the file. If this is {@code null},
       *                then default {@link Charset} will be used.
       * @param dataStringBuilder The {@code StringBuilder} to read data into.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to read doesn't exist.
       * @return Returns the {@code error} if reading was not successful, otherwise {@code null}.
       */
      public static Error readTextFromFile(String label, final String filePath, Charset charset, @NonNull final StringBuilder dataStringBuilder, final boolean ignoreNonExistentFile) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "readStringFromFile");
  
          Logger.logVerbose(LOG_TAG, "Reading text from " + label + "file at path \"" + filePath + "\"");
  
          Error error;
  
          FileType fileType = getFileType(filePath, false);
  
          // If file exists but not a regular file
          if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
              return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
          }
  
          // If file does not exist
          if (fileType == FileType.NO_EXIST) {
              // If reading is to be ignored if file does not exist
              if (ignoreNonExistentFile)
                  return null;
                  // Else return with error
              else {
                  label += "file meant to be read";
                  return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
              }
          }
  
          if (charset == null) charset = Charset.defaultCharset();
  
          // Check if charset is supported
          error = isCharsetSupported(charset);
          if (error != null)
              return error;
  
          FileInputStream fileInputStream = null;
          BufferedReader bufferedReader = null;
          try {
              // Read text from file
              fileInputStream = new FileInputStream(filePath);
              bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream, charset));
  
              String receiveString;
  
              boolean firstLine = true;
              while ((receiveString = bufferedReader.readLine()) != null ) {
                  if (!firstLine) dataStringBuilder.append("\n"); else firstLine = false;
                  dataStringBuilder.append(receiveString);
              }
  
              Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-"));
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_READING_TEXT_FROM_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
          } finally {
              closeCloseable(fileInputStream);
              closeCloseable(bufferedReader);
          }
  
          return null;
      }
  
      public static class ReadSerializableObjectResult {
          public final Error error;
          public final Serializable serializableObject;
  
          ReadSerializableObjectResult(Error error, Serializable serializableObject) {
              this.error = error;
              this.serializableObject = serializableObject;
          }
      }
  
      /**
       * Read a {@link Serializable} object from file at path.
       *
       * @param label The optional label for file to read. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to read.
       * @param readObjectType The {@link Class} of the object.
       * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
       *                              error if file to read doesn't exist.
       * @return Returns the {@code error} if reading was not successful, otherwise {@code null}.
       */
      @NonNull
      public static <T extends Serializable> ReadSerializableObjectResult readSerializableObjectFromFile(String label, final String filePath, Class<T> readObjectType, final boolean ignoreNonExistentFile) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return new ReadSerializableObjectResult(FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "readSerializableObjectFromFile"), null);
  
          Logger.logVerbose(LOG_TAG, "Reading serializable object from " + label + "file at path \"" + filePath + "\"");
  
          T serializableObject;
  
          FileType fileType = getFileType(filePath, false);
  
          // If file exists but not a regular file
          if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
              return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file"), null);
          }
  
          // If file does not exist
          if (fileType == FileType.NO_EXIST) {
              // If reading is to be ignored if file does not exist
              if (ignoreNonExistentFile)
                  return new ReadSerializableObjectResult(null, null);
                  // Else return with error
              else {
                  label += "file meant to be read";
                  return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label), null);
              }
          }
  
          FileInputStream fileInputStream = null;
          ObjectInputStream objectInputStream = null;
          try {
              // Read serializable object from file
              fileInputStream = new FileInputStream(filePath);
              objectInputStream = new ObjectInputStream(fileInputStream);
              //serializableObject = (T) objectInputStream.readObject();
              serializableObject = readObjectType.cast(objectInputStream.readObject());
  
              //Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-"));
          } catch (Exception e) {
              return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage()), null);
          } finally {
              closeCloseable(fileInputStream);
              closeCloseable(objectInputStream);
          }
  
          return new ReadSerializableObjectResult(null, serializableObject);
      }
  
      /**
       * Write text {@code dataString} with a specific {@link Charset} to file at path.
       *
       * @param label The optional label for file to write. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to write.
       * @param charset The {@link Charset} of the {@code dataString}. If this is {@code null},
       *                then default {@link Charset} will be used.
       * @param dataString The data to write to file.
       * @param append The {@code boolean} that decides if file should be appended to or not.
       * @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
       */
      public static Error writeTextToFile(String label, final String filePath, Charset charset, final String dataString, final boolean append) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "writeStringToFile");
  
          Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("Writing text to " + label + "file at path \"" + filePath + "\"", DataUtils.getTruncatedCommandOutput(dataString, Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-"));
  
          Error error;
  
          error = preWriteToFile(label, filePath);
          if (error != null)
              return error;
  
          if (charset == null) charset = Charset.defaultCharset();
  
          // Check if charset is supported
          error = isCharsetSupported(charset);
          if (error != null)
              return error;
  
          FileOutputStream fileOutputStream = null;
          BufferedWriter bufferedWriter = null;
          try {
              // Write text to file
              fileOutputStream = new FileOutputStream(filePath, append);
              bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream, charset));
  
              bufferedWriter.write(dataString);
              bufferedWriter.flush();
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_WRITING_TEXT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
          } finally {
              closeCloseable(fileOutputStream);
              closeCloseable(bufferedWriter);
          }
  
          return null;
      }
  
      /**
       * Write the {@link Serializable} {@code serializableObject} to file at path.
       *
       * @param label The optional label for file to write. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to write.
       * @param serializableObject The object to write to file.
       * @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
       */
      public static <T extends Serializable> Error writeSerializableObjectToFile(String label, final String filePath, final T serializableObject) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "writeSerializableObjectToFile");
  
          Logger.logVerbose(LOG_TAG, "Writing serializable object to " + label + "file at path \"" + filePath + "\"");
  
          Error error;
  
          error = preWriteToFile(label, filePath);
          if (error != null)
              return error;
  
          FileOutputStream fileOutputStream = null;
          ObjectOutputStream objectOutputStream = null;
          try {
              // Write serializable object to file
              fileOutputStream = new FileOutputStream(filePath);
              objectOutputStream = new ObjectOutputStream(fileOutputStream);
  
              objectOutputStream.writeObject(serializableObject);
              objectOutputStream.flush();
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
          } finally {
              closeCloseable(fileOutputStream);
              closeCloseable(objectOutputStream);
          }
  
          return null;
      }
  
      private static Error preWriteToFile(String label, String filePath) {
          Error error;
  
          FileType fileType = getFileType(filePath, false);
  
          // If file exists but not a regular file
          if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
              return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
          }
  
          // Create the file parent directory
          error = createParentDirectoryFile(label + "file parent", filePath);
          if (error != null)
              return error;
  
          return null;
      }
  
  
  
      /**
       * Check if a specific {@link Charset} is supported.
       *
       * @param charset The {@link Charset} to check.
       * @return Returns the {@code error} if charset is not supported or failed to check it, otherwise {@code null}.
       */
      public static Error isCharsetSupported(final Charset charset) {
          if (charset == null) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("charset", "isCharsetSupported");
  
          try {
              if (!Charset.isSupported(charset.name())) {
                  return FileUtilsErrno.ERRNO_UNSUPPORTED_CHARSET.getError(charset.name());
              }
          } catch (Exception e) {
              return FileUtilsErrno.ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED.getError(e, charset.name(), e.getMessage());
          }
  
          return null;
      }
  
  
  
      /**
       * Close a {@link Closeable} object if not {@code null} and ignore any exceptions raised.
       *
       * @param closeable The {@link Closeable} object to close.
       */
      public static void closeCloseable(final Closeable closeable) {
          if (closeable != null) {
              try {
                  closeable.close();
              }
              catch (IOException e) {
                  // ignore
              }
          }
      }
  
  
  
      /**
       * Set permissions for file at path. Existing permission outside the {@code permissionsToSet}
       * will be removed.
       *
       * @param filePath The {@code path} for file to set permissions to.
       * @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
       */
      public static void setFilePermissions(final String filePath, final String permissionsToSet) {
          setFilePermissions(null, filePath, permissionsToSet);
      }
  
      /**
       * Set permissions for file at path. Existing permission outside the {@code permissionsToSet}
       * will be removed.
       *
       * @param label The optional label for the file. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to set permissions to.
       * @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
       */
      public static void setFilePermissions(String label, final String filePath, final String permissionsToSet) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return;
  
          if (!isValidPermissionString(permissionsToSet)) {
              Logger.logError(LOG_TAG, "Invalid permissionsToSet passed to setFilePermissions: \"" + permissionsToSet + "\"");
              return;
          }
  
          File file = new File(filePath);
  
          if (permissionsToSet.contains("r")) {
              if (!file.canRead()) {
                  Logger.logVerbose(LOG_TAG, "Setting read permissions for " + label + "file at path \"" + filePath + "\"");
                  file.setReadable(true);
              }
          } else {
              if (file.canRead()) {
                  Logger.logVerbose(LOG_TAG, "Removing read permissions for " + label + "file at path \"" + filePath + "\"");
                  file.setReadable(false);
              }
          }
  
  
          if (permissionsToSet.contains("w")) {
              if (!file.canWrite()) {
                  Logger.logVerbose(LOG_TAG, "Setting write permissions for " + label + "file at path \"" + filePath + "\"");
                  file.setWritable(true);
              }
          } else {
              if (file.canWrite()) {
                  Logger.logVerbose(LOG_TAG, "Removing write permissions for " + label + "file at path \"" + filePath + "\"");
                  file.setWritable(false);
              }
          }
  
  
          if (permissionsToSet.contains("x")) {
              if (!file.canExecute()) {
                  Logger.logVerbose(LOG_TAG, "Setting execute permissions for " + label + "file at path \"" + filePath + "\"");
                  file.setExecutable(true);
              }
          } else {
              if (file.canExecute()) {
                  Logger.logVerbose(LOG_TAG, "Removing execute permissions for " + label + "file at path \"" + filePath + "\"");
                  file.setExecutable(false);
              }
          }
      }
  
  
  
      /**
       * Set missing permissions for file at path. Existing permission outside the {@code permissionsToSet}
       * will not be removed.
       *
       * @param filePath The {@code path} for file to set permissions to.
       * @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
       */
      public static void setMissingFilePermissions(final String filePath, final String permissionsToSet) {
          setMissingFilePermissions(null, filePath, permissionsToSet);
      }
  
      /**
       * Set missing permissions for file at path. Existing permission outside the {@code permissionsToSet}
       * will not be removed.
       *
       * @param label The optional label for the file. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to set permissions to.
       * @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
       */
      public static void setMissingFilePermissions(String label, final String filePath, final String permissionsToSet) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return;
  
          if (!isValidPermissionString(permissionsToSet)) {
              Logger.logError(LOG_TAG, "Invalid permissionsToSet passed to setMissingFilePermissions: \"" + permissionsToSet + "\"");
              return;
          }
  
          File file = new File(filePath);
  
          if (permissionsToSet.contains("r") && !file.canRead()) {
              Logger.logVerbose(LOG_TAG, "Setting missing read permissions for " + label + "file at path \"" + filePath + "\"");
              file.setReadable(true);
          }
  
          if (permissionsToSet.contains("w") && !file.canWrite()) {
              Logger.logVerbose(LOG_TAG, "Setting missing write permissions for " + label + "file at path \"" + filePath + "\"");
              file.setWritable(true);
          }
  
          if (permissionsToSet.contains("x") && !file.canExecute()) {
              Logger.logVerbose(LOG_TAG, "Setting missing execute permissions for " + label + "file at path \"" + filePath + "\"");
              file.setExecutable(true);
          }
      }
  
  
  
      /**
       * Checking missing permissions for file at path.
       *
       * @param filePath The {@code path} for file to check permissions for.
       * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
       * @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
       *                              error is to be ignored.
       * @return Returns the {@code error} if validating permissions failed, otherwise {@code null}.
       */
      public static Error checkMissingFilePermissions(final String filePath, final String permissionsToCheck, final boolean ignoreIfNotExecutable) {
          return checkMissingFilePermissions(null, filePath, permissionsToCheck, ignoreIfNotExecutable);
      }
  
      /**
       * Checking missing permissions for file at path.
       *
       * @param label The optional label for the file. This can optionally be {@code null}.
       * @param filePath The {@code path} for file to check permissions for.
       * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
       * @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
       *                              error is to be ignored.
       * @return Returns the {@code error} if validating permissions failed, otherwise {@code null}.
       */
      public static Error checkMissingFilePermissions(String label, final String filePath, final String permissionsToCheck, final boolean ignoreIfNotExecutable) {
          label = (label == null || label.isEmpty() ? "" : label + " ");
          if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "checkMissingFilePermissions");
  
          if (!isValidPermissionString(permissionsToCheck)) {
              Logger.logError(LOG_TAG, "Invalid permissionsToCheck passed to checkMissingFilePermissions: \"" + permissionsToCheck + "\"");
              return FileUtilsErrno.ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK.getError();
          }
  
          File file = new File(filePath);
  
          // If file is not readable
          if (permissionsToCheck.contains("r") && !file.canRead()) {
              return FileUtilsErrno.ERRNO_FILE_NOT_READABLE.getError(label + "file", filePath).setLabel(label + "file");
          }
  
          // If file is not writable
          if (permissionsToCheck.contains("w") && !file.canWrite()) {
              return FileUtilsErrno.ERRNO_FILE_NOT_WRITABLE.getError(label + "file", filePath).setLabel(label + "file");
          }
          // If file is not executable
          // This canExecute() will give "avc: granted { execute }" warnings for target sdk 29
          else if (permissionsToCheck.contains("x") && !file.canExecute() && !ignoreIfNotExecutable) {
              return FileUtilsErrno.ERRNO_FILE_NOT_EXECUTABLE.getError(label + "file", filePath).setLabel(label + "file");
          }
  
          return null;
      }
  
  
  
      /**
       * Checks whether string exactly matches the 3 character permission string that
       * contains the "r", "w", "x" or "-" in-order.
       *
       * @param string The {@link String} to check.
       * @return Returns {@code true} if string exactly matches a permission string, otherwise {@code false}.
       */
      public static boolean isValidPermissionString(final String string) {
          if (string == null || string.isEmpty()) return false;
          return Pattern.compile("^([r-])[w-][x-]$", 0).matcher(string).matches();
      }
  
  
  
      /**
       * Get a {@link Error} that contains a shorter version of {@link Errno} message.
       *
       * @param error The original {@link Error} returned by one of the {@link FileUtils} functions.
       * @return Returns the shorter {@link Error} if one exists, otherwise original {@code error}.
       */
      public static Error getShortFileUtilsError(final Error error) {
          String type = error.getType();
          if (!FileUtilsErrno.TYPE.equals(type)) return error;
  
          Errno shortErrno = FileUtilsErrno.ERRNO_SHORT_MAPPING.get(Errno.valueOf(type, error.getCode()));
          if (shortErrno == null) return error;
  
          List<Throwable> throwables = error.getThrowablesList();
          if (throwables.isEmpty())
              return shortErrno.getError(DataUtils.getDefaultIfNull(error.getLabel(), "file"));
          else
              return shortErrno.getError(throwables, error.getLabel(), "file");
      }
  
  
      /**
       * Get file dirname for file at {@code filePath}.
       *
       * @param filePath The {@code path} for file.
       * @return Returns the file dirname if not {@code null}.
       */
      public static String getFileDirname(String filePath) {
          if (DataUtils.isNullOrEmpty(filePath)) return null;
          int lastSlash = filePath.lastIndexOf('/');
          return (lastSlash == -1) ? null : filePath.substring(0, lastSlash);
      }
  
      /**
       * Get file basename for file at {@code filePath}.
       *
       * @param filePath The {@code path} for file.
       * @return Returns the file basename if not {@code null}.
       */
      public static String getFileBasename(String filePath) {
          if (DataUtils.isNullOrEmpty(filePath)) return null;
          int lastSlash = filePath.lastIndexOf('/');
          return (lastSlash == -1) ? filePath : filePath.substring(lastSlash + 1);
      }
  
      /**
       * Get file basename for file at {@code filePath} without extension.
       *
       * @param filePath The {@code path} for file.
       * @return Returns the file basename without extension if not {@code null}.
       */
      public static String getFileBasenameWithoutExtension(String filePath) {
          String fileBasename = getFileBasename(filePath);
          if (DataUtils.isNullOrEmpty(fileBasename)) return null;
          int lastDot = fileBasename.lastIndexOf('.');
          return (lastDot == -1) ? fileBasename : fileBasename.substring(0, lastDot);
      }
  
  }
  