diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index 7a1b5456..6b4782eb 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -14,6 +14,7 @@ import com.termux.R; import com.termux.shared.file.FileUtils; import com.termux.shared.interact.DialogUtils; import com.termux.shared.logger.Logger; +import com.termux.shared.models.errors.Error; import com.termux.shared.termux.TermuxConstants; import java.io.BufferedReader; @@ -88,21 +89,21 @@ final class TermuxInstaller { try { Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages."); - String errmsg; + Error error; final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH; final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH); // Delete prefix staging directory or any file at its destination - errmsg = FileUtils.deleteFile(activity, "prefix staging directory", STAGING_PREFIX_PATH, true); - if (errmsg != null) { - throw new RuntimeException(errmsg); + error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true); + if (error != null) { + throw new RuntimeException(error.toString()); } // Delete prefix directory or any file at its destination - errmsg = FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true); - if (errmsg != null) { - throw new RuntimeException(errmsg); + error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true); + if (error != null) { + throw new RuntimeException(error.toString()); } Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\"."); @@ -125,14 +126,14 @@ final class TermuxInstaller { String newPath = STAGING_PREFIX_PATH + "/" + parts[1]; symlinks.add(Pair.create(oldPath, newPath)); - ensureDirectoryExists(activity, new File(newPath).getParentFile()); + ensureDirectoryExists(new File(newPath).getParentFile()); } } else { String zipEntryName = zipEntry.getName(); File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName); boolean isDirectory = zipEntry.isDirectory(); - ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile()); + ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile()); if (!isDirectory) { try (FileOutputStream outStream = new FileOutputStream(targetFile)) { @@ -173,7 +174,7 @@ final class TermuxInstaller { activity.finish(); }).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> { dialog.dismiss(); - FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true); + FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true); TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone); }).show(); } catch (WindowManager.BadTokenException e1) { @@ -201,12 +202,13 @@ final class TermuxInstaller { new Thread() { public void run() { try { - String errmsg; + Error error; File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR; - errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath()); - if (errmsg != null) { - Logger.logErrorAndShowToast(context, LOG_TAG, errmsg); + error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath()); + if (error != null) { + Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage()); + Logger.logErrorExtended(LOG_TAG, error.toString()); return; } @@ -249,12 +251,12 @@ final class TermuxInstaller { }.start(); } - private static void ensureDirectoryExists(Context context, File directory) { - String errmsg; + private static void ensureDirectoryExists(File directory) { + Error error; - errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath()); - if (errmsg != null) { - throw new RuntimeException(errmsg); + error = FileUtils.createDirectoryFile(directory.getAbsolutePath()); + if (error != null) { + throw new RuntimeException(error.toString()); } } diff --git a/app/src/main/java/com/termux/app/utils/CrashUtils.java b/app/src/main/java/com/termux/app/utils/CrashUtils.java index 55a65ef4..63cd0356 100644 --- a/app/src/main/java/com/termux/app/utils/CrashUtils.java +++ b/app/src/main/java/com/termux/app/utils/CrashUtils.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import com.termux.R; import com.termux.shared.activities.ReportActivity; +import com.termux.shared.models.errors.Error; import com.termux.shared.notification.NotificationUtils; import com.termux.shared.file.FileUtils; import com.termux.shared.models.ReportInfo; @@ -62,25 +63,25 @@ public class CrashUtils { if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false)) return; - String errmsg; + Error error; StringBuilder reportStringBuilder = new StringBuilder(); // Read report string from crash log file - errmsg = FileUtils.readStringFromFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false); - if (errmsg != null) { - Logger.logError(logTag, errmsg); + error = FileUtils.readStringFromFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false); + if (error != null) { + Logger.logErrorExtended(logTag, error.toString()); return; } // Move crash log file to backup location if it exists - FileUtils.moveRegularFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true); - if (errmsg != null) { - Logger.logError(logTag, errmsg); + error = FileUtils.moveRegularFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true); + if (error != null) { + Logger.logErrorExtended(logTag, error.toString()); } String reportString = reportStringBuilder.toString(); - if (reportString == null || reportString.isEmpty()) + if (reportString.isEmpty()) return; // Send a notification to show the crash log which when clicked will open the {@link ReportActivity} diff --git a/termux-shared/src/main/java/com/termux/shared/crash/CrashHandler.java b/termux-shared/src/main/java/com/termux/shared/crash/CrashHandler.java index 8cdbdf24..1f7c82ad 100644 --- a/termux-shared/src/main/java/com/termux/shared/crash/CrashHandler.java +++ b/termux-shared/src/main/java/com/termux/shared/crash/CrashHandler.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import com.termux.shared.file.FileUtils; import com.termux.shared.logger.Logger; import com.termux.shared.markdown.MarkdownUtils; +import com.termux.shared.models.errors.Error; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxUtils; @@ -57,7 +58,7 @@ public class CrashHandler implements Thread.UncaughtExceptionHandler { reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-")); reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", TermuxUtils.getCurrentTimeStamp(), "-")); - reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTraceStringArray(throwable))); + reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTracesStringArray(throwable))); reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true)); reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context)); @@ -65,9 +66,9 @@ public class CrashHandler implements Thread.UncaughtExceptionHandler { Logger.logError(reportString.toString()); // Write report string to crash log file - String errmsg = FileUtils.writeStringToFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportString.toString(), false); - if (errmsg != null) { - Logger.logError(LOG_TAG, errmsg); + Error error = FileUtils.writeStringToFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportString.toString(), false); + if (error != null) { + Logger.logErrorExtended(LOG_TAG, error.toString()); } } diff --git a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java index 71b1ae15..4bc81933 100644 --- a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java @@ -1,18 +1,18 @@ package com.termux.shared.file; -import android.content.Context; import android.os.Build; import android.system.Os; import androidx.annotation.NonNull; import com.google.common.io.RecursiveDeleteOption; -import com.termux.shared.R; -import com.termux.shared.termux.TermuxConstants; 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.models.errors.Error; +import com.termux.shared.models.errors.FileUtilsErrno; +import com.termux.shared.models.errors.FunctionErrno; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -21,9 +21,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.nio.file.LinkOption; @@ -32,45 +30,18 @@ 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"; /** - * Replace "$PREFIX/" or "~/" prefix with termux absolute paths. + * Get canonical path. * - * @param path The {@code path} to expand. - * @return Returns the {@code expand path}. - */ - public static String getExpandedTermuxPath(String path) { - if (path != null && !path.isEmpty()) { - path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH); - path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/"); - path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH); - path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/"); - } - - return path; - } - - /** - * Replace termux absolute paths with "$PREFIX/" or "~/" prefix. - * - * @param path The {@code path} to unexpand. - * @return Returns the {@code unexpand path}. - */ - public static String getUnExpandedTermuxPath(String path) { - if (path != null && !path.isEmpty()) { - path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/"); - path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/"); - } - - return path; - } - - /** - * If {@code expandPath} is enabled, then input path is first attempted to be expanded by calling - * {@link #getExpandedTermuxPath(String)}. - * - * Then if path is already an absolute path, then it is used as is to 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 @@ -85,12 +56,9 @@ public class FileUtils { * will automatically do this anyways. * @return Returns the {@code canonical path}. */ - public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath, final boolean expandPath) { + public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath) { if (path == null) path = ""; - if (expandPath) - path = getExpandedTermuxPath(path); - String absolutePath; // If path is already an absolute path @@ -160,7 +128,6 @@ public class FileUtils { } - /** * Checks whether a regular file exists at {@code filePath}. * @@ -215,7 +182,7 @@ public class FileUtils { * * 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 @@ -236,7 +203,6 @@ public class FileUtils { * 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 context The {@link Context} to get error string. * @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. @@ -249,21 +215,21 @@ public class FileUtils { * @param ignoreErrorsIfPathIsUnderParentDirPath The {@code boolean} that decides if permission * errors are to be ignored if path is under * {@code parentDirPath}. - * @return Returns the {@code errmsg} if path is not a regular file, or validating permissions + * @return Returns the {@code error} if path is not a regular file, or validating permissions * failed, otherwise {@code null}. */ - public static String validateRegularFileExistenceAndPermissions(@NonNull final Context context, String label, final String filePath, final String parentDirPath, + 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 + " "); - if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "regular file path", "validateRegularFileExistenceAndPermissions"); + 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 context.getString(R.string.error_non_regular_file_found, label + "file"); + return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file"); } boolean isPathUnderParentDirPath = false; @@ -286,7 +252,7 @@ public class FileUtils { // If path is not a regular file // Regular files cannot be automatically created so we do not ignore if missing if (fileType != FileType.REGULAR) { - return context.getString(R.string.error_no_regular_file_found, label + "file"); + return FileUtilsErrno.ERRNO_NO_REGULAR_FILE_FOUND.getError(label + "file"); } // If there is not parentDirPath restriction or path is not under parentDirPath or @@ -294,11 +260,11 @@ public class FileUtils { if (parentDirPath == null || !isPathUnderParentDirPath || !ignoreErrorsIfPathIsUnderParentDirPath) { if (permissionsToCheck != null) { // Check if permissions are missing - return checkMissingFilePermissions(context, label + "regular", filePath, permissionsToCheck, false); + return checkMissingFilePermissions(label + "regular", filePath, permissionsToCheck, false); } } } catch (Exception e) { - return context.getString(R.string.error_validate_file_existence_and_permissions_failed_with_exception, label + "file", filePath, e.getMessage()); + return FileUtilsErrno.ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage()); } return null; @@ -312,7 +278,6 @@ public class FileUtils { * setting of missing permissions will only be done if {@code path} is under * {@code parentDirPath} or equals {@code parentDirPath}. * - * @param context The {@link Context} to get error string. * @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. @@ -330,14 +295,14 @@ public class FileUtils { * @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 errmsg} if path is not a directory file, failed to create it, + * @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 String validateDirectoryFileExistenceAndPermissions(@NonNull final Context context, String label, final String filePath, final String parentDirPath, final boolean createDirectoryIfMissing, + 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 + " "); - if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "directory file path", "validateDirectoryExistenceAndPermissions"); + if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "directory file path", "validateDirectoryExistenceAndPermissions"); try { File file = new File(filePath); @@ -345,7 +310,7 @@ public class FileUtils { // If file exists but not a directory file if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) { - return context.getString(R.string.error_non_directory_file_found, label + "directory"); + return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory"); } boolean isPathInParentDirPath = false; @@ -364,7 +329,7 @@ public class FileUtils { if (file.mkdirs()) fileType = getFileType(filePath, false); else - return context.getString(R.string.error_creating_file_failed, label + "directory file", filePath); + return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED.getError(label + "directory file", filePath); } // If setPermissions is enabled and path is a directory @@ -383,16 +348,16 @@ public class FileUtils { // If path is not a directory // Directories can be automatically created so we can ignore if missing with above check if (fileType != FileType.DIRECTORY) { - return context.getString(R.string.error_file_not_found_at_path, label + "directory", filePath); + return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "directory", filePath); } if (permissionsToCheck != null) { // Check if permissions are missing - return checkMissingFilePermissions(context, label + "directory", filePath, permissionsToCheck, ignoreIfNotExecutable); + return checkMissingFilePermissions(label + "directory", filePath, permissionsToCheck, ignoreIfNotExecutable); } } } catch (Exception e) { - return context.getString(R.string.error_validate_directory_existence_and_permissions_failed_with_exception, label + "directory file", filePath, e.getMessage()); + return FileUtilsErrno.ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION.getError(e, label + "directory file", filePath, e.getMessage()); } return null; @@ -404,31 +369,29 @@ public class FileUtils { * Create a regular file at path. * * This function is a wrapper for - * {@link #validateDirectoryFileExistenceAndPermissions(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. + * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. * - * @param context The {@link Context} to get error string. * @param filePath The {@code path} for regular file to create. - * @return Returns the {@code errmsg} if path is not a regular file or failed to create it, + * @return Returns the {@code error} if path is not a regular file or failed to create it, * otherwise {@code null}. */ - public static String createRegularFile(@NonNull final Context context, final String filePath) { - return createRegularFile(context, null, filePath); + 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(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. + * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. * - * @param context The {@link Context} to get error string. * @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 errmsg} if path is not a regular file or failed to create it, + * @return Returns the {@code error} if path is not a regular file or failed to create it, * otherwise {@code null}. */ - public static String createRegularFile(@NonNull final Context context, final String label, final String filePath) { - return createRegularFile(context, label, filePath, + public static Error createRegularFile(final String label, final String filePath) { + return createRegularFile(label, filePath, null, false, false); } @@ -436,9 +399,8 @@ public class FileUtils { * Create a regular file at path. * * This function is a wrapper for - * {@link #validateRegularFileExistenceAndPermissions(Context, String, String, String, String, boolean, boolean, boolean)}. + * {@link #validateRegularFileExistenceAndPermissions(String, String, String, String, boolean, boolean, boolean)}. * - * @param context The {@link Context} to get error string. * @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. @@ -446,22 +408,22 @@ public class FileUtils { * 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 errmsg} if path is not a regular file, failed to create it, + * @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 String createRegularFile(@NonNull final Context context, String label, final String filePath, + public static Error createRegularFile(String label, final String filePath, final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly) { label = (label == null ? "" : label + " "); - if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "createRegularFile"); + if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "createRegularFile"); - String errmsg; + 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 context.getString(R.string.error_non_regular_file_found, label + "file"); + return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file"); } // If regular file already exists @@ -470,20 +432,20 @@ public class FileUtils { } // Create the file parent directory - errmsg = createParentDirectoryFile(context, label + "regular file parent", filePath); - if (errmsg != null) - return errmsg; + 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 context.getString(R.string.error_creating_file_failed, label + "regular file", filePath); + return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED.getError(label + "regular file", filePath); } catch (Exception e) { - return context.getString(R.string.error_creating_file_failed_with_exception, label + "regular file", filePath, e.getMessage()); + return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "regular file", filePath, e.getMessage()); } - return validateRegularFileExistenceAndPermissions(context, label, filePath, + return validateRegularFileExistenceAndPermissions(label, filePath, null, permissionsToCheck, setPermissions, setMissingPermissionsOnly, false); @@ -495,23 +457,22 @@ public class FileUtils { * Create parent directory of file at path. * * This function is a wrapper for - * {@link #validateDirectoryFileExistenceAndPermissions(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. + * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. * - * @param context The {@link Context} to get error string. * @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 errmsg} if parent path is not a directory file or failed to create it, + * @return Returns the {@code error} if parent path is not a directory file or failed to create it, * otherwise {@code null}. */ - public static String createParentDirectoryFile(@NonNull final Context context, final String label, final String filePath) { - if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "createParentDirectoryFile"); + 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(context, label, fileParentPath, - null, false, false); + return createDirectoryFile(label, fileParentPath, + null, false, false); else return null; } @@ -520,41 +481,38 @@ public class FileUtils { * Create a directory file at path. * * This function is a wrapper for - * {@link #validateDirectoryFileExistenceAndPermissions(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. + * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. * - * @param context The {@link Context} to get error string. * @param filePath The {@code path} for directory file to create. - * @return Returns the {@code errmsg} if path is not a directory file or failed to create it, + * @return Returns the {@code error} if path is not a directory file or failed to create it, * otherwise {@code null}. */ - public static String createDirectoryFile(@NonNull final Context context, final String filePath) { - return createDirectoryFile(context, null, filePath); + 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(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. + * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. * - * @param context The {@link Context} to get error string. * @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 errmsg} if path is not a directory file or failed to create it, + * @return Returns the {@code error} if path is not a directory file or failed to create it, * otherwise {@code null}. */ - public static String createDirectoryFile(@NonNull final Context context, final String label, final String filePath) { - return createDirectoryFile(context, label, filePath, + 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(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}. * - * @param context The {@link Context} to get error string. + * 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. @@ -562,12 +520,12 @@ public class FileUtils { * 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 errmsg} if path is not a directory file, failed to create it, + * @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 String createDirectoryFile(@NonNull final Context context, final String label, final String filePath, + public static Error createDirectoryFile(final String label, final String filePath, final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly) { - return validateDirectoryFileExistenceAndPermissions(context, label, filePath, + return validateDirectoryFileExistenceAndPermissions(label, filePath, null, true, permissionsToCheck, setPermissions, setMissingPermissionsOnly, false, false); @@ -579,19 +537,18 @@ public class FileUtils { * Create a symlink file at path. * * This function is a wrapper for - * {@link #createSymlinkFile(Context, String, String, String, boolean, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if path is not a symlink file, failed to create it, + * @return Returns the {@code error} if path is not a symlink file, failed to create it, * otherwise {@code null}. */ - public static String createSymlinkFile(@NonNull final Context context, final String targetFilePath, final String destFilePath) { - return createSymlinkFile(context, null, targetFilePath, destFilePath, + public static Error createSymlinkFile(final String targetFilePath, final String destFilePath) { + return createSymlinkFile(null, targetFilePath, destFilePath, true, true, true); } @@ -599,27 +556,25 @@ public class FileUtils { * Create a symlink file at path. * * This function is a wrapper for - * {@link #createSymlinkFile(Context, String, String, String, boolean, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if path is not a symlink file, failed to create it, + * @return Returns the {@code error} if path is not a symlink file, failed to create it, * otherwise {@code null}. */ - public static String createSymlinkFile(@NonNull final Context context, String label, final String targetFilePath, final String destFilePath) { - return createSymlinkFile(context, label, targetFilePath, destFilePath, + 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 context The {@link Context} to get error string. * @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. @@ -630,16 +585,16 @@ public class FileUtils { * 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 errmsg} if path is not a symlink file, failed to create it, + * @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 String createSymlinkFile(@NonNull final Context context, String label, final String targetFilePath, final String destFilePath, + 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 + " "); - if (targetFilePath == null || targetFilePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "target file path", "createSymlinkFile"); - if (destFilePath == null || destFilePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "destination file path", "createSymlinkFile"); + 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"); - String errmsg; + Error error; try { File destFile = new File(destFilePath); @@ -659,7 +614,7 @@ public class FileUtils { if (targetFileType == FileType.NO_EXIST) { // If dangling symlink should not be allowed, then return with error if (!allowDangling) - return context.getString(R.string.error_file_not_found_at_path, label + "symlink target file", targetFileAbsolutePath); + return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "symlink target file", targetFileAbsolutePath); } // If destination exists @@ -671,24 +626,24 @@ public class FileUtils { // If overwriteOnlyIfDestIsASymlink is enabled but destination file is not a symlink if (overwriteOnlyIfDestIsASymlink && destFileType != FileType.SYMLINK) - return context.getString(R.string.error_cannot_overwrite_a_non_symlink_file_type, label + " file", destFilePath, targetFilePath, destFileType.getName()); + return FileUtilsErrno.ERRNO_CANNOT_OVERWRITE_A_NON_SYMLINK_FILE_TYPE.getError(label + " file", destFilePath, targetFilePath, destFileType.getName()); // Delete the destination file - errmsg = deleteFile(context, label + "symlink destination", destFilePath, true); - if (errmsg != null) - return errmsg; + error = deleteFile(label + "symlink destination", destFilePath, true); + if (error != null) + return error; } else { // Create the destination file parent directory - errmsg = createParentDirectoryFile(context, label + "symlink destination file parent", destFilePath); - if (errmsg != null) - return errmsg; + 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 context.getString(R.string.error_creating_symlink_file_failed_with_exception, label + "symlink file", destFilePath, targetFilePath, e.getMessage()); + return FileUtilsErrno.ERRNO_CREATING_SYMLINK_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "symlink file", destFilePath, targetFilePath, e.getMessage()); } return null; @@ -700,21 +655,20 @@ public class FileUtils { * Copy a regular file from {@code sourceFilePath} to {@code destFilePath}. * * This function is a wrapper for - * {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if copy was not successful, otherwise {@code null}. + * @return Returns the {@code error} if copy was not successful, otherwise {@code null}. */ - public static String copyRegularFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) { - return copyOrMoveFile(context, label, srcFilePath, destFilePath, + 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); } @@ -723,21 +677,20 @@ public class FileUtils { * Move a regular file from {@code sourceFilePath} to {@code destFilePath}. * * This function is a wrapper for - * {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if move was not successful, otherwise {@code null}. + * @return Returns the {@code error} if move was not successful, otherwise {@code null}. */ - public static String moveRegularFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) { - return copyOrMoveFile(context, label, srcFilePath, destFilePath, + 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); } @@ -746,21 +699,20 @@ public class FileUtils { * Copy a directory file from {@code sourceFilePath} to {@code destFilePath}. * * This function is a wrapper for - * {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if copy was not successful, otherwise {@code null}. + * @return Returns the {@code error} if copy was not successful, otherwise {@code null}. */ - public static String copyDirectoryFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) { - return copyOrMoveFile(context, label, srcFilePath, destFilePath, + 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); } @@ -769,22 +721,21 @@ public class FileUtils { * Move a directory file from {@code sourceFilePath} to {@code destFilePath}. * * This function is a wrapper for - * {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if move was not successful, otherwise {@code null}. + * @return Returns the {@code error} if move was not successful, otherwise {@code null}. */ - public static String moveDirectoryFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) { - return copyOrMoveFile(context, label, srcFilePath, destFilePath, - true, ignoreNonExistentSrcFile, FileType.DIRECTORY.getValue(), + 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); } @@ -792,21 +743,20 @@ public class FileUtils { * Copy a symlink file from {@code sourceFilePath} to {@code destFilePath}. * * This function is a wrapper for - * {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if copy was not successful, otherwise {@code null}. + * @return Returns the {@code error} if copy was not successful, otherwise {@code null}. */ - public static String copySymlinkFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) { - return copyOrMoveFile(context, label, srcFilePath, destFilePath, + 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); } @@ -815,21 +765,20 @@ public class FileUtils { * Move a symlink file from {@code sourceFilePath} to {@code destFilePath}. * * This function is a wrapper for - * {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if move was not successful, otherwise {@code null}. + * @return Returns the {@code error} if move was not successful, otherwise {@code null}. */ - public static String moveSymlinkFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) { - return copyOrMoveFile(context, label, srcFilePath, destFilePath, + 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); } @@ -838,21 +787,20 @@ public class FileUtils { * Copy a file from {@code sourceFilePath} to {@code destFilePath}. * * This function is a wrapper for - * {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if copy was not successful, otherwise {@code null}. + * @return Returns the {@code error} if copy was not successful, otherwise {@code null}. */ - public static String copyFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) { - return copyOrMoveFile(context, label, srcFilePath, destFilePath, + 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); } @@ -861,25 +809,24 @@ public class FileUtils { * Move a file from {@code sourceFilePath} to {@code destFilePath}. * * This function is a wrapper for - * {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}. + * {@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 context The {@link Context} to get error string. * @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 errmsg} if move was not successful, otherwise {@code null}. + * @return Returns the {@code error} if move was not successful, otherwise {@code null}. */ - public static String moveFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) { - return copyOrMoveFile(context, label, srcFilePath, destFilePath, + 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}. * @@ -890,7 +837,6 @@ public class FileUtils { * 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 context The {@link Context} to get error string. * @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. @@ -910,23 +856,20 @@ public class FileUtils { * @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 errmsg} if copy or move was not successful, otherwise {@code null}. + * @return Returns the {@code error} if copy or move was not successful, otherwise {@code null}. */ - public static String copyOrMoveFile(@NonNull final Context context, String label, final String srcFilePath, final String destFilePath, + 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 + " "); - if (srcFilePath == null || srcFilePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "source file path", "copyOrMoveFile"); - if (destFilePath == null || destFilePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "destination file path", "copyOrMoveFile"); + 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"); - String errmsg; + Error error; - InputStream inputStream = null; - OutputStream outputStream = null; - try { Logger.logVerbose(LOG_TAG, mode + " " + label + "source file from \"" + srcFilePath + "\" to destination \"" + destFilePath + "\""); @@ -944,18 +887,18 @@ public class FileUtils { // If copy or move is to be ignored if source file is not found if (ignoreNonExistentSrcFile) return null; - // Else return with error + // Else return with error else - return context.getString(R.string.error_file_not_found_at_path, label + "source file", srcFilePath); + return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "source file", srcFilePath); } // If the file type of the source file does not exist in the allowedFileTypeFlags, then return with error if ((allowedFileTypeFlags & srcFileType.getValue()) <= 0) - return context.getString(R.string.error_file_not_an_allowed_file_type, label + "source file meant to be " + modePast, srcFilePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags)); + 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 context.getString(R.string.error_copying_or_moving_file_to_same_path, mode + " " + label + "source file", srcFilePath, destFilePath); + 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) { @@ -966,12 +909,12 @@ public class FileUtils { // If overwriteOnlyIfDestSameFileTypeAsSrc is enabled but destination file does not match source file type if (overwriteOnlyIfDestSameFileTypeAsSrc && destFileType != srcFileType) - return context.getString(R.string.error_cannot_overwrite_a_different_file_type, label + "source file", mode.toLowerCase(), srcFilePath, destFilePath, destFileType.getName(), srcFileType.getName()); + 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 - errmsg = deleteFile(context, label + "destination file", destFilePath, true); - if (errmsg != null) - return errmsg; + error = deleteFile(label + "destination file", destFilePath, true); + if (error != null) + return error; } @@ -990,7 +933,7 @@ public class FileUtils { // 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 context.getString(R.string.error_cannot_move_directory_to_sub_directory_of_itself, label + "source directory", srcFilePath, destFilePath); + 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."); @@ -1003,9 +946,9 @@ public class FileUtils { Logger.logVerbose(LOG_TAG, "Attempting to copy source to destination."); // Create the dest file parent directory - errmsg = createParentDirectoryFile(context, label + "dest file parent", destFilePath); - if (errmsg != null) - return errmsg; + 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 @@ -1016,9 +959,9 @@ public class FileUtils { } else { // read the target for the source file and create a symlink at dest // source file metadata will be lost - errmsg = createSymlinkFile(context, label + "dest file", Os.readlink(srcFilePath), destFilePath); - if (errmsg != null) - return errmsg; + error = createSymlinkFile(label + "dest file", Os.readlink(srcFilePath), destFilePath); + if (error != null) + return error; } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -1033,18 +976,15 @@ public class FileUtils { // If source file had to be moved if (moveFile) { // Delete the source file since copying would have succeeded - errmsg = deleteFile(context, label + "source file", srcFilePath, true); - if (errmsg != null) - return errmsg; + error = deleteFile(label + "source file", srcFilePath, true); + if (error != null) + return error; } Logger.logVerbose(LOG_TAG, mode + " successful."); } catch (Exception e) { - return context.getString(R.string.error_copying_or_moving_file_failed_with_exception, mode + " " + label + "file", srcFilePath, destFilePath, e.getMessage()); - } finally { - closeCloseable(inputStream); - closeCloseable(outputStream); + return FileUtilsErrno.ERRNO_COPYING_OR_MOVING_FILE_FAILED_WITH_EXCEPTION.getError(e, mode + " " + label + "file", srcFilePath, destFilePath, e.getMessage()); } return null; @@ -1055,65 +995,61 @@ public class FileUtils { /** * Delete regular file at path. * - * This function is a wrapper for {@link #deleteFile(Context, String, String, boolean, int)}. + * This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}. * - * @param context The {@link Context} to get error string. * @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 errmsg} if deletion was not successful, otherwise {@code null}. + * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. */ - public static String deleteRegularFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile) { - return deleteFile(context, label, filePath, ignoreNonExistentFile, FileType.REGULAR.getValue()); + public static Error deleteRegularFile(String label, final String filePath, final boolean ignoreNonExistentFile) { + return deleteFile(label, filePath, ignoreNonExistentFile, FileType.REGULAR.getValue()); } /** * Delete directory file at path. * - * This function is a wrapper for {@link #deleteFile(Context, String, String, boolean, int)}. + * This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}. * - * @param context The {@link Context} to get error string. * @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 errmsg} if deletion was not successful, otherwise {@code null}. + * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. */ - public static String deleteDirectoryFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile) { - return deleteFile(context, label, filePath, ignoreNonExistentFile, FileType.DIRECTORY.getValue()); + public static Error deleteDirectoryFile(String label, final String filePath, final boolean ignoreNonExistentFile) { + return deleteFile(label, filePath, ignoreNonExistentFile, FileType.DIRECTORY.getValue()); } /** * Delete symlink file at path. * - * This function is a wrapper for {@link #deleteFile(Context, String, String, boolean, int)}. + * This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}. * - * @param context The {@link Context} to get error string. * @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 errmsg} if deletion was not successful, otherwise {@code null}. + * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. */ - public static String deleteSymlinkFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile) { - return deleteFile(context, label, filePath, ignoreNonExistentFile, FileType.SYMLINK.getValue()); + public static Error deleteSymlinkFile(String label, final String filePath, final boolean ignoreNonExistentFile) { + return deleteFile(label, filePath, ignoreNonExistentFile, FileType.SYMLINK.getValue()); } /** * Delete regular, directory or symlink file at path. * - * This function is a wrapper for {@link #deleteFile(Context, String, String, boolean, int)}. + * This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}. * - * @param context The {@link Context} to get error string. * @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 errmsg} if deletion was not successful, otherwise {@code null}. + * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. */ - public static String deleteFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile) { - return deleteFile(context, label, filePath, ignoreNonExistentFile, FileTypes.FILE_TYPE_NORMAL_FLAGS); + public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile) { + return deleteFile(label, filePath, ignoreNonExistentFile, FileTypes.FILE_TYPE_NORMAL_FLAGS); } /** @@ -1124,7 +1060,6 @@ public class FileUtils { * 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 context The {@link Context} to get error string. * @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 @@ -1134,12 +1069,12 @@ public class FileUtils { * 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 errmsg} if deletion was not successful, otherwise {@code null}. + * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. */ - public static String deleteFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) { + public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) { label = (label == null ? "" : label + " "); - if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "deleteFile"); - + if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFile"); + try { Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\""); @@ -1151,25 +1086,34 @@ public class FileUtils { // If delete is to be ignored if file does not exist if (ignoreNonExistentFile) return null; - // Else return with error + // Else return with error else - return context.getString(R.string.error_file_not_found_at_path, label + "file meant to be deleted", filePath); + return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "file meant to be deleted", filePath); } // If the file type of the file does not exist in the allowedFileTypeFlags, then return with error if ((allowedFileTypeFlags & fileType.getValue()) <= 0) - return context.getString(R.string.error_file_not_an_allowed_file_type, label + "file meant to be deleted", filePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags)); + return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "file meant to be deleted", filePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags)); 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 - */ + /* + * 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 { @@ -1186,10 +1130,10 @@ public class FileUtils { // If file still exists after deleting it fileType = getFileType(filePath, false); if (fileType != FileType.NO_EXIST) - return context.getString(R.string.error_file_still_exists_after_deleting, label + "file meant to be deleted", filePath); + return FileUtilsErrno.ERRNO_FILE_STILL_EXISTS_AFTER_DELETING.getError(label + "file meant to be deleted", filePath); } catch (Exception e) { - return context.getString(R.string.error_deleting_file_failed_with_exception, label + "file", filePath, e.getMessage()); + return FileUtilsErrno.ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage()); } return null; @@ -1202,14 +1146,13 @@ public class FileUtils { * it will be created automatically. * * This function is a wrapper for - * {@link #clearDirectory(Context, String, String)}. + * {@link #clearDirectory(String, String)}. * - * @param context The {@link Context} to get error string. * @param filePath The {@code path} for directory to clear. - * @return Returns the {@code errmsg} if clearing was not successful, otherwise {@code null}. + * @return Returns the {@code error} if clearing was not successful, otherwise {@code null}. */ - public static String clearDirectory(Context context, String filePath) { - return clearDirectory(context, null, filePath); + public static Error clearDirectory(String filePath) { + return clearDirectory(null, filePath); } /** @@ -1219,16 +1162,15 @@ public class FileUtils { * 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 context The {@link Context} to get error string. * @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 errmsg} if clearing was not successful, otherwise {@code null}. + * @return Returns the {@code error} if clearing was not successful, otherwise {@code null}. */ - public static String clearDirectory(@NonNull final Context context, String label, final String filePath) { + public static Error clearDirectory(String label, final String filePath) { label = (label == null ? "" : label + " "); - if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "clearDirectory"); + if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "clearDirectory"); - String errmsg; + Error error; try { Logger.logVerbose(LOG_TAG, "Clearing " + label + "directory at path \"" + filePath + "\""); @@ -1238,12 +1180,14 @@ public class FileUtils { // If file exists but not a directory file if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) { - return context.getString(R.string.error_non_directory_file_found, label + "directory"); + return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(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 { @@ -1253,12 +1197,12 @@ public class FileUtils { } // Else create it else { - errmsg = createDirectoryFile(context, label, filePath); - if (errmsg != null) - return errmsg; + error = createDirectoryFile(label, filePath); + if (error != null) + return error; } } catch (Exception e) { - return context.getString(R.string.error_clearing_directory_failed_with_exception, label + "directory", filePath, e.getMessage()); + return FileUtilsErrno.ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, e.getMessage()); } return null; @@ -1269,7 +1213,6 @@ public class FileUtils { /** * Read a {@link String} from file at path with a specific {@link Charset} into {@code dataString}. * - * @param context The {@link Context} to get error string. * @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}, @@ -1277,21 +1220,21 @@ public class FileUtils { * @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 errmsg} if reading was not successful, otherwise {@code null}. + * @return Returns the {@code error} if reading was not successful, otherwise {@code null}. */ - public static String readStringFromFile(@NonNull final Context context, String label, final String filePath, Charset charset, @NonNull final StringBuilder dataStringBuilder, final boolean ignoreNonExistentFile) { + public static Error readStringFromFile(String label, final String filePath, Charset charset, @NonNull final StringBuilder dataStringBuilder, final boolean ignoreNonExistentFile) { label = (label == null ? "" : label + " "); - if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "readStringFromFile"); + if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "readStringFromFile"); Logger.logVerbose(LOG_TAG, "Reading string from " + label + "file at path \"" + filePath + "\""); - String errmsg; + Error error; FileType fileType = getFileType(filePath, false); // If file exists but not a regular file if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) { - return context.getString(R.string.error_non_regular_file_found, label + "file"); + return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file"); } // If file does not exist @@ -1299,17 +1242,17 @@ public class FileUtils { // If reading is to be ignored if file does not exist if (ignoreNonExistentFile) return null; - // Else return with error + // Else return with error else - return context.getString(R.string.error_file_not_found_at_path, label + "file meant to be read", filePath); + return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "file meant to be read", filePath); } if (charset == null) charset = Charset.defaultCharset(); // Check if charset is supported - errmsg = isCharsetSupported(context, charset); - if (errmsg != null) - return errmsg; + error = isCharsetSupported(charset); + if (error != null) + return error; FileInputStream fileInputStream = null; BufferedReader bufferedReader = null; @@ -1326,9 +1269,9 @@ public class FileUtils { dataStringBuilder.append(receiveString); } - Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES, true, false, true), "-")); + Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-")); } catch (Exception e) { - return context.getString(R.string.error_reading_string_to_file_failed_with_exception, label + "file", filePath, e.getMessage()); + return FileUtilsErrno.ERRNO_READING_STRING_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage()); } finally { closeCloseable(fileInputStream); closeCloseable(bufferedReader); @@ -1340,40 +1283,39 @@ public class FileUtils { /** * Write the {@link String} {@code dataString} with a specific {@link Charset} to file at path. * - * @param context The {@link Context} to get error string. * @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 append The {@code boolean} that decides if file should be appended to or not. - * @return Returns the {@code errmsg} if writing was not successful, otherwise {@code null}. + * @return Returns the {@code error} if writing was not successful, otherwise {@code null}. */ - public static String writeStringToFile(@NonNull final Context context, String label, final String filePath, Charset charset, final String dataString, final boolean append) { + public static Error writeStringToFile(String label, final String filePath, Charset charset, final String dataString, final boolean append) { label = (label == null ? "" : label + " "); - if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "writeStringToFile"); + if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "writeStringToFile"); - Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("Writing string to " + label + "file at path \"" + filePath + "\"", DataUtils.getTruncatedCommandOutput(dataString, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES, true, false, true), "-")); + Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("Writing string to " + label + "file at path \"" + filePath + "\"", DataUtils.getTruncatedCommandOutput(dataString, Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-")); - String errmsg; + Error error; FileType fileType = getFileType(filePath, false); // If file exists but not a regular file if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) { - return context.getString(R.string.error_non_regular_file_found, label + "file"); + return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file"); } // Create the file parent directory - errmsg = createParentDirectoryFile(context, label + "file parent", filePath); - if (errmsg != null) - return errmsg; + error = createParentDirectoryFile(label + "file parent", filePath); + if (error != null) + return error; if (charset == null) charset = Charset.defaultCharset(); // Check if charset is supported - errmsg = isCharsetSupported(context, charset); - if (errmsg != null) - return errmsg; + error = isCharsetSupported(charset); + if (error != null) + return error; FileOutputStream fileOutputStream = null; BufferedWriter bufferedWriter = null; @@ -1385,7 +1327,7 @@ public class FileUtils { bufferedWriter.write(dataString); bufferedWriter.flush(); } catch (Exception e) { - return context.getString(R.string.error_writing_string_to_file_failed_with_exception, label + "file", filePath, e.getMessage()); + return FileUtilsErrno.ERRNO_WRITING_STRING_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage()); } finally { closeCloseable(fileOutputStream); closeCloseable(bufferedWriter); @@ -1399,19 +1341,18 @@ public class FileUtils { /** * Check if a specific {@link Charset} is supported. * - * @param context The {@link Context} to get error string. * @param charset The {@link Charset} to check. - * @return Returns the {@code errmsg} if charset is not supported or failed to check it, otherwise {@code null}. + * @return Returns the {@code error} if charset is not supported or failed to check it, otherwise {@code null}. */ - public static String isCharsetSupported(@NonNull final Context context, final Charset charset) { - if (charset == null) return context.getString(R.string.error_null_or_empty_parameter, "charset", "isCharsetSupported"); + 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 context.getString(R.string.error_unsupported_charset, charset.name()); + return FileUtilsErrno.ERRNO_UNSUPPORTED_CHARSET.getError(charset.name()); } } catch (Exception e) { - return context.getString(R.string.error_checking_if_charset_supported_failed, charset.name(), e.getMessage()); + return FileUtilsErrno.ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED.getError(e, charset.name(), e.getMessage()); } return null; @@ -1559,52 +1500,50 @@ public class FileUtils { /** * Checking missing permissions for file at path. * - * @param context The {@link Context} to get error string. * @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 errmsg} if validating permissions failed, otherwise {@code null}. + * @return Returns the {@code error} if validating permissions failed, otherwise {@code null}. */ - public static String checkMissingFilePermissions(@NonNull final Context context, final String filePath, final String permissionsToCheck, final boolean ignoreIfNotExecutable) { - return checkMissingFilePermissions(context, null, filePath, permissionsToCheck, ignoreIfNotExecutable); + 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 context The {@link Context} to get error string. * @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 errmsg} if validating permissions failed, otherwise {@code null}. + * @return Returns the {@code error} if validating permissions failed, otherwise {@code null}. */ - public static String checkMissingFilePermissions(@NonNull final Context context, String label, final String filePath, final String permissionsToCheck, final boolean ignoreIfNotExecutable) { + public static Error checkMissingFilePermissions(String label, final String filePath, final String permissionsToCheck, final boolean ignoreIfNotExecutable) { label = (label == null ? "" : label + " "); - if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "checkMissingFilePermissions"); + 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 context.getString(R.string.error_invalid_file_permissions_string_to_check); + 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 context.getString(R.string.error_file_not_readable, label + "file"); + return FileUtilsErrno.ERRNO_FILE_NOT_READABLE.getError(label + "file"); } // If file is not writable if (permissionsToCheck.contains("w") && !file.canWrite()) { - return context.getString(R.string.error_file_not_writable, label + "file"); + return FileUtilsErrno.ERRNO_FILE_NOT_WRITABLE.getError(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 context.getString(R.string.error_file_not_executable, label + "file"); + return FileUtilsErrno.ERRNO_FILE_NOT_EXECUTABLE.getError(label + "file"); } return null; @@ -1623,5 +1562,5 @@ public class FileUtils { if (string == null || string.isEmpty()) return false; return Pattern.compile("^([r-])[w-][x-]$", 0).matcher(string).matches(); } - + } diff --git a/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java b/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java new file mode 100644 index 00000000..7748a6bc --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java @@ -0,0 +1,123 @@ +package com.termux.shared.file; + +import android.os.Environment; + +import com.termux.shared.models.errors.Error; +import com.termux.shared.termux.TermuxConstants; + +import java.io.File; +import java.util.regex.Pattern; + +public class TermuxFileUtils { + /** + * Replace "$PREFIX/" or "~/" prefix with termux absolute paths. + * + * @param path The {@code path} to expand. + * @return Returns the {@code expand path}. + */ + public static String getExpandedTermuxPath(String path) { + if (path != null && !path.isEmpty()) { + path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH); + path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/"); + path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH); + path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/"); + } + + return path; + } + + /** + * Replace termux absolute paths with "$PREFIX/" or "~/" prefix. + * + * @param path The {@code path} to unexpand. + * @return Returns the {@code unexpand path}. + */ + public static String getUnExpandedTermuxPath(String path) { + if (path != null && !path.isEmpty()) { + path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/"); + path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/"); + } + + return path; + } + + /** + * Get canonical path. + * + * @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. + * @param expandPath The {@code boolean} that decides if input path is first attempted to be expanded by calling + * {@link TermuxFileUtils#getExpandedTermuxPath(String)} before its passed to + * {@link FileUtils#getCanonicalPath(String, String)}. + + * @return Returns the {@code canonical path}. + */ + public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath, final boolean expandPath) { + if (path == null) path = ""; + + if (expandPath) + path = getExpandedTermuxPath(path); + + return FileUtils.getCanonicalPath(path, prefixForNonAbsolutePath); + } + + /** + * Check if {@code path} is under the allowed termux working directory paths. If it is, then + * allowed parent path is returned. + * + * @param path The {@code path} to check. + * @return Returns the allowed path if it {@code path} is under it, otherwise {@link TermuxConstants#TERMUX_FILES_DIR_PATH}. + */ + public static String getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String path) { + if (path == null || path.isEmpty()) return TermuxConstants.TERMUX_FILES_DIR_PATH; + + if (path.startsWith(TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH + "/")) { + return TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH; + } if (path.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath() + "/")) { + return Environment.getExternalStorageDirectory().getAbsolutePath(); + } else if (path.startsWith("/sdcard/")) { + return "/sdcard"; + } else { + return TermuxConstants.TERMUX_FILES_DIR_PATH; + } + } + + /** + * Validate the existence and permissions of directory file at path as a working directory for + * termux app. + * + * The creation of missing directory and setting of missing permissions will only be done if + * {@code path} is under paths returned by {@link #getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String)}. + * + * The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}. + * + * @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 createDirectoryIfMissing The {@code boolean} that decides if directory file + * should be created if its missing. + * @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 boolean createDirectoryIfMissing, + final boolean setPermissions, final boolean setMissingPermissionsOnly, + final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) { + return FileUtils.validateDirectoryFileExistenceAndPermissions(label, filePath, + TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(filePath), createDirectoryIfMissing, + FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setPermissions, setMissingPermissionsOnly, + ignoreErrorsIfPathIsInParentDirPath, ignoreIfNotExecutable); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTypes.java b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTypes.java index c6a78d7a..cf599cd2 100644 --- a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTypes.java +++ b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTypes.java @@ -90,7 +90,7 @@ public class FileTypes { return getFileType(fileAttributes); } catch (Exception e) { // If not a ENOENT (No such file or directory) exception - if (!e.getMessage().contains("ENOENT")) + if (e.getMessage() != null && !e.getMessage().contains("ENOENT")) Logger.logError("Failed to get file type for file at path \"" + filePath + "\": " + e.getMessage()); return FileType.NO_EXIST; } diff --git a/termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java b/termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java index 47f4e86b..82205317 100644 --- a/termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java +++ b/termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import com.termux.shared.file.FileUtils; import com.termux.shared.logger.Logger; +import com.termux.shared.models.errors.Error; import java.io.File; import java.nio.charset.Charset; @@ -31,18 +32,19 @@ public class FileUtilsTests { Logger.logInfo(LOG_TAG, "Running tests"); Logger.logInfo(LOG_TAG, "testRootDirectoryPath: \"" + testRootDirectoryPath + "\""); - String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null, false); + String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null); assertEqual("FileUtilsTests directory path is not a canonical path", testRootDirectoryPath, fileUtilsTestsDirectoryCanonicalPath); - runTestsInner(context, testRootDirectoryPath); + runTestsInner(testRootDirectoryPath); Logger.logInfo(LOG_TAG, "All tests successful"); } catch (Exception e) { - Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage()); + Logger.logErrorExtended(LOG_TAG, e.getMessage()); + Logger.showToast(context, e.getMessage() != null ? e.getMessage().replaceAll("(?s)\nFull Error:\n.*", "") : null, true); } } - private static void runTestsInner(@NonNull final Context context, @NonNull final String testRootDirectoryPath) throws Exception { - String errmsg; + private static void runTestsInner(@NonNull final String testRootDirectoryPath) throws Exception { + Error error; String label; String path; @@ -101,20 +103,20 @@ public class FileUtilsTests { // Create or clear test root directory file label = "testRootDirectoryPath"; - errmsg = FileUtils.clearDirectory(context, label, testRootDirectoryPath); - assertEqual("Failed to create " + label + " directory file", null, errmsg); + error = FileUtils.clearDirectory(label, testRootDirectoryPath); + assertEqual("Failed to create " + label + " directory file", null, error); if (!FileUtils.directoryFileExists(testRootDirectoryPath, false)) throwException("The " + label + " directory file does not exist as expected after creation"); // Create dir1 directory file - errmsg = FileUtils.createDirectoryFile(context, dir1_label, dir1_path); - assertEqual("Failed to create " + dir1_label + " directory file", null, errmsg); + error = FileUtils.createDirectoryFile(dir1_label, dir1_path); + assertEqual("Failed to create " + dir1_label + " directory file", null, error); // Create dir2 directory file - errmsg = FileUtils.createDirectoryFile(context, dir2_label, dir2_path); - assertEqual("Failed to create " + dir2_label + " directory file", null, errmsg); + error = FileUtils.createDirectoryFile(dir2_label, dir2_path); + assertEqual("Failed to create " + dir2_label + " directory file", null, error); @@ -122,29 +124,29 @@ public class FileUtilsTests { // Create dir1/sub_dir1 directory file label = dir1__sub_dir1_label; path = dir1__sub_dir1_path; - errmsg = FileUtils.createDirectoryFile(context, label, path); - assertEqual("Failed to create " + label + " directory file", null, errmsg); + error = FileUtils.createDirectoryFile(label, path); + assertEqual("Failed to create " + label + " directory file", null, error); if (!FileUtils.directoryFileExists(path, false)) throwException("The " + label + " directory file does not exist as expected after creation"); // Create dir1/sub_reg1 regular file label = dir1__sub_reg1_label; path = dir1__sub_reg1_path; - errmsg = FileUtils.createRegularFile(context, label, path); - assertEqual("Failed to create " + label + " regular file", null, errmsg); + error = FileUtils.createRegularFile(label, path); + assertEqual("Failed to create " + label + " regular file", null, error); if (!FileUtils.regularFileExists(path, false)) throwException("The " + label + " regular file does not exist as expected after creation"); // Create dir1/sub_sym1 -> dir2 absolute symlink file label = dir1__sub_sym1_label; path = dir1__sub_sym1_path; - errmsg = FileUtils.createSymlinkFile(context, label, dir2_path, path); - assertEqual("Failed to create " + label + " symlink file", null, errmsg); + error = FileUtils.createSymlinkFile(label, dir2_path, path); + assertEqual("Failed to create " + label + " symlink file", null, error); if (!FileUtils.symlinkFileExists(path)) throwException("The " + label + " symlink file does not exist as expected after creation"); // Copy dir1/sub_sym1 symlink file to dir1/sub_sym2 label = dir1__sub_sym2_label; path = dir1__sub_sym2_path; - errmsg = FileUtils.copySymlinkFile(context, label, dir1__sub_sym1_path, path, false); - assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, errmsg); + error = FileUtils.copySymlinkFile(label, dir1__sub_sym1_path, path, false); + assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, error); if (!FileUtils.symlinkFileExists(path)) throwException("The " + label + " symlink file does not exist as expected after copying it from " + dir1__sub_sym1_label); if (!new File(path).getCanonicalPath().equals(dir2_path)) @@ -156,25 +158,25 @@ public class FileUtilsTests { // Write "line1" to dir2/sub_reg1 regular file label = dir2__sub_reg1_label; path = dir2__sub_reg1_path; - errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "line1", false); - assertEqual("Failed to write string to " + label + " file with append mode false", null, errmsg); + error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "line1", false); + assertEqual("Failed to write string to " + label + " file with append mode false", null, error); if (!FileUtils.regularFileExists(path, false)) throwException("The " + label + " file does not exist as expected after writing to it with append mode false"); // Write "line2" to dir2/sub_reg1 regular file - errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "\nline2", true); - assertEqual("Failed to write string to " + label + " file with append mode true", null, errmsg); + error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "\nline2", true); + assertEqual("Failed to write string to " + label + " file with append mode true", null, error); // Read dir2/sub_reg1 regular file StringBuilder dataStringBuilder = new StringBuilder(); - errmsg = FileUtils.readStringFromFile(context, label, path, Charset.defaultCharset(), dataStringBuilder, false); - assertEqual("Failed to read from " + label + " file", null, errmsg); + error = FileUtils.readStringFromFile(label, path, Charset.defaultCharset(), dataStringBuilder, false); + assertEqual("Failed to read from " + label + " file", null, error); assertEqual("The data read from " + label + " file in not as expected", "line1\nline2", dataStringBuilder.toString()); // Copy dir2/sub_reg1 regular file to dir2/sub_reg2 file label = dir2__sub_reg2_label; path = dir2__sub_reg2_path; - errmsg = FileUtils.copyRegularFile(context, label, dir2__sub_reg1_path, path, false); - assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, errmsg); + error = FileUtils.copyRegularFile(label, dir2__sub_reg1_path, path, false); + assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, error); if (!FileUtils.regularFileExists(path, false)) throwException("The " + label + " regular file does not exist as expected after copying it from " + dir2__sub_reg1_label); @@ -184,22 +186,22 @@ public class FileUtilsTests { // Copy dir1 directory file to dir3 label = dir3_label; path = dir3_path; - errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false); - assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg); + error = FileUtils.copyDirectoryFile(label, dir2_path, path, false); + assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error); if (!FileUtils.directoryFileExists(path, false)) throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label); // Copy dir1 directory file to dir3 again to test overwrite label = dir3_label; path = dir3_path; - errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false); - assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg); + error = FileUtils.copyDirectoryFile(label, dir2_path, path, false); + assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error); if (!FileUtils.directoryFileExists(path, false)) throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label); // Move dir3 directory file to dir4 label = dir4_label; path = dir4_path; - errmsg = FileUtils.moveDirectoryFile(context, label, dir3_path, path, false); - assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, errmsg); + error = FileUtils.moveDirectoryFile(label, dir3_path, path, false); + assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, error); if (!FileUtils.directoryFileExists(path, false)) throwException("The " + label + " directory file does not exist as expected after copying it from " + dir3_label); @@ -209,16 +211,16 @@ public class FileUtilsTests { // Create dir1/sub_sym3 -> dir4 relative symlink file label = dir1__sub_sym3_label; path = dir1__sub_sym3_path; - errmsg = FileUtils.createSymlinkFile(context, label, "../dir4", path); - assertEqual("Failed to create " + label + " symlink file", null, errmsg); + error = FileUtils.createSymlinkFile(label, "../dir4", path); + assertEqual("Failed to create " + label + " symlink file", null, error); if (!FileUtils.symlinkFileExists(path)) throwException("The " + label + " symlink file does not exist as expected after creation"); // Create dir1/sub_sym3 -> dirX relative dangling symlink file // This is to ensure that symlinkFileExists returns true if a symlink file exists but is dangling label = dir1__sub_sym3_label; path = dir1__sub_sym3_path; - errmsg = FileUtils.createSymlinkFile(context, label, "../dirX", path); - assertEqual("Failed to create " + label + " symlink file", null, errmsg); + error = FileUtils.createSymlinkFile(label, "../dirX", path); + assertEqual("Failed to create " + label + " symlink file", null, error); if (!FileUtils.symlinkFileExists(path)) throwException("The " + label + " dangling symlink file does not exist as expected after creation"); @@ -228,8 +230,8 @@ public class FileUtilsTests { // Delete dir1/sub_sym2 symlink file label = dir1__sub_sym2_label; path = dir1__sub_sym2_path; - errmsg = FileUtils.deleteSymlinkFile(context, label, path, false); - assertEqual("Failed to delete " + label + " symlink file", null, errmsg); + error = FileUtils.deleteSymlinkFile(label, path, false); + assertEqual("Failed to delete " + label + " symlink file", null, error); if (FileUtils.fileExists(path, false)) throwException("The " + label + " symlink file still exist after deletion"); @@ -245,8 +247,8 @@ public class FileUtilsTests { // Delete dir1 directory file label = dir1_label; path = dir1_path; - errmsg = FileUtils.deleteDirectoryFile(context, label, path, false); - assertEqual("Failed to delete " + label + " directory file", null, errmsg); + error = FileUtils.deleteDirectoryFile(label, path, false); + assertEqual("Failed to delete " + label + " directory file", null, error); if (FileUtils.fileExists(path, false)) throwException("The " + label + " directory file still exist after deletion"); @@ -267,8 +269,8 @@ public class FileUtilsTests { // Delete dir2/sub_reg1 regular file label = dir2__sub_reg1_label; path = dir2__sub_reg1_path; - errmsg = FileUtils.deleteRegularFile(context, label, path, false); - assertEqual("Failed to delete " + label + " regular file", null, errmsg); + error = FileUtils.deleteRegularFile(label, path, false); + assertEqual("Failed to delete " + label + " regular file", null, error); if (FileUtils.fileExists(path, false)) throwException("The " + label + " regular file still exist after deletion"); @@ -276,6 +278,14 @@ public class FileUtilsTests { FileUtils.getFileType("/dev/null", false); } + + + public static void assertEqual(@NonNull final String message, final String expected, final Error actual) throws Exception { + String actualString = actual != null ? actual.getMessage() : null; + if (!equalsRegardingNull(expected, actualString)) + throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actualString + "\"\nFull Error:\n" + (actual != null ? actual.toString() : "")); + } + public static void assertEqual(@NonNull final String message, final String expected, final String actual) throws Exception { if (!equalsRegardingNull(expected, actual)) throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\""); diff --git a/termux-shared/src/main/java/com/termux/shared/models/errors/Errno.java b/termux-shared/src/main/java/com/termux/shared/models/errors/Errno.java new file mode 100644 index 00000000..0c21f02b --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/models/errors/Errno.java @@ -0,0 +1,89 @@ +package com.termux.shared.models.errors; + +import android.app.Activity; + +import androidx.annotation.NonNull; + +import com.termux.shared.logger.Logger; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** The {@link Class} that defines error messages and codes. */ +public class Errno { + + public static final String TYPE = "Error"; + + + public static final Errno ERRNO_SUCCESS = new Errno(TYPE, Activity.RESULT_OK, "Success"); + public static final Errno ERRNO_MINOR_FAILURES = new Errno(TYPE, Activity.RESULT_FIRST_USER, "Minor failure"); + public static final Errno ERRNO_FAILED = new Errno(TYPE, Activity.RESULT_FIRST_USER + 1, "Failed"); + public static final Errno ERRNO_CANCELED = new Errno(TYPE, Activity.RESULT_FIRST_USER + 2, "Cancelled"); + + /** The errno type. */ + protected String type; + /** The errno code. */ + protected final int code; + /** The errno message. */ + protected final String message; + + private static final String LOG_TAG = "Errno"; + + + public Errno(final String type, final int code, final String message) { + this.type = type; + this.code = code; + this.message = message; + } + + @NonNull + @Override + public String toString() { + return "type=" + type + ", code=" + code + ", message=\"" + message + "\""; + } + + + public String getType() { + return type; + } + + public String getMessage() { + return message; + } + + public int getCode() { + return code; + } + + + + public Error getError() { + return new Error(getType(), getCode(), getMessage()); + } + + public Error getError(Object... args) { + try { + return new Error(getType(), getCode(), String.format(getMessage(), args)); + } catch (Exception e) { + Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage()); + // Return unformatted message as a backup + return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args)); + } + } + + public Error getError(Throwable throwable, Object... args) { + return getError(Collections.singletonList(throwable), args); + } + + public Error getError(List throwablesList, Object... args) { + try { + return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList); + } catch (Exception e) { + Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage()); + // Return unformatted message as a backup + return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args), throwablesList); + } + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/models/errors/Error.java b/termux-shared/src/main/java/com/termux/shared/models/errors/Error.java new file mode 100644 index 00000000..b9dba33f --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/models/errors/Error.java @@ -0,0 +1,249 @@ +package com.termux.shared.models.errors; + +import androidx.annotation.NonNull; + +import com.termux.shared.logger.Logger; +import com.termux.shared.markdown.MarkdownUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Error implements Serializable { + + /** The error type. */ + private String type; + /** The error code. */ + private int code; + /** The error message. */ + private String message; + /** The error exceptions. */ + private List throwablesList = new ArrayList<>(); + + private static final String LOG_TAG = "Error"; + + + public Error() { + InitError(null, null, null, null); + } + + public Error(String type, Integer code, String message, List throwablesList) { + InitError(type, code, message, throwablesList); + } + + public Error(String type, Integer code, String message, Throwable throwable) { + InitError(type, code, message, Collections.singletonList(throwable)); + } + + public Error(String type, Integer code, String message) { + InitError(type, code, message, null); + } + + public Error(Integer code, String message, List throwablesList) { + InitError(null, code, message, throwablesList); + } + + public Error(Integer code, String message, Throwable throwable) { + InitError(null, code, message, Collections.singletonList(throwable)); + } + + public Error(Integer code, String message) { + InitError(null, code, message, null); + } + + public Error(String message, Throwable throwable) { + InitError(null, null, message, Collections.singletonList(throwable)); + } + + public Error(String message, List throwablesList) { + InitError(null, null, message, throwablesList); + } + + public Error(String message) { + InitError(null, null, message, null); + } + + private void InitError(String type, Integer code, String message, List throwablesList) { + if (type != null && !type.isEmpty()) + this.type = type; + else + this.type = Errno.TYPE; + + if (code != null && code > Errno.ERRNO_SUCCESS.getCode()) + this.code = code; + else + this.code = Errno.ERRNO_SUCCESS.getCode(); + + this.message = message; + this.throwablesList = throwablesList; + } + + + public String getType() { + return type; + } + + public Integer getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public void prependMessage(String message) { + if (message != null && isStateFailed()) + this.message = message + this.message; + } + + public void appendMessage(String message) { + if (message != null && isStateFailed()) + this.message = this.message + message; + } + + public List getThrowablesList() { + return Collections.unmodifiableList(throwablesList); + } + + + public synchronized boolean setStateFailed(@NonNull Error error) { + return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null); + } + + public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) { + return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable)); + } + public synchronized boolean setStateFailed(@NonNull Error error, List throwablesList) { + return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList); + } + + public synchronized boolean setStateFailed(int code, String message) { + return setStateFailed(this.type, code, message, null); + } + + public synchronized boolean setStateFailed(int code, String message, Throwable throwable) { + return setStateFailed(this.type, code, message, Collections.singletonList(throwable)); + } + + public synchronized boolean setStateFailed(int code, String message, List throwablesList) { + return setStateFailed(this.type, code, message, throwablesList); + } + + public synchronized boolean setStateFailed(String type, int code, String message, List throwablesList) { + this.message = message; + this.throwablesList = throwablesList; + + if (type != null && !type.isEmpty()) + this.type = type; + + if (code > Errno.ERRNO_SUCCESS.getCode()) { + this.code = code; + return true; + } else { + Logger.logWarn(LOG_TAG, "Ignoring invalid error code value \"" + code + "\". Force setting it to RESULT_CODE_FAILED \"" + Errno.ERRNO_FAILED.getCode() + "\""); + this.code = Errno.ERRNO_FAILED.getCode(); + return false; + } + } + + public boolean isStateFailed() { + return code > Errno.ERRNO_SUCCESS.getCode(); + } + + + @NonNull + @Override + public String toString() { + return getErrorLogString(this); + } + + /** + * Get a log friendly {@link String} for {@link Error} error parameters. + * + * @param error The {@link Error} to convert. + * @return Returns the log friendly {@link String}. + */ + public static String getErrorLogString(final Error error) { + if (error == null) return "null"; + + StringBuilder logString = new StringBuilder(); + + logString.append(error.getCodeString()); + logString.append("\n").append(error.getTypeAndMessageLogString()); + if (error.throwablesList != null) + logString.append("\n").append(error.geStackTracesLogString()); + + return logString.toString(); + } + + /** + * Get a minimal log friendly {@link String} for {@link Error} error parameters. + * + * @param error The {@link Error} to convert. + * @return Returns the log friendly {@link String}. + */ + public static String getMinimalErrorLogString(final Error error) { + if (error == null) return "null"; + + StringBuilder logString = new StringBuilder(); + + logString.append(error.getCodeString()); + logString.append(error.getTypeAndMessageLogString()); + + return logString.toString(); + } + + /** + * Get a minimal {@link String} for {@link Error} error parameters. + * + * @param error The {@link Error} to convert. + * @return Returns the {@link String}. + */ + public static String getMinimalErrorString(final Error error) { + if (error == null) return "null"; + + StringBuilder logString = new StringBuilder(); + + logString.append("(").append(error.getCode()).append(") "); + logString.append(error.getType()).append(": ").append(error.getMessage()); + + return logString.toString(); + } + + /** + * Get a markdown {@link String} for {@link Error}. + * + * @param error The {@link Error} to convert. + * @return Returns the markdown {@link String}. + */ + public static String getErrorMarkdownString(final Error error) { + if (error == null) return "null"; + + StringBuilder markdownString = new StringBuilder(); + + markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Error Code", error.getCode(), "-")); + markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry((Errno.TYPE.equals(error.getType()) ? "Error Message" : "Error Message (" + error.getType() + ")"), error.message, "-")); + markdownString.append("\n\n").append(error.geStackTracesMarkdownString()); + + return markdownString.toString(); + } + + + public String getCodeString() { + return Logger.getSingleLineLogStringEntry("Error Code", code, "-"); + } + + public String getTypeAndMessageLogString() { + return Logger.getMultiLineLogStringEntry(Errno.TYPE.equals(type) ? "Error Message" : "Error Message (" + type + ")", message, "-"); + } + + public String geStackTracesLogString() { + return Logger.getStackTracesString("StackTraces:", Logger.getStackTracesStringArray(throwablesList)); + } + + public String geStackTracesMarkdownString() { + return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTracesStringArray(throwablesList)); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/models/errors/FileUtilsErrno.java b/termux-shared/src/main/java/com/termux/shared/models/errors/FileUtilsErrno.java new file mode 100644 index 00000000..2935d7a4 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/models/errors/FileUtilsErrno.java @@ -0,0 +1,81 @@ +package com.termux.shared.models.errors; + +/** The {@link Class} that defines FileUtils error messages and codes. */ +public class FileUtilsErrno extends Errno { + + public static final String TYPE = "FileUtils Error"; + + + /* Errors for null or empty paths (100-150) */ + public static final Errno ERRNO_EXECUTABLE_REQUIRED = new Errno(TYPE, 100, "Executable required."); + public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE_PATH = new Errno(TYPE, 101, "The regular file path is null or empty."); + public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE = new Errno(TYPE, 102, "The regular file is null or empty."); + public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE_PATH = new Errno(TYPE, 103, "The executable file path is null or empty."); + public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE = new Errno(TYPE, 104, "The executable file is null or empty."); + public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE_PATH = new Errno(TYPE, 105, "The directory file path is null or empty."); + public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE = new Errno(TYPE, 106, "The directory file is null or empty."); + + + + /* Errors for invalid or not found files at path (150-200) */ + public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH = new Errno(TYPE, 150, "The %1$s is not found at path \"%2$s\"."); + + public static final Errno ERRNO_NO_REGULAR_FILE_FOUND = new Errno(TYPE, 151, "Regular file not found at %1$s path."); + public static final Errno ERRNO_NOT_A_REGULAR_FILE = new Errno(TYPE, 152, "The %1$s at path \"%2$s\" is not a regular file."); + + public static final Errno ERRNO_NON_REGULAR_FILE_FOUND = new Errno(TYPE, 153, "Non-regular file found at %1$s path."); + public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND = new Errno(TYPE, 154, "Non-directory file found at %1$s path."); + public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 155, "Non-symlink file found at %1$s path."); + + public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 156, "The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\"."); + + public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 157, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s"); + public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 158, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s"); + + + + /* Errors for file creation (200-250) */ + public static final Errno ERRNO_CREATING_FILE_FAILED = new Errno(TYPE, 200, "Creating %1$s at path \"%2$s\" failed."); + public static final Errno ERRNO_CREATING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 201, "Creating %1$s at path \"%2$s\" failed.\nException: %3$s"); + + public static final Errno ERRNO_CANNOT_OVERWRITE_A_NON_SYMLINK_FILE_TYPE = new Errno(TYPE, 202, "Cannot overwrite %1$s while creating symlink at \"%2$s\" to \"%3$s\" since destination file type \"%4$s\" is not a symlink."); + public static final Errno ERRNO_CREATING_SYMLINK_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 203, "Creating %1$s at path \"%2$s\" to \"%3$s\" failed.\nException: %4$s"); + + + + /* Errors for file copying and moving (250-300) */ + public static final Errno ERRNO_COPYING_OR_MOVING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 250, "%1$s from \"%2$s\" to \"%3$s\" failed.\nException: %4$s"); + public static final Errno ERRNO_COPYING_OR_MOVING_FILE_TO_SAME_PATH = new Errno(TYPE, 251, "%1$s from \"%2$s\" to \"%3$s\" cannot be done since they point to the same path."); + public static final Errno ERRNO_CANNOT_OVERWRITE_A_DIFFERENT_FILE_TYPE = new Errno(TYPE, 252, "Cannot overwrite %1$s while %2$s it from \"%3$s\" to \"%4$s\" since destination file type \"%5$s\" is different from source file type \"%6$s\"."); + public static final Errno ERRNO_CANNOT_MOVE_DIRECTORY_TO_SUB_DIRECTORY_OF_ITSELF = new Errno(TYPE, 253, "Cannot move %1$s from \"%2$s\" to \"%3$s\" since destination is a subdirectory of the source."); + + + + /* Errors for file deletion (300-350) */ + public static final Errno ERRNO_DELETING_FILE_FAILED = new Errno(TYPE, 300, "Deleting %1$s at path \"%2$s\" failed."); + public static final Errno ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 301, "Deleting %1$s at path \"%2$s\" failed.\nException: %3$s"); + public static final Errno ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 302, "Clearing %1$s at path \"%2$s\" failed.\nException: %3$s"); + public static final Errno ERRNO_FILE_STILL_EXISTS_AFTER_DELETING = new Errno(TYPE, 303, "The %1$s still exists after deleting it from \"%2$s\"."); + + + + /* Errors for file reading and writing (350-400) */ + public static final Errno ERRNO_READING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 350, "Reading string from %1$s at path \"%2$s\" failed.\nException: %3$s"); + public static final Errno ERRNO_WRITING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 351, "Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s"); + public static final Errno ERRNO_UNSUPPORTED_CHARSET = new Errno(TYPE, 352, "Unsupported charset \"%1$s\""); + public static final Errno ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED = new Errno(TYPE, 353, "Checking if charset \"%1$s\" is supported failed.\nException: %2$s"); + + + + /* Errors for invalid file permissions (400-450) */ + public static final Errno ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK = new Errno(TYPE, 400, "The file permission string to check is invalid."); + public static final Errno ERRNO_FILE_NOT_READABLE = new Errno(TYPE, 401, "The %1$s at path is not readable. Permission Denied."); + public static final Errno ERRNO_FILE_NOT_WRITABLE = new Errno(TYPE, 402, "The %1$s at path is not writable. Permission Denied."); + public static final Errno ERRNO_FILE_NOT_EXECUTABLE = new Errno(TYPE, 403, "The %1$s at path is not executable. Permission Denied."); + + + FileUtilsErrno(final String type, final int code, final String message) { + super(type, code, message); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/models/errors/FunctionErrno.java b/termux-shared/src/main/java/com/termux/shared/models/errors/FunctionErrno.java new file mode 100644 index 00000000..8d099b34 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/models/errors/FunctionErrno.java @@ -0,0 +1,20 @@ +package com.termux.shared.models.errors; + +/** The {@link Class} that defines function error messages and codes. */ +public class FunctionErrno extends Errno { + + public static final String TYPE = "Function Error"; + + + /* Errors for null or empty parameters (100-150) */ + public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETER = new Errno(TYPE, 100, "The %1$s parameter passed to \"%2$s\" is null or empty."); + public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETERS = new Errno(TYPE, 101, "The %1$s parameters passed to \"%2$s\" are null or empty."); + public static final Errno ERRNO_UNSET_PARAMETER = new Errno(TYPE, 102, "The %1$s parameter passed to \"%2$s\" must be set."); + public static final Errno ERRNO_UNSET_PARAMETERS = new Errno(TYPE, 103, "The %1$s parameters passed to \"%2$s\" must be set."); + + + FunctionErrno(final String type, final int code, final String message) { + super(type, code, message); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java b/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java index fe1efc8d..89c66f3c 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java @@ -4,6 +4,7 @@ import android.content.Context; import androidx.annotation.NonNull; +import com.termux.shared.models.errors.Error; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.file.FileUtils; import com.termux.shared.logger.Logger; @@ -150,14 +151,14 @@ public class ShellUtils { return (lastSlash == -1) ? executable : executable.substring(lastSlash + 1); } - public static void clearTermuxTMPDIR(Context context, boolean onlyIfExists) { + public static void clearTermuxTMPDIR(boolean onlyIfExists) { if(onlyIfExists && !FileUtils.directoryFileExists(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, false)) return; - String errmsg; - errmsg = FileUtils.clearDirectory(context, "$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null, false)); - if (errmsg != null) { - Logger.logError(errmsg); + Error error; + error = FileUtils.clearDirectory("$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null)); + if (error != null) { + Logger.logErrorExtended(error.toString()); } } diff --git a/termux-shared/src/main/res/values/strings.xml b/termux-shared/src/main/res/values/strings.xml index b1049f8e..21a85046 100644 --- a/termux-shared/src/main/res/values/strings.xml +++ b/termux-shared/src/main/res/values/strings.xml @@ -13,52 +13,7 @@ - - Executable required. - The %1$s is to \"%2$s\" null or empty. - The regular file path is null or empty. - The regular file is null or empty. - The executable file path is null or empty. - The executable file is null or empty. - The directory file path is null or empty. - The directory file is null or empty. - - The %1$s is not found at path \"%2$s\". - Regular file not found at %1$s path. - The %1$s at path \"%2$s\" is not a regular file. - Non-regular file found at %1$s path. - Non-directory file found at %1$s path. - Non-symlink file found at %1$s path. - The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\". - - Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s - Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s - - Creating %1$s at path \"%2$s\" failed. - Creating %1$s at path \"%2$s\" failed.\nException: %3$s - - Cannot overwrite %1$s while creating symlink at \"%2$s\" to \"%3$s\" since destination file type \"%4$s\" is not a symlink. - Creating %1$s at path \"%2$s\" to \"%3$s\" failed.\nException: %4$s - - %1$s from \"%2$s\" to \"%3$s\" failed.\nException: %4$s - %1$s from \"%2$s\" to \"%3$s\" cannot be done since they point to the same path. - Cannot overwrite %1$s while %2$s it from \"%3$s\" to \"%4$s\" since destination file type \"%5$s\" is different from source file type \"%6$s\". - Cannot move %1$s from \"%2$s\" to \"%3$s\" since destination is a subdirectory of the source. - - The %1$s still exists after deleting it from \"%2$s\". - Deleting %1$s at path \"%2$s\" failed. - Deleting %1$s at path \"%2$s\" failed.\nException: %3$s - Clearing %1$s at path \"%2$s\" failed.\nException: %3$s - - Reading string from %1$s at path \"%2$s\" failed.\nException: %3$s - Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s - Unsupported charset \"%1$s\" - Checking if charset \"%1$s\" is suppoted failed.\nException: %2$s - - The file permission string to check is invalid. - The %1$s at path is not readable. Permission Denied. - The %1$s at path is not writable. Permission Denied. - The %1$s at path is not executable. Permission Denied. + %1$s Directory Absolute Path: \"%2$s\"