From a2df7d791a5c09f9ed16e5755bb15ab871cee9e6 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 18 Jun 2022 05:53:26 +0500 Subject: [PATCH] Fixed: Fix bootstrap not installing on app install Previously, bootstrap was only installed if `$PREFIX` didn't exist, was empty or only had `$PREFIX/tmp`. But now with 03e1d14e, `$PREFIX/etc/termux/termux.env` was also created at app startup before bootstrap check was made, hence it was being assumed that bootstrap was already installed. Now, bootstrap will be installed even if `$PREFIX/tmp`, `$PREFIX/etc/termux/termux.env.tmp` or `$PREFIX/etc/termux/termux.env` exist but no other files do. Closes #2844 --- .../java/com/termux/app/TermuxInstaller.java | 11 +- .../java/com/termux/shared/errors/Errno.java | 5 + .../com/termux/shared/file/FileUtils.java | 113 ++++++++++++++++++ .../termux/shared/file/FileUtilsErrno.java | 6 +- .../shared/file/tests/FileUtilsTests.java | 86 +++++++++++++ .../termux/shared/termux/TermuxConstants.java | 10 +- .../shared/termux/file/TermuxFileUtils.java | 17 +++ 7 files changed, 241 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index 504b515f..b1414999 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -21,6 +21,7 @@ import com.termux.shared.errors.Error; import com.termux.shared.android.PackageUtils; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxUtils; +import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -103,10 +104,8 @@ final class TermuxInstaller { // If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) { - File[] PREFIX_FILE_LIST = TERMUX_PREFIX_DIR.listFiles(); - // If prefix directory is empty or only contains the tmp directory - if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) { - Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains the tmp directory."); + if (TermuxFileUtils.isTermuxPrefixDirectoryEmpty()) { + Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains specific unimportant files."); } else { whenDone.run(); return; @@ -218,6 +217,10 @@ final class TermuxInstaller { } Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully."); + + // Recreate env file since termux prefix was wiped earlier + TermuxShellEnvironment.writeEnvironmentToFile(activity); + activity.runOnUiThread(whenDone); } catch (final Exception e) { diff --git a/termux-shared/src/main/java/com/termux/shared/errors/Errno.java b/termux-shared/src/main/java/com/termux/shared/errors/Errno.java index 1ac9fd08..5dfa745c 100644 --- a/termux-shared/src/main/java/com/termux/shared/errors/Errno.java +++ b/termux-shared/src/main/java/com/termux/shared/errors/Errno.java @@ -110,4 +110,9 @@ public class Errno { } } + public boolean equalsErrorTypeAndCode(Error error) { + if (error == null) return false; + return type.equals(error.getType()) && code == error.getCode(); + } + } 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 308bc77e..602c4982 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 @@ -33,6 +33,7 @@ import java.io.Serializable; import java.nio.charset.Charset; import java.nio.file.LinkOption; import java.nio.file.StandardCopyOption; +import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Iterator; @@ -185,6 +186,118 @@ public class FileUtils { } + + /** + * Validate that directory is empty or contains only files in {@code ignoredSubFilePaths}. + * + * If parent path of an ignored file exists, but ignored file itself does not exist, then directory + * is not considered empty. + * + * @param label The optional label for directory to check. This can optionally be {@code null}. + * @param filePath The {@code path} for directory to check. + * @param ignoredSubFilePaths The list of absolute file paths under {@code filePath} dir. + * Validation is done for the paths. + * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an + * error if file to be checked doesn't exist. + * @return Returns {@code null} if directory is empty or contains only files in {@code ignoredSubFilePaths}. + * Returns {@code FileUtilsErrno#ERRNO_NON_EMPTY_DIRECTORY_FILE} if a file was found that did not + * exist in the {@code ignoredSubFilePaths}, otherwise returns an appropriate {@code error} if + * checking was not successful. + */ + public static Error validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(String label, String filePath, + final List ignoredSubFilePaths, + final boolean ignoreNonExistentFile) { + label = (label == null || label.isEmpty() ? "" : label + " "); + if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "isDirectoryFileEmptyOrOnlyContainsSpecificFiles"); + + try { + File file = new File(filePath); + FileType fileType = getFileType(filePath, false); + + // If file exists but not a directory file + if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) { + return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory"); + } + + // If file does not exist + if (fileType == FileType.NO_EXIST) { + // If checking is to be ignored if file does not exist + if (ignoreNonExistentFile) + return null; + else { + label += "directory to check if is empty or only contains specific files"; + return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label); + } + } + + File[] subFiles = file.listFiles(); + if (subFiles == null || subFiles.length == 0) + return null; + + // If sub files exists but no file should be ignored + if (ignoredSubFilePaths == null || ignoredSubFilePaths.size() == 0) + return FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.getError(label, filePath); + + // If a sub file does not exist in ignored file path + if (nonIgnoredSubFileExists(subFiles, ignoredSubFilePaths)) { + return FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.getError(label, filePath); + } + + } catch (Exception e) { + return FileUtilsErrno.ERRNO_VALIDATE_DIRECTORY_EMPTY_OR_ONLY_CONTAINS_SPECIFIC_FILES_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, e.getMessage()); + } + + return null; + } + + /** + * Check if {@code subFiles} contains contains a file not in {@code ignoredSubFilePaths}. + * + * If parent path of an ignored file exists, but ignored file itself does not exist, then directory + * is not considered empty. + * + * This function should ideally not be called by itself but through + * {@link #validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(String, String, List, boolean)}. + * + * @param subFiles The list of files of a directory to check. + * @param ignoredSubFilePaths The list of absolute file paths under {@code filePath} dir. + * Validation is done for the paths. + * @return Returns {@code true} if a file was found that did not exist in the {@code ignoredSubFilePaths}, + * otherwise {@code false}. + */ + public static boolean nonIgnoredSubFileExists(File[] subFiles, @NonNull List ignoredSubFilePaths) { + if (subFiles == null || subFiles.length == 0) return false; + + String subFilePath; + for (File subFile : subFiles) { + subFilePath = subFile.getAbsolutePath(); + // If sub file does not exist in ignored sub file paths + if (!ignoredSubFilePaths.contains(subFilePath)) { + boolean isParentPath = false; + for (String ignoredSubFilePath : ignoredSubFilePaths) { + if (ignoredSubFilePath.startsWith(subFilePath + "/") && fileExists(ignoredSubFilePath, false)) { + isParentPath = true; + break; + } + } + // If sub file is not a parent of any existing ignored sub file paths + if (!isParentPath) { + return true; + } + } + + if (getFileType(subFilePath, false) == FileType.DIRECTORY) { + // If non ignored sub file found, then early exit, otherwise continue looking + if (nonIgnoredSubFileExists(subFile.listFiles(), ignoredSubFilePaths)) + return true; + } + } + + return false; + } + + + /** * Checks whether a regular file exists at {@code filePath}. * diff --git a/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java b/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java index 4a0d73f4..7e336aad 100644 --- a/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java +++ b/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java @@ -34,9 +34,11 @@ public class FileUtilsErrno extends Errno { public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND_SHORT = new Errno(TYPE, 157, "Non-symlink file found at %1$s path."); public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 158, "The %1$s found at path \"%2$s\" of type \"%3$s\" is not one of allowed file types \"%4$s\"."); + public static final Errno ERRNO_NON_EMPTY_DIRECTORY_FILE = new Errno(TYPE, 159, "The %1$s directory at path \"%2$s\" is not empty."); - public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 159, "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, 160, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s"); + public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 160, "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, 161, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s"); + public static final Errno ERRNO_VALIDATE_DIRECTORY_EMPTY_OR_ONLY_CONTAINS_SPECIFIC_FILES_FAILED_WITH_EXCEPTION = new Errno(TYPE, 162, "Validating directory is empty or only contains specific files of %1$s at path \"%2$s\" failed.\nException: %3$s"); 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 927a20a8..bf1c210c 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 @@ -4,12 +4,16 @@ import android.content.Context; import androidx.annotation.NonNull; +import com.termux.shared.errors.Errno; import com.termux.shared.file.FileUtils; +import com.termux.shared.file.FileUtilsErrno; import com.termux.shared.logger.Logger; import com.termux.shared.errors.Error; import java.io.File; import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; public class FileUtilsTests { @@ -68,6 +72,15 @@ public class FileUtilsTests { String dir1__sub_dir1_label = "dir1/sub_dir1"; String dir1__sub_dir1_path = dir1_path + "/sub_dir1"; + String dir1__sub_dir2_label = "dir1/sub_dir2"; + String dir1__sub_dir2_path = dir1_path + "/sub_dir2"; + + String dir1__sub_dir3_label = "dir1/sub_dir3"; + String dir1__sub_dir3_path = dir1_path + "/sub_dir3"; + + String dir1__sub_dir3__sub_reg1_label = "dir1/sub_dir3/sub_reg1"; + String dir1__sub_dir3__sub_reg1_path = dir1__sub_dir3_path + "/sub_reg1"; + String dir1__sub_reg1_label = "dir1/sub_reg1"; String dir1__sub_reg1_path = dir1_path + "/sub_reg1"; @@ -274,6 +287,72 @@ public class FileUtilsTests { if (FileUtils.fileExists(path, false)) throwException("The " + label + " regular file still exist after deletion"); + + List ignoredSubFilePaths = Arrays.asList(dir1__sub_dir2_path, dir1__sub_dir3__sub_reg1_path); + + // Create dir1 directory file + error = FileUtils.createDirectoryFile(dir1_label, dir1_path); + assertEqual("Failed to create " + dir1_label + " directory file", null, error); + + // Test empty dir + error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false); + assertEqual("Failed to validate if " + dir1_label + " directory file is empty", null, error); + + + // Create dir1/sub_dir3 directory file + label = dir1__sub_dir3_label; path = dir1__sub_dir3_path; + 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"); + + // Test parent dir existing of non existing ignored regular file + error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false); + assertErrnoEqual("Failed to validate if " + dir1_label + " directory file is empty with parent dir existing of non existing ignored regular file", FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE, error); + + + // Write "line1" to dir1/sub_dir3/sub_reg1 regular file + label = dir1__sub_dir3__sub_reg1_label; path = dir1__sub_dir3__sub_reg1_path; + error = FileUtils.writeTextToFile(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"); + + // Test ignored regular file existing + error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false); + assertEqual("Failed to validate if " + dir1_label + " directory file is empty with ignored regular file existing", null, error); + + + // Create dir1/sub_dir2 directory file + label = dir1__sub_dir2_label; path = dir1__sub_dir2_path; + 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"); + + // Test ignored dir file existing + error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false); + assertEqual("Failed to validate if " + dir1_label + " directory file is empty with ignored dir file existing", null, error); + + + // Create dir1/sub_dir1 directory file + label = dir1__sub_dir1_label; path = dir1__sub_dir1_path; + 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"); + + // Test non ignored dir file existing + error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false); + assertErrnoEqual("Failed to validate if " + dir1_label + " directory file is empty with non ignored dir file existing", FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE, error); + + + // Delete dir1 directory file + label = dir1_label; path = dir1_path; + error = FileUtils.deleteDirectoryFile(label, path, false); + assertEqual("Failed to delete " + label + " directory file", null, error); + + FileUtils.getFileType("/dev/ptmx", false); FileUtils.getFileType("/dev/null", false); } @@ -299,6 +378,13 @@ public class FileUtilsTests { return isEquals(expected, actual); } + public static void assertErrnoEqual(@NonNull final String message, final Errno expected, final Error actual) throws Exception { + if ((expected == null && actual != null) || (expected != null && !expected.equalsErrorTypeAndCode(actual))) + throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\"\nFull Error:\n" + (actual != null ? actual.toString() : "")); + } + + + private static boolean isEquals(String expected, String actual) { return expected.equals(actual); } diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java index 203f514f..a3cc5dd3 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java @@ -11,7 +11,7 @@ import java.util.Formatter; import java.util.List; /* - * Version: v0.51.0 + * Version: v0.52.0 * SPDX-License-Identifier: MIT * * Changelog @@ -274,6 +274,9 @@ import java.util.List; * * - 0.51.0 (2022-06-13) * - Added `TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME` and `TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME`. + * + * - 0.52.0 (2022-06-18) + * - Added `TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY`. */ /** @@ -681,6 +684,11 @@ public final class TermuxConstants { public static final File TERMUX_APPS_DIR = new File(TERMUX_APPS_DIR_PATH); + /** Termux app $PREFIX directory path ignored sub file paths to consider it empty */ + public static final List TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY = Arrays.asList( + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, TermuxConstants.TERMUX_ENV_TEMP_FILE_PATH, TermuxConstants.TERMUX_ENV_FILE_PATH); + + /* * Termux app and plugin preferences and properties file paths. diff --git a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java index 2f73c689..6fc79a7e 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java @@ -1,5 +1,7 @@ package com.termux.shared.termux.file; +import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH; + import android.content.Context; import android.os.Environment; @@ -325,6 +327,21 @@ public class TermuxFileUtils { false, false); } + /** + * If {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} doesn't exist, is empty or only contains + * files in {@link TermuxConstants#TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY}. + */ + public static boolean isTermuxPrefixDirectoryEmpty() { + Error error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles("termux prefix", + TERMUX_PREFIX_DIR_PATH, TermuxConstants.TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY, true); + if (error == null) + return true; + + if (!FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.equalsErrorTypeAndCode(error)) + Logger.logErrorExtended(LOG_TAG, "Failed to check if termux prefix directory is empty:\n" + error.getErrorLogString()); + return false; + } + /** * Get a markdown {@link String} for stat output for various Termux app files paths. *