Files
termux-app/app/src/main/java/com/termux/app/utils/FileUtils.java
agnostic-apollo b4995ef9a7 Refactor RunCommandService
- The `FileUtils` and `PluginUtils` have been added to provide utility functions.
- The executable and working directory validation has been added to check for existence and missing permissions.
- The `expandPath()` function is removed from `RunCommandService`.
- Working directory will automatically be created if under `TermuxConstants.TERMUX_FILES_DIR_PATH` if missing.
- Better logging has been added. This will later be used to notify the user in foreground.
- Javadocs have been updated.
2021-03-19 21:18:35 +05:00

377 lines
17 KiB
Java

package com.termux.app.utils;
import android.content.Context;
import com.termux.R;
import com.termux.app.TermuxConstants;
import java.io.File;
import java.util.regex.Pattern;
public class FileUtils {
private static final String LOG_TAG = "FileUtils";
/**
* 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;
}
/**
* 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 not an absolute path and {code prefixForNonAbsolutePath} is not {@code null}, then
* {code prefixForNonAbsolutePath} + "/" is prefixed before path before getting canonical path.
* If path is not an absolute path and {code prefixForNonAbsolutePath} is {@code null}, then
* "/" is prefixed before path before getting canonical path.
*
* If an exception is raised to get the canonical path, then absolute path is returned.
*
* @param path The {@code path} to convert.
* @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
* can be set to {@code null} if non-absolute paths should
* be prefixed with "/". The call to {@link File#getCanonicalPath()}
* will automatically do this anyways.
* @return Returns the {@code canonical path}.
*/
public static String getCanonicalPath(String path, String prefixForNonAbsolutePath, boolean expandPath) {
if (path == null) path = "";
if(expandPath)
path = getExpandedTermuxPath(path);
String absolutePath;
// If path is already an absolute path
if (path.startsWith("/") ) {
absolutePath = path;
} else {
if (prefixForNonAbsolutePath != null)
absolutePath = prefixForNonAbsolutePath + "/" + path;
else
absolutePath = "/" + path;
}
try {
return new File(absolutePath).getCanonicalPath();
} catch(Exception e) {
}
return absolutePath;
}
/**
* Removes one or more forward slashes "//" with single slash "/"
* Removes "./"
* Removes trailing forward slash "/"
*
* @param path The {@code path} to convert.
* @return Returns the {@code normalized path}.
*/
public static String normalizePath(String path) {
if (path == null) return null;
path = path.replaceAll("/+", "/");
path = path.replaceAll("\\./", "");
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return path;
}
/**
* Determines whether path is in {@code dirPath}.
*
* @param path The {@code path} to check.
* @param dirPath The {@code directory path} to check in.
* @param ensureUnder If set to {@code true}, then it will be ensured that {@code path} is
* under the directory and does not equal it.
* @return Returns {@code true} if path in {@code dirPath}, otherwise returns {@code false}.
*/
public static boolean isPathInDirPath(String path, String dirPath, boolean ensureUnder) {
if (path == null || dirPath == null) return false;
try {
path = new File(path).getCanonicalPath();
} catch(Exception e) {
return false;
}
String normalizedDirPath = normalizePath(dirPath);
if(ensureUnder)
return !path.equals(normalizedDirPath) && path.startsWith(normalizedDirPath + "/");
else
return path.startsWith(normalizedDirPath + "/");
}
/**
* Validate the existence and permissions of regular file at path.
*
* If the {@code parentDirPath} is not {@code null}, then setting of missing permissions will
* only be done if {@code path} is under {@code parentDirPath}.
*
* @param context The {@link Context} to get error string.
* @param path The {@code path} for file to validate.
* @param parentDirPath The optional {@code parent directory path} to restrict operations to.
* This can optionally be {@code null}.
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
* @param setMissingPermissions The {@code boolean} that decides if missing permissions are to be
* automatically set.
* @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
* failed, otherwise {@code null}.
*/
public static String validateRegularFileExistenceAndPermissions(final Context context, final String path, final String parentDirPath, String permissionsToCheck, final boolean setMissingPermissions, final boolean ignoreErrorsIfPathIsUnderParentDirPath) {
if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_file);
try {
File file = new File(path);
// If file exits but not a regular file
if (file.exists() && !file.isFile()) {
return context.getString(R.string.non_regular_file_found);
}
boolean isPathUnderParentDirPath = false;
if (parentDirPath != null) {
// The path can only be under parent directory path
isPathUnderParentDirPath = isPathInDirPath(path, parentDirPath, true);
}
// If setMissingPermissions is enabled and path is a regular file
if (setMissingPermissions && permissionsToCheck != null && file.isFile()) {
// If there is not parentDirPath restriction or path is under parentDirPath
if (parentDirPath == null || (isPathUnderParentDirPath && new File(parentDirPath).isDirectory())) {
setMissingFilePermissions(path, permissionsToCheck);
}
}
// If path is not a regular file
// Regular files cannot be automatically created so we do not ignore if missing
if (!file.isFile()) {
return context.getString(R.string.no_regular_file_found);
}
// If there is not parentDirPath restriction or path is not under parentDirPath or
// if permission errors must not be ignored for paths under parentDirPath
if (parentDirPath == null || !isPathUnderParentDirPath || !ignoreErrorsIfPathIsUnderParentDirPath) {
if (permissionsToCheck != null) {
// Check if permissions are missing
return checkMissingFilePermissions(context, path, permissionsToCheck, "File", false);
}
}
}
// Some function calls may throw SecurityException, etc
catch (Exception e) {
return context.getString(R.string.validate_file_existence_and_permissions_failed_with_exception, path, e.getMessage());
}
return null;
}
/**
* Validate the existence and permissions of directory at path.
*
* If the {@code parentDirPath} is not {@code null}, then creation of missing directory and
* setting of missing permissions will only be done if {@code path} is under
* {@code parentDirPath} or equals {@code parentDirPath}.
*
* @param context The {@link Context} to get error string.
* @param path The {@code path} for file to validate.
* @param parentDirPath The optional {@code parent directory path} to restrict operations to.
* This can optionally be {@code null}.
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
* @param createDirectoryIfMissing The {@code boolean} that decides if directory
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if missing permissions are to be
* automatically set.
* @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 errmsg} if path is not a directory, or validating permissions
* failed, otherwise {@code null}.
*/
public static String validateDirectoryExistenceAndPermissions(final Context context, final String path, final String parentDirPath, String permissionsToCheck, final boolean createDirectoryIfMissing, final boolean setMissingPermissions, final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_directory);
try {
File file = new File(path);
// If file exits but not a directory file
if (file.exists() && !file.isDirectory()) {
return context.getString(R.string.non_directory_file_found);
}
boolean isPathInParentDirPath = false;
if (parentDirPath != null) {
// The path can be equal to parent directory path or under it
isPathInParentDirPath = isPathInDirPath(path, parentDirPath, false);
}
if (createDirectoryIfMissing || setMissingPermissions) {
// If there is not parentDirPath restriction or path is in parentDirPath
if (parentDirPath == null || (isPathInParentDirPath && new File(parentDirPath).isDirectory())) {
// If createDirectoryIfMissing is enabled and no file exists at path, then create directory
if (createDirectoryIfMissing && !file.exists()) {
Logger.logVerbose(LOG_TAG, "Creating missing directory at path: \"" + path + "\"");
// If failed to create directory
if (!file.mkdirs()) {
return context.getString(R.string.creating_missing_directory_failed, path);
}
}
// If setMissingPermissions is enabled and path is a directory
if (setMissingPermissions && permissionsToCheck != null && file.isDirectory()) {
setMissingFilePermissions(path, permissionsToCheck);
}
}
}
// If there is not parentDirPath restriction or path is not in parentDirPath or
// if existence or permission errors must not be ignored for paths in parentDirPath
if (parentDirPath == null || !isPathInParentDirPath || !ignoreErrorsIfPathIsInParentDirPath) {
// If path is not a directory
// Directories can be automatically created so we can ignore if missing with above check
if (!file.isDirectory()) {
return context.getString(R.string.no_directory_found);
}
if (permissionsToCheck != null) {
// Check if permissions are missing
return checkMissingFilePermissions(context, path, permissionsToCheck, "Directory", ignoreIfNotExecutable);
}
}
}
// Some function calls may throw SecurityException, etc
catch (Exception e) {
return context.getString(R.string.validate_directory_existence_and_permissions_failed_with_exception, path, e.getMessage());
}
return null;
}
/**
* Set missing permissions for file at path.
*
* @param path The {@code path} for file to set permissions to.
* @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
*/
public static void setMissingFilePermissions(String path, String permissionsToSet) {
if (path == null || path.isEmpty()) return;
if (!isValidPermissingString(permissionsToSet)) {
Logger.logError(LOG_TAG, "Invalid permissionsToSet passed to setMissingFilePermissions: \"" + permissionsToSet + "\"");
return;
}
File file = new File(path);
if (permissionsToSet.contains("r") && !file.canRead()) {
Logger.logVerbose(LOG_TAG, "Setting missing read permissions for file at path: \"" + path + "\"");
file.setReadable(true);
}
if (permissionsToSet.contains("w") && !file.canWrite()) {
Logger.logVerbose(LOG_TAG, "Setting missing write permissions for file at path: \"" + path + "\"");
file.setWritable(true);
}
if (permissionsToSet.contains("x") && !file.canExecute()) {
Logger.logVerbose(LOG_TAG, "Setting missing execute permissions for file at path: \"" + path + "\"");
file.setExecutable(true);
}
}
/**
* Checking missing permissions for file at path.
*
* @param context The {@link Context} to get error string.
* @param path 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 fileType The label for the type of file to use for error string.
* @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}.
*/
public static String checkMissingFilePermissions(Context context, String path, String permissionsToCheck, String fileType, boolean ignoreIfNotExecutable) {
if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_path);
if (!isValidPermissingString(permissionsToCheck)) {
Logger.logError(LOG_TAG, "Invalid permissionsToCheck passed to checkMissingFilePermissions: \"" + permissionsToCheck + "\"");
return context.getString(R.string.invalid_file_permissions_string_to_check);
}
if (fileType == null || fileType.isEmpty()) fileType = "File";
File file = new File(path);
// If file is not readable
if (permissionsToCheck.contains("r") && !file.canRead()) {
return context.getString(R.string.file_not_readable, fileType);
}
// If file is not writable
if (permissionsToCheck.contains("w") && !file.canWrite()) {
return context.getString(R.string.file_not_writable, fileType);
}
// 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.file_not_executable, fileType);
}
return null;
}
/**
* Determines whether string exactly matches the 3 character permission string that
* contains the "r", "w", "x" or "-" in-order.
*
* @param string The {@link String} to check.
* @return Returns {@code true} if string exactly matches a permission string, otherwise {@code false}.
*/
public static boolean isValidPermissingString(String string) {
if (string == null || string.isEmpty()) return false;
return Pattern.compile("^([r-])[w-][x-]$", 0).matcher(string).matches();
}
}