mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-06 02:35:19 +08:00
Move FileUtils to file package and define more file util functions
A lot of utils have been defined now that can be used to safely manage files. The java java.io.File API has poor support for detecting symlinks including broken symlinks. Android implementation also has issues. Check FileTypes.getFileType() function for more info. For this reason, the UnixFileAttributes and related classes has been ported from AOSP to get file attributes and type. Some file utils and android versions use google's Guava com.google.common.io.MoreFiles library for managing files, specially for safer directory deletion with SecureDirectoryStream. Some file utils and android versions use org.apache.commons.io.FileUtils for managing files. The library version used is 2.5 and it must not be incremented for compatibility with android version < 8, otherwise runtime crashes will occur.
This commit is contained in:
@@ -9,18 +9,22 @@ android {
|
||||
ndkVersion project.properties.ndkVersion
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:1.1.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.core:core:1.5.0-beta03'
|
||||
implementation "androidx.annotation:annotation:1.2.0"
|
||||
implementation "androidx.core:core:1.5.0-rc01"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
implementation 'com.google.guava:guava:24.1-jre'
|
||||
implementation "io.noties.markwon:core:$markwon_version"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||
implementation "io.noties.markwon:linkify:$markwon_version"
|
||||
implementation "io.noties.markwon:recycler:$markwon_version"
|
||||
implementation "com.google.guava:guava:24.1-jre"
|
||||
implementation "io.noties.markwon:core:$markwon_version"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||
implementation "io.noties.markwon:linkify:$markwon_version"
|
||||
implementation "io.noties.markwon:recycler:$markwon_version"
|
||||
implementation project(":terminal-view")
|
||||
|
||||
// Do not increment version higher than 2.5 or there
|
||||
// will be runtime exceptions on android < 8
|
||||
// due to missing classes like java.nio.file.Path.
|
||||
implementation "commons-io:commons-io:2.5"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -95,10 +99,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.4'
|
||||
testImplementation "junit:junit:4.13.1"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
}
|
||||
|
||||
task versionName {
|
||||
|
@@ -14,7 +14,7 @@ import android.os.IBinder;
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.utils.FileUtils;
|
||||
import com.termux.app.file.FileUtils;
|
||||
import com.termux.app.utils.Logger;
|
||||
import com.termux.app.utils.NotificationUtils;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
@@ -354,9 +354,9 @@ public class RunCommandService extends Service {
|
||||
|
||||
// If executable is not a regular file, or is not readable or executable, then just return
|
||||
// Setting of missing read and execute permissions is not done
|
||||
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executionCommand.executable,
|
||||
null, PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS,
|
||||
false, false);
|
||||
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, "executable", executionCommand.executable, null,
|
||||
PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||
false);
|
||||
if (errmsg != null) {
|
||||
errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
@@ -376,10 +376,9 @@ public class RunCommandService extends Service {
|
||||
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
||||
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||
// for working directories.
|
||||
errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, executionCommand.workingDirectory,
|
||||
TermuxConstants.TERMUX_FILES_DIR_PATH, PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS,
|
||||
true, true, false,
|
||||
true);
|
||||
errmsg = FileUtils.validateDirectoryFileExistenceAndPermissions(this, "working", executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, true,
|
||||
PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true,
|
||||
true, true);
|
||||
if (errmsg != null) {
|
||||
errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
|
1627
app/src/main/java/com/termux/app/file/FileUtils.java
Normal file
1627
app/src/main/java/com/termux/app/file/FileUtils.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,416 @@
|
||||
/*
|
||||
* Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.termux.app.file.filesystem;
|
||||
|
||||
import android.os.Build;
|
||||
import android.system.StructStat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.app.utils.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Unix implementation of PosixFileAttributes.
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixFileAttributes.java
|
||||
*/
|
||||
|
||||
public class FileAttributes {
|
||||
private String filePath;
|
||||
private FileDescriptor fileDescriptor;
|
||||
|
||||
private int st_mode;
|
||||
private long st_ino;
|
||||
private long st_dev;
|
||||
private long st_rdev;
|
||||
private long st_nlink;
|
||||
private int st_uid;
|
||||
private int st_gid;
|
||||
private long st_size;
|
||||
private long st_blksize;
|
||||
private long st_blocks;
|
||||
private long st_atime_sec;
|
||||
private long st_atime_nsec;
|
||||
private long st_mtime_sec;
|
||||
private long st_mtime_nsec;
|
||||
private long st_ctime_sec;
|
||||
private long st_ctime_nsec;
|
||||
|
||||
// created lazily
|
||||
private volatile String owner;
|
||||
private volatile String group;
|
||||
private volatile FileKey key;
|
||||
|
||||
private FileAttributes(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
private FileAttributes(FileDescriptor fileDescriptor) {
|
||||
this.fileDescriptor = fileDescriptor;
|
||||
}
|
||||
|
||||
// get the FileAttributes for a given file
|
||||
public static FileAttributes get(String filePath, boolean followLinks) throws IOException {
|
||||
FileAttributes fileAttributes;
|
||||
|
||||
if (filePath == null || filePath.isEmpty())
|
||||
fileAttributes = new FileAttributes((String) null);
|
||||
else
|
||||
fileAttributes = new FileAttributes(new File(filePath).getAbsolutePath());
|
||||
|
||||
if (followLinks) {
|
||||
NativeDispatcher.stat(filePath, fileAttributes);
|
||||
} else {
|
||||
NativeDispatcher.lstat(filePath, fileAttributes);
|
||||
}
|
||||
|
||||
// Logger.logDebug(fileAttributes.toString());
|
||||
|
||||
return fileAttributes;
|
||||
}
|
||||
|
||||
// get the FileAttributes for an open file
|
||||
public static FileAttributes get(FileDescriptor fileDescriptor) throws IOException {
|
||||
FileAttributes fileAttributes = new FileAttributes(fileDescriptor);
|
||||
NativeDispatcher.fstat(fileDescriptor, fileAttributes);
|
||||
return fileAttributes;
|
||||
}
|
||||
|
||||
public String file() {
|
||||
if(filePath != null)
|
||||
return filePath;
|
||||
else if(fileDescriptor != null)
|
||||
return fileDescriptor.toString();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
// package-private
|
||||
public boolean isSameFile(FileAttributes attrs) {
|
||||
return ((st_ino == attrs.st_ino) && (st_dev == attrs.st_dev));
|
||||
}
|
||||
|
||||
// package-private
|
||||
public int mode() {
|
||||
return st_mode;
|
||||
}
|
||||
|
||||
public long blksize() {
|
||||
return st_blksize;
|
||||
}
|
||||
|
||||
public long blocks() {
|
||||
return st_blocks;
|
||||
}
|
||||
|
||||
public long ino() {
|
||||
return st_ino;
|
||||
}
|
||||
|
||||
public long dev() {
|
||||
return st_dev;
|
||||
}
|
||||
|
||||
public long rdev() {
|
||||
return st_rdev;
|
||||
}
|
||||
|
||||
public long nlink() {
|
||||
return st_nlink;
|
||||
}
|
||||
|
||||
public int uid() {
|
||||
return st_uid;
|
||||
}
|
||||
|
||||
public int gid() {
|
||||
return st_gid;
|
||||
}
|
||||
|
||||
private static FileTime toFileTime(long sec, long nsec) {
|
||||
if (nsec == 0) {
|
||||
return FileTime.from(sec, TimeUnit.SECONDS);
|
||||
} else {
|
||||
// truncate to microseconds to avoid overflow with timestamps
|
||||
// way out into the future. We can re-visit this if FileTime
|
||||
// is updated to define a from(secs,nsecs) method.
|
||||
long micro = sec * 1000000L + nsec / 1000L;
|
||||
return FileTime.from(micro, TimeUnit.MICROSECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public FileTime lastAccessTime() {
|
||||
return toFileTime(st_atime_sec, st_atime_nsec);
|
||||
}
|
||||
|
||||
public FileTime lastModifiedTime() {
|
||||
return toFileTime(st_mtime_sec, st_mtime_nsec);
|
||||
}
|
||||
|
||||
public FileTime lastChangeTime() {
|
||||
return toFileTime(st_ctime_sec, st_ctime_nsec);
|
||||
}
|
||||
|
||||
public FileTime creationTime() {
|
||||
return lastModifiedTime();
|
||||
}
|
||||
|
||||
public boolean isRegularFile() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFREG);
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFDIR);
|
||||
}
|
||||
|
||||
public boolean isSymbolicLink() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFLNK);
|
||||
}
|
||||
|
||||
public boolean isCharacter() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFCHR);
|
||||
}
|
||||
|
||||
public boolean isFifo() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFIFO);
|
||||
}
|
||||
|
||||
public boolean isBlock() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFBLK);
|
||||
}
|
||||
|
||||
public boolean isOther() {
|
||||
int type = st_mode & UnixConstants.S_IFMT;
|
||||
return (type != UnixConstants.S_IFREG &&
|
||||
type != UnixConstants.S_IFDIR &&
|
||||
type != UnixConstants.S_IFLNK);
|
||||
}
|
||||
|
||||
public boolean isDevice() {
|
||||
int type = st_mode & UnixConstants.S_IFMT;
|
||||
return (type == UnixConstants.S_IFCHR ||
|
||||
type == UnixConstants.S_IFBLK ||
|
||||
type == UnixConstants.S_IFIFO);
|
||||
}
|
||||
|
||||
public long size() {
|
||||
return st_size;
|
||||
}
|
||||
|
||||
public FileKey fileKey() {
|
||||
if (key == null) {
|
||||
synchronized (this) {
|
||||
if (key == null) {
|
||||
key = new FileKey(st_dev, st_ino);
|
||||
}
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
public String owner() {
|
||||
if (owner == null) {
|
||||
synchronized (this) {
|
||||
if (owner == null) {
|
||||
owner = Integer.toString(st_uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
public String group() {
|
||||
if (group == null) {
|
||||
synchronized (this) {
|
||||
if (group == null) {
|
||||
group = Integer.toString(st_gid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
public Set<FilePermission> permissions() {
|
||||
int bits = (st_mode & UnixConstants.S_IAMB);
|
||||
HashSet<FilePermission> perms = new HashSet<>();
|
||||
|
||||
if ((bits & UnixConstants.S_IRUSR) > 0)
|
||||
perms.add(FilePermission.OWNER_READ);
|
||||
if ((bits & UnixConstants.S_IWUSR) > 0)
|
||||
perms.add(FilePermission.OWNER_WRITE);
|
||||
if ((bits & UnixConstants.S_IXUSR) > 0)
|
||||
perms.add(FilePermission.OWNER_EXECUTE);
|
||||
|
||||
if ((bits & UnixConstants.S_IRGRP) > 0)
|
||||
perms.add(FilePermission.GROUP_READ);
|
||||
if ((bits & UnixConstants.S_IWGRP) > 0)
|
||||
perms.add(FilePermission.GROUP_WRITE);
|
||||
if ((bits & UnixConstants.S_IXGRP) > 0)
|
||||
perms.add(FilePermission.GROUP_EXECUTE);
|
||||
|
||||
if ((bits & UnixConstants.S_IROTH) > 0)
|
||||
perms.add(FilePermission.OTHERS_READ);
|
||||
if ((bits & UnixConstants.S_IWOTH) > 0)
|
||||
perms.add(FilePermission.OTHERS_WRITE);
|
||||
if ((bits & UnixConstants.S_IXOTH) > 0)
|
||||
perms.add(FilePermission.OTHERS_EXECUTE);
|
||||
|
||||
return perms;
|
||||
}
|
||||
|
||||
public void loadFromStructStat(StructStat structStat) {
|
||||
this.st_mode = structStat.st_mode;
|
||||
this.st_ino = structStat.st_ino;
|
||||
this.st_dev = structStat.st_dev;
|
||||
this.st_rdev = structStat.st_rdev;
|
||||
this.st_nlink = structStat.st_nlink;
|
||||
this.st_uid = structStat.st_uid;
|
||||
this.st_gid = structStat.st_gid;
|
||||
this.st_size = structStat.st_size;
|
||||
this.st_blksize = structStat.st_blksize;
|
||||
this.st_blocks = structStat.st_blocks;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
this.st_atime_sec = structStat.st_atim.tv_sec;
|
||||
this.st_atime_nsec = structStat.st_atim.tv_nsec;
|
||||
this.st_mtime_sec = structStat.st_mtim.tv_sec;
|
||||
this.st_mtime_nsec = structStat.st_mtim.tv_nsec;
|
||||
this.st_ctime_sec = structStat.st_ctim.tv_sec;
|
||||
this.st_ctime_nsec = structStat.st_ctim.tv_nsec;
|
||||
} else {
|
||||
this.st_atime_sec = structStat.st_atime;
|
||||
this.st_atime_nsec = 0;
|
||||
this.st_mtime_sec = structStat.st_mtime;
|
||||
this.st_mtime_nsec = 0;
|
||||
this.st_ctime_sec = structStat.st_ctime;
|
||||
this.st_ctime_nsec = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getFileString() {
|
||||
return "File: `" + file() + "`";
|
||||
}
|
||||
|
||||
public String getTypeString() {
|
||||
return "Type: `" + FileTypes.getFileType(this).getName() + "`";
|
||||
}
|
||||
|
||||
public String getSizeString() {
|
||||
return "Size: `" + size() + "`";
|
||||
}
|
||||
|
||||
public String getBlocksString() {
|
||||
return "Blocks: `" + blocks() + "`";
|
||||
}
|
||||
|
||||
public String getIOBlockString() {
|
||||
return "IO Block: `" + blksize() + "`";
|
||||
}
|
||||
|
||||
public String getDeviceString() {
|
||||
return "Device: `" + Long.toHexString(st_dev) + "`";
|
||||
}
|
||||
|
||||
public String getInodeString() {
|
||||
return "Inode: `" + st_ino + "`";
|
||||
}
|
||||
|
||||
public String getLinksString() {
|
||||
return "Links: `" + nlink() + "`";
|
||||
}
|
||||
|
||||
public String getDeviceTypeString() {
|
||||
return "Device Type: `" + rdev() + "`";
|
||||
}
|
||||
|
||||
public String getOwnerString() {
|
||||
return "Owner: `" + owner() + "`";
|
||||
}
|
||||
|
||||
public String getGroupString() {
|
||||
return "Group: `" + group() + "`";
|
||||
}
|
||||
|
||||
public String getPermissionString() {
|
||||
return "Permissions: `" + FilePermissions.toString(permissions()) + "`";
|
||||
}
|
||||
|
||||
public String getAccessTimeString() {
|
||||
return "Access Time: `" + lastAccessTime() + "`";
|
||||
}
|
||||
|
||||
public String getModifiedTimeString() {
|
||||
return "Modified Time: `" + lastModifiedTime() + "`";
|
||||
}
|
||||
|
||||
public String getChangeTimeString() {
|
||||
return "Change Time: `" + lastChangeTime() + "`";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getFileAttributesLogString(this);
|
||||
}
|
||||
|
||||
public static String getFileAttributesLogString(final FileAttributes fileAttributes) {
|
||||
if (fileAttributes == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(fileAttributes.getFileString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getTypeString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getSizeString());
|
||||
logString.append("\n").append(fileAttributes.getBlocksString());
|
||||
logString.append("\n").append(fileAttributes.getIOBlockString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getDeviceString());
|
||||
logString.append("\n").append(fileAttributes.getInodeString());
|
||||
logString.append("\n").append(fileAttributes.getLinksString());
|
||||
|
||||
if(fileAttributes.isBlock() || fileAttributes.isCharacter())
|
||||
logString.append("\n").append(fileAttributes.getDeviceTypeString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getOwnerString());
|
||||
logString.append("\n").append(fileAttributes.getGroupString());
|
||||
logString.append("\n").append(fileAttributes.getPermissionString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getAccessTimeString());
|
||||
logString.append("\n").append(fileAttributes.getModifiedTimeString());
|
||||
logString.append("\n").append(fileAttributes.getChangeTimeString());
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.termux.app.file.filesystem;
|
||||
|
||||
/**
|
||||
* Container for device/inode to uniquely identify file.
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixFileKey.java
|
||||
*/
|
||||
|
||||
public class FileKey {
|
||||
private final long st_dev;
|
||||
private final long st_ino;
|
||||
|
||||
FileKey(long st_dev, long st_ino) {
|
||||
this.st_dev = st_dev;
|
||||
this.st_ino = st_ino;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int)(st_dev ^ (st_dev >>> 32)) +
|
||||
(int)(st_ino ^ (st_ino >>> 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this)
|
||||
return true;
|
||||
if (!(obj instanceof FileKey))
|
||||
return false;
|
||||
FileKey other = (FileKey)obj;
|
||||
return (this.st_dev == other.st_dev) && (this.st_ino == other.st_ino);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("(dev=")
|
||||
.append(Long.toHexString(st_dev))
|
||||
.append(",ino=")
|
||||
.append(st_ino)
|
||||
.append(')');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
|
||||
/*
|
||||
* Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.termux.app.file.filesystem;
|
||||
|
||||
/**
|
||||
* Defines the bits for use with the {@link FileAttributes#permissions()
|
||||
* permissions} attribute.
|
||||
*
|
||||
* <p> The {@link FileAttributes} class defines methods for manipulating
|
||||
* set of permissions.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/PosixFilePermission.java
|
||||
*
|
||||
* @since 1.7
|
||||
*/
|
||||
|
||||
public enum FilePermission {
|
||||
|
||||
/**
|
||||
* Read permission, owner.
|
||||
*/
|
||||
OWNER_READ,
|
||||
|
||||
/**
|
||||
* Write permission, owner.
|
||||
*/
|
||||
OWNER_WRITE,
|
||||
|
||||
/**
|
||||
* Execute/search permission, owner.
|
||||
*/
|
||||
OWNER_EXECUTE,
|
||||
|
||||
/**
|
||||
* Read permission, group.
|
||||
*/
|
||||
GROUP_READ,
|
||||
|
||||
/**
|
||||
* Write permission, group.
|
||||
*/
|
||||
GROUP_WRITE,
|
||||
|
||||
/**
|
||||
* Execute/search permission, group.
|
||||
*/
|
||||
GROUP_EXECUTE,
|
||||
|
||||
/**
|
||||
* Read permission, others.
|
||||
*/
|
||||
OTHERS_READ,
|
||||
|
||||
/**
|
||||
* Write permission, others.
|
||||
*/
|
||||
OTHERS_WRITE,
|
||||
|
||||
/**
|
||||
* Execute/search permission, others.
|
||||
*/
|
||||
OTHERS_EXECUTE;
|
||||
}
|
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.termux.app.file.filesystem;
|
||||
|
||||
import static com.termux.app.file.filesystem.FilePermission.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* This class consists exclusively of static methods that operate on sets of
|
||||
* {@link FilePermission} objects.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/PosixFilePermissions.java
|
||||
*
|
||||
* @since 1.7
|
||||
*/
|
||||
|
||||
public final class FilePermissions {
|
||||
private FilePermissions() { }
|
||||
|
||||
// Write string representation of permission bits to {@code sb}.
|
||||
private static void writeBits(StringBuilder sb, boolean r, boolean w, boolean x) {
|
||||
if (r) {
|
||||
sb.append('r');
|
||||
} else {
|
||||
sb.append('-');
|
||||
}
|
||||
if (w) {
|
||||
sb.append('w');
|
||||
} else {
|
||||
sb.append('-');
|
||||
}
|
||||
if (x) {
|
||||
sb.append('x');
|
||||
} else {
|
||||
sb.append('-');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code String} representation of a set of permissions. It
|
||||
* is guaranteed that the returned {@code String} can be parsed by the
|
||||
* {@link #fromString} method.
|
||||
*
|
||||
* <p> If the set contains {@code null} or elements that are not of type
|
||||
* {@code FilePermission} then these elements are ignored.
|
||||
*
|
||||
* @param perms
|
||||
* the set of permissions
|
||||
*
|
||||
* @return the string representation of the permission set
|
||||
*/
|
||||
public static String toString(Set<FilePermission> perms) {
|
||||
StringBuilder sb = new StringBuilder(9);
|
||||
writeBits(sb, perms.contains(OWNER_READ), perms.contains(OWNER_WRITE),
|
||||
perms.contains(OWNER_EXECUTE));
|
||||
writeBits(sb, perms.contains(GROUP_READ), perms.contains(GROUP_WRITE),
|
||||
perms.contains(GROUP_EXECUTE));
|
||||
writeBits(sb, perms.contains(OTHERS_READ), perms.contains(OTHERS_WRITE),
|
||||
perms.contains(OTHERS_EXECUTE));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isSet(char c, char setValue) {
|
||||
if (c == setValue)
|
||||
return true;
|
||||
if (c == '-')
|
||||
return false;
|
||||
throw new IllegalArgumentException("Invalid mode");
|
||||
}
|
||||
private static boolean isR(char c) { return isSet(c, 'r'); }
|
||||
private static boolean isW(char c) { return isSet(c, 'w'); }
|
||||
private static boolean isX(char c) { return isSet(c, 'x'); }
|
||||
|
||||
/**
|
||||
* Returns the set of permissions corresponding to a given {@code String}
|
||||
* representation.
|
||||
*
|
||||
* <p> The {@code perms} parameter is a {@code String} representing the
|
||||
* permissions. It has 9 characters that are interpreted as three sets of
|
||||
* three. The first set refers to the owner's permissions; the next to the
|
||||
* group permissions and the last to others. Within each set, the first
|
||||
* character is {@code 'r'} to indicate permission to read, the second
|
||||
* character is {@code 'w'} to indicate permission to write, and the third
|
||||
* character is {@code 'x'} for execute permission. Where a permission is
|
||||
* not set then the corresponding character is set to {@code '-'}.
|
||||
*
|
||||
* <p> <b>Usage Example:</b>
|
||||
* Suppose we require the set of permissions that indicate the owner has read,
|
||||
* write, and execute permissions, the group has read and execute permissions
|
||||
* and others have none.
|
||||
* <pre>
|
||||
* Set<FilePermission> perms = FilePermissions.fromString("rwxr-x---");
|
||||
* </pre>
|
||||
*
|
||||
* @param perms
|
||||
* string representing a set of permissions
|
||||
*
|
||||
* @return the resulting set of permissions
|
||||
*
|
||||
* @throws IllegalArgumentException
|
||||
* if the string cannot be converted to a set of permissions
|
||||
*
|
||||
* @see #toString(Set)
|
||||
*/
|
||||
public static Set<FilePermission> fromString(String perms) {
|
||||
if (perms.length() != 9)
|
||||
throw new IllegalArgumentException("Invalid mode");
|
||||
Set<FilePermission> result = EnumSet.noneOf(FilePermission.class);
|
||||
if (isR(perms.charAt(0))) result.add(OWNER_READ);
|
||||
if (isW(perms.charAt(1))) result.add(OWNER_WRITE);
|
||||
if (isX(perms.charAt(2))) result.add(OWNER_EXECUTE);
|
||||
if (isR(perms.charAt(3))) result.add(GROUP_READ);
|
||||
if (isW(perms.charAt(4))) result.add(GROUP_WRITE);
|
||||
if (isX(perms.charAt(5))) result.add(GROUP_EXECUTE);
|
||||
if (isR(perms.charAt(6))) result.add(OTHERS_READ);
|
||||
if (isW(perms.charAt(7))) result.add(OTHERS_WRITE);
|
||||
if (isX(perms.charAt(8))) result.add(OTHERS_EXECUTE);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
156
app/src/main/java/com/termux/app/file/filesystem/FileTime.java
Normal file
156
app/src/main/java/com/termux/app/file/filesystem/FileTime.java
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.termux.app.file.filesystem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Represents the value of a file's time stamp attribute. For example, it may
|
||||
* represent the time that the file was last
|
||||
* {@link FileAttributes#lastModifiedTime() modified},
|
||||
* {@link FileAttributes#lastAccessTime() accessed},
|
||||
* or {@link FileAttributes#creationTime() created}.
|
||||
*
|
||||
* <p> Instances of this class are immutable.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/FileTime.java
|
||||
*
|
||||
* @since 1.7
|
||||
* @see java.nio.file.Files#setLastModifiedTime
|
||||
* @see java.nio.file.Files#getLastModifiedTime
|
||||
*/
|
||||
|
||||
public final class FileTime {
|
||||
/**
|
||||
* The unit of granularity to interpret the value. Null if
|
||||
* this {@code FileTime} is converted from an {@code Instant},
|
||||
* the {@code value} and {@code unit} pair will not be used
|
||||
* in this scenario.
|
||||
*/
|
||||
private final TimeUnit unit;
|
||||
|
||||
/**
|
||||
* The value since the epoch; can be negative.
|
||||
*/
|
||||
private final long value;
|
||||
|
||||
|
||||
/**
|
||||
* The value return by toString (created lazily)
|
||||
*/
|
||||
private String valueAsString;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of this class.
|
||||
*/
|
||||
private FileTime(long value, TimeUnit unit) {
|
||||
this.value = value;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code FileTime} representing a value at the given unit of
|
||||
* granularity.
|
||||
*
|
||||
* @param value
|
||||
* the value since the epoch (1970-01-01T00:00:00Z); can be
|
||||
* negative
|
||||
* @param unit
|
||||
* the unit of granularity to interpret the value
|
||||
*
|
||||
* @return a {@code FileTime} representing the given value
|
||||
*/
|
||||
public static FileTime from(long value, @NonNull TimeUnit unit) {
|
||||
Objects.requireNonNull(unit, "unit");
|
||||
return new FileTime(value, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code FileTime} representing the given value in milliseconds.
|
||||
*
|
||||
* @param value
|
||||
* the value, in milliseconds, since the epoch
|
||||
* (1970-01-01T00:00:00Z); can be negative
|
||||
*
|
||||
* @return a {@code FileTime} representing the given value
|
||||
*/
|
||||
public static FileTime fromMillis(long value) {
|
||||
return new FileTime(value, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the given unit of granularity.
|
||||
*
|
||||
* <p> Conversion from a coarser granularity that would numerically overflow
|
||||
* saturate to {@code Long.MIN_VALUE} if negative or {@code Long.MAX_VALUE}
|
||||
* if positive.
|
||||
*
|
||||
* @param unit
|
||||
* the unit of granularity for the return value
|
||||
*
|
||||
* @return value in the given unit of granularity, since the epoch
|
||||
* since the epoch (1970-01-01T00:00:00Z); can be negative
|
||||
*/
|
||||
public long to(TimeUnit unit) {
|
||||
Objects.requireNonNull(unit, "unit");
|
||||
return unit.convert(this.value, this.unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value in milliseconds.
|
||||
*
|
||||
* <p> Conversion from a coarser granularity that would numerically overflow
|
||||
* saturate to {@code Long.MIN_VALUE} if negative or {@code Long.MAX_VALUE}
|
||||
* if positive.
|
||||
*
|
||||
* @return the value in milliseconds, since the epoch (1970-01-01T00:00:00Z)
|
||||
*/
|
||||
public long toMillis() {
|
||||
return unit.toMillis(value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getDate(toMillis(), "yyyy.MM.dd HH:mm:ss.SSS z");
|
||||
}
|
||||
|
||||
public static String getDate(long milliSeconds, String format) {
|
||||
try {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(milliSeconds);
|
||||
return new SimpleDateFormat(format).format(calendar.getTime());
|
||||
} catch(Exception e) {
|
||||
return Long.toString(milliSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package com.termux.app.file.filesystem;
|
||||
|
||||
/** The {@link Enum} that defines file types. */
|
||||
public enum FileType {
|
||||
|
||||
NO_EXIST("no exist", 0), // 0000000
|
||||
REGULAR("regular", 1), // 0000001
|
||||
DIRECTORY("directory", 2), // 0000010
|
||||
SYMLINK("symlink", 4), // 0000100
|
||||
CHARACTER("character", 8), // 0001000
|
||||
FIFO("fifo", 16), // 0010000
|
||||
BLOCK("block", 32), // 0100000
|
||||
UNKNOWN("unknown", 64); // 1000000
|
||||
|
||||
private final String name;
|
||||
private final int value;
|
||||
|
||||
FileType(final String name, final int value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
116
app/src/main/java/com/termux/app/file/filesystem/FileTypes.java
Normal file
116
app/src/main/java/com/termux/app/file/filesystem/FileTypes.java
Normal file
@@ -0,0 +1,116 @@
|
||||
package com.termux.app.file.filesystem;
|
||||
|
||||
import android.system.Os;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.app.utils.Logger;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class FileTypes {
|
||||
|
||||
/** Flags to represent regular, directory and symlink file types defined by {@link FileType} */
|
||||
public static final int FILE_TYPE_NORMAL_FLAGS = FileType.REGULAR.getValue() | FileType.DIRECTORY.getValue() | FileType.SYMLINK.getValue();
|
||||
|
||||
/** Flags to represent any file type defined by {@link FileType} */
|
||||
public static final int FILE_TYPE_ANY_FLAGS = Integer.MAX_VALUE; // 1111111111111111111111111111111 (31 1's)
|
||||
|
||||
public static String convertFileTypeFlagsToNamesString(int fileTypeFlags) {
|
||||
StringBuilder fileTypeFlagsStringBuilder = new StringBuilder();
|
||||
|
||||
FileType[] fileTypes = {FileType.REGULAR, FileType.DIRECTORY, FileType.SYMLINK, FileType.CHARACTER, FileType.FIFO, FileType.BLOCK, FileType.UNKNOWN};
|
||||
for (FileType fileType : fileTypes) {
|
||||
if ((fileTypeFlags & fileType.getValue()) > 0)
|
||||
fileTypeFlagsStringBuilder.append(fileType.getName()).append(",");
|
||||
}
|
||||
|
||||
String fileTypeFlagsString = fileTypeFlagsStringBuilder.toString();
|
||||
|
||||
if (fileTypeFlagsString.endsWith(","))
|
||||
fileTypeFlagsString = fileTypeFlagsString.substring(0, fileTypeFlagsString.lastIndexOf(","));
|
||||
|
||||
return fileTypeFlagsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the type of file that exists at {@code filePath}.
|
||||
*
|
||||
* Returns:
|
||||
* - {@link FileType#NO_EXIST} if {@code filePath} is {@code null}, empty, an exception is raised
|
||||
* or no file exists at {@code filePath}.
|
||||
* - {@link FileType#REGULAR} if file at {@code filePath} is a regular file.
|
||||
* - {@link FileType#DIRECTORY} if file at {@code filePath} is a directory file.
|
||||
* - {@link FileType#SYMLINK} if file at {@code filePath} is a symlink file and {@code followLinks} is {@code false}.
|
||||
* - {@link FileType#CHARACTER} if file at {@code filePath} is a character special file.
|
||||
* - {@link FileType#FIFO} if file at {@code filePath} is a fifo special file.
|
||||
* - {@link FileType#BLOCK} if file at {@code filePath} is a block special file.
|
||||
* - {@link FileType#UNKNOWN} if file at {@code filePath} is of unknown type.
|
||||
*
|
||||
* The {@link File#isFile()} and {@link File#isDirectory()} uses {@link Os#stat(String)} system
|
||||
* call (not {@link Os#lstat(String)}) to check file type and does follow symlinks.
|
||||
*
|
||||
* The {@link File#exists()} uses {@link Os#access(String, int)} system call to check if file is
|
||||
* accessible and does not follow symlinks. However, it returns {@code false} for dangling symlinks,
|
||||
* on android at least. Check https://stackoverflow.com/a/57747064/14686958
|
||||
*
|
||||
* Basically {@link File} API is not reliable to check for symlinks.
|
||||
*
|
||||
* So we get the file type directly with {@link Os#lstat(String)} if {@code followLinks} is
|
||||
* {@code false} and {@link Os#stat(String)} if {@code followLinks} is {@code true}. All exceptions
|
||||
* are assumed as non-existence.
|
||||
*
|
||||
* The {@link org.apache.commons.io.FileUtils#isSymlink(File)} can also be used for checking
|
||||
* symlinks but {@link FileAttributes} will provide access to more attributes if necessary,
|
||||
* including getting other special file types considering that {@link File#exists()} can't be
|
||||
* used to reliably check for non-existence and exclude the other 3 file types. commons.io is
|
||||
* also not compatible with android < 8 for many things.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/io/File.java;l=793
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/io/UnixFileSystem.java;l=248
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/native/UnixFileSystem_md.c;l=121
|
||||
* https://cs.android.com/android/_/android/platform/libcore/+/001ac51d61ad7443ba518bf2cf7e086efe698c6d
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/luni/src/main/java/libcore/io/Os.java;l=51
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/luni/src/main/java/libcore/io/Libcore.java;l=45
|
||||
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7530
|
||||
*
|
||||
* @param filePath The {@code path} for file to check.
|
||||
* @param followLinks The {@code boolean} that decides if symlinks will be followed while
|
||||
* finding type. If set to {@code true}, then type of symlink target will
|
||||
* be returned if file at {@code filePath} is a symlink. If set to
|
||||
* {@code false}, then type of file at {@code filePath} itself will be
|
||||
* returned.
|
||||
* @return Returns the {@link FileType} of file.
|
||||
*/
|
||||
public static FileType getFileType(final String filePath, final boolean followLinks) {
|
||||
if (filePath == null || filePath.isEmpty()) return FileType.NO_EXIST;
|
||||
|
||||
try {
|
||||
FileAttributes fileAttributes = FileAttributes.get(filePath, followLinks);
|
||||
return getFileType(fileAttributes);
|
||||
} catch (Exception e) {
|
||||
// If not a ENOENT (No such file or directory) exception
|
||||
if(!e.getMessage().contains("ENOENT"))
|
||||
Logger.logError("Failed to get file type for file at path \"" + filePath + "\": " + e.getMessage());
|
||||
return FileType.NO_EXIST;
|
||||
}
|
||||
}
|
||||
|
||||
public static FileType getFileType(@NonNull final FileAttributes fileAttributes) {
|
||||
if (fileAttributes.isRegularFile())
|
||||
return FileType.REGULAR;
|
||||
else if (fileAttributes.isDirectory())
|
||||
return FileType.DIRECTORY;
|
||||
else if (fileAttributes.isSymbolicLink())
|
||||
return FileType.SYMLINK;
|
||||
else if (fileAttributes.isCharacter())
|
||||
return FileType.CHARACTER;
|
||||
else if (fileAttributes.isFifo())
|
||||
return FileType.FIFO;
|
||||
else if (fileAttributes.isBlock())
|
||||
return FileType.BLOCK;
|
||||
else
|
||||
return FileType.UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
package com.termux.app.file.filesystem;
|
||||
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
|
||||
public class NativeDispatcher {
|
||||
|
||||
public static void stat(String filePath, FileAttributes fileAttributes) throws IOException {
|
||||
validateFileExistence(filePath);
|
||||
|
||||
try {
|
||||
fileAttributes.loadFromStructStat(Os.stat(filePath));
|
||||
} catch (ErrnoException e) {
|
||||
throw new IOException("Failed to run Os.stat() on file at path \"" + filePath + "\": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void lstat(String filePath, FileAttributes fileAttributes) throws IOException {
|
||||
validateFileExistence(filePath);
|
||||
|
||||
try {
|
||||
fileAttributes.loadFromStructStat(Os.lstat(filePath));
|
||||
} catch (ErrnoException e) {
|
||||
throw new IOException("Failed to run Os.lstat() on file at path \"" + filePath + "\": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void fstat(FileDescriptor fileDescriptor, FileAttributes fileAttributes) throws IOException {
|
||||
validateFileDescriptor(fileDescriptor);
|
||||
|
||||
try {
|
||||
fileAttributes.loadFromStructStat(Os.fstat(fileDescriptor));
|
||||
} catch (ErrnoException e) {
|
||||
throw new IOException("Failed to run Os.fstat() on file descriptor \"" + fileDescriptor.toString() + "\": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateFileExistence(String filePath) throws IOException {
|
||||
if (filePath == null || filePath.isEmpty()) throw new IOException("The path is null or empty");
|
||||
|
||||
File file = new File(filePath);
|
||||
|
||||
//if(!file.exists())
|
||||
// throw new IOException("No such file or directory: \"" + filePath + "\"");
|
||||
}
|
||||
|
||||
public static void validateFileDescriptor(FileDescriptor fileDescriptor) throws IOException {
|
||||
if (fileDescriptor == null) throw new IOException("The file descriptor is null");
|
||||
|
||||
if(!fileDescriptor.valid())
|
||||
throw new IOException("No such file descriptor: \"" + fileDescriptor.toString() + "\"");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,149 @@
|
||||
|
||||
/*
|
||||
* Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*
|
||||
*/
|
||||
// AUTOMATICALLY GENERATED FILE - DO NOT EDIT
|
||||
package com.termux.app.file.filesystem;
|
||||
|
||||
// BEGIN Android-changed: Use constants from android.system.OsConstants. http://b/32203242
|
||||
// Those constants are initialized by native code to ensure correctness on different architectures.
|
||||
// AT_SYMLINK_NOFOLLOW (used by fstatat) and AT_REMOVEDIR (used by unlinkat) as of July 2018 do not
|
||||
// have equivalents in android.system.OsConstants so left unchanged.
|
||||
import android.system.OsConstants;
|
||||
|
||||
/**
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixConstants.java
|
||||
*/
|
||||
public class UnixConstants {
|
||||
private UnixConstants() { }
|
||||
|
||||
static final int O_RDONLY = OsConstants.O_RDONLY;
|
||||
|
||||
static final int O_WRONLY = OsConstants.O_WRONLY;
|
||||
|
||||
static final int O_RDWR = OsConstants.O_RDWR;
|
||||
|
||||
static final int O_APPEND = OsConstants.O_APPEND;
|
||||
|
||||
static final int O_CREAT = OsConstants.O_CREAT;
|
||||
|
||||
static final int O_EXCL = OsConstants.O_EXCL;
|
||||
|
||||
static final int O_TRUNC = OsConstants.O_TRUNC;
|
||||
|
||||
static final int O_SYNC = OsConstants.O_SYNC;
|
||||
|
||||
static final int O_DSYNC = OsConstants.O_DSYNC;
|
||||
|
||||
static final int O_NOFOLLOW = OsConstants.O_NOFOLLOW;
|
||||
|
||||
static final int S_IAMB = get_S_IAMB();
|
||||
|
||||
static final int S_IRUSR = OsConstants.S_IRUSR;
|
||||
|
||||
static final int S_IWUSR = OsConstants.S_IWUSR;
|
||||
|
||||
static final int S_IXUSR = OsConstants.S_IXUSR;
|
||||
|
||||
static final int S_IRGRP = OsConstants.S_IRGRP;
|
||||
|
||||
static final int S_IWGRP = OsConstants.S_IWGRP;
|
||||
|
||||
static final int S_IXGRP = OsConstants.S_IXGRP;
|
||||
|
||||
static final int S_IROTH = OsConstants.S_IROTH;
|
||||
|
||||
static final int S_IWOTH = OsConstants.S_IWOTH;
|
||||
|
||||
static final int S_IXOTH = OsConstants.S_IXOTH;
|
||||
|
||||
static final int S_IFMT = OsConstants.S_IFMT;
|
||||
|
||||
static final int S_IFREG = OsConstants.S_IFREG;
|
||||
|
||||
static final int S_IFDIR = OsConstants.S_IFDIR;
|
||||
|
||||
static final int S_IFLNK = OsConstants.S_IFLNK;
|
||||
|
||||
static final int S_IFCHR = OsConstants.S_IFCHR;
|
||||
|
||||
static final int S_IFBLK = OsConstants.S_IFBLK;
|
||||
|
||||
static final int S_IFIFO = OsConstants.S_IFIFO;
|
||||
|
||||
static final int R_OK = OsConstants.R_OK;
|
||||
|
||||
static final int W_OK = OsConstants.W_OK;
|
||||
|
||||
static final int X_OK = OsConstants.X_OK;
|
||||
|
||||
static final int F_OK = OsConstants.F_OK;
|
||||
|
||||
static final int ENOENT = OsConstants.ENOENT;
|
||||
|
||||
static final int EACCES = OsConstants.EACCES;
|
||||
|
||||
static final int EEXIST = OsConstants.EEXIST;
|
||||
|
||||
static final int ENOTDIR = OsConstants.ENOTDIR;
|
||||
|
||||
static final int EINVAL = OsConstants.EINVAL;
|
||||
|
||||
static final int EXDEV = OsConstants.EXDEV;
|
||||
|
||||
static final int EISDIR = OsConstants.EISDIR;
|
||||
|
||||
static final int ENOTEMPTY = OsConstants.ENOTEMPTY;
|
||||
|
||||
static final int ENOSPC = OsConstants.ENOSPC;
|
||||
|
||||
static final int EAGAIN = OsConstants.EAGAIN;
|
||||
|
||||
static final int ENOSYS = OsConstants.ENOSYS;
|
||||
|
||||
static final int ELOOP = OsConstants.ELOOP;
|
||||
|
||||
static final int EROFS = OsConstants.EROFS;
|
||||
|
||||
static final int ENODATA = OsConstants.ENODATA;
|
||||
|
||||
static final int ERANGE = OsConstants.ERANGE;
|
||||
|
||||
static final int EMFILE = OsConstants.EMFILE;
|
||||
|
||||
// S_IAMB are access mode bits, therefore, calculated by taking OR of all the read, write and
|
||||
// execute permissions bits for owner, group and other.
|
||||
private static int get_S_IAMB() {
|
||||
return (OsConstants.S_IRUSR | OsConstants.S_IWUSR | OsConstants.S_IXUSR |
|
||||
OsConstants.S_IRGRP | OsConstants.S_IWGRP | OsConstants.S_IXGRP |
|
||||
OsConstants.S_IROTH | OsConstants.S_IWOTH | OsConstants.S_IXOTH);
|
||||
}
|
||||
// END Android-changed: Use constants from android.system.OsConstants. http://b/32203242
|
||||
|
||||
|
||||
static final int AT_SYMLINK_NOFOLLOW = 0x100;
|
||||
static final int AT_REMOVEDIR = 0x200;
|
||||
}
|
301
app/src/main/java/com/termux/app/file/tests/FileUtilsTests.java
Normal file
301
app/src/main/java/com/termux/app/file/tests/FileUtilsTests.java
Normal file
@@ -0,0 +1,301 @@
|
||||
package com.termux.app.file.tests;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.app.TermuxConstants;
|
||||
import com.termux.app.file.FileUtils;
|
||||
import com.termux.app.utils.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public class FileUtilsTests {
|
||||
|
||||
private static final String LOG_TAG = "FileUtilsTests";
|
||||
|
||||
/**
|
||||
* Run basic tests for {@link FileUtils} class.
|
||||
*
|
||||
* Move tests need to be written, specially for failures.
|
||||
*
|
||||
* The log level must be set to verbose.
|
||||
*
|
||||
* Run at app startup like in an activity
|
||||
* FileUtilsTests.runTests(this, TermuxConstants.TERMUX_HOME_DIR_PATH + "/FileUtilsTests");
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
*/
|
||||
public static void runTests(@NonNull final Context context, @NonNull final String testRootDirectoryPath) {
|
||||
try {
|
||||
Logger.logInfo(LOG_TAG, "Running tests");
|
||||
Logger.logInfo(LOG_TAG, "testRootDirectoryPath: \"" + testRootDirectoryPath + "\"");
|
||||
|
||||
String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null, false);
|
||||
assertEqual("FileUtilsTests directory path is not a canonical path", testRootDirectoryPath, fileUtilsTestsDirectoryCanonicalPath);
|
||||
|
||||
runTestsInner(context, testRootDirectoryPath);
|
||||
Logger.logInfo(LOG_TAG, "All tests successful");
|
||||
} catch (Exception e) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void runTestsInner(@NonNull final Context context, @NonNull final String testRootDirectoryPath) throws Exception {
|
||||
String errmsg;
|
||||
String label;
|
||||
String path;
|
||||
|
||||
/*
|
||||
* - dir1
|
||||
* - sub_dir1
|
||||
* - sub_reg1
|
||||
* - sub_sym1 (absolute symlink to dir2)
|
||||
* - sub_sym2 (copy of sub_sym1 for symlink to dir2)
|
||||
* - sub_sym3 (relative symlink to dir4)
|
||||
* - dir2
|
||||
* - sub_reg1
|
||||
* - sub_reg2 (copy of dir2/sub_reg1)
|
||||
* - dir3 (copy of dir1)
|
||||
* - dir4 (moved from dir3)
|
||||
*/
|
||||
|
||||
String dir1_label = "dir1";
|
||||
String dir1_path = testRootDirectoryPath + "/dir1";
|
||||
|
||||
String dir1__sub_dir1_label = "dir1/sub_dir1";
|
||||
String dir1__sub_dir1_path = dir1_path + "/sub_dir1";
|
||||
|
||||
String dir1__sub_reg1_label = "dir1/sub_reg1";
|
||||
String dir1__sub_reg1_path = dir1_path + "/sub_reg1";
|
||||
|
||||
String dir1__sub_sym1_label = "dir1/sub_sym1";
|
||||
String dir1__sub_sym1_path = dir1_path + "/sub_sym1";
|
||||
|
||||
String dir1__sub_sym2_label = "dir1/sub_sym2";
|
||||
String dir1__sub_sym2_path = dir1_path + "/sub_sym2";
|
||||
|
||||
String dir1__sub_sym3_label = "dir1/sub_sym3";
|
||||
String dir1__sub_sym3_path = dir1_path + "/sub_sym3";
|
||||
|
||||
|
||||
String dir2_label = "dir2";
|
||||
String dir2_path = testRootDirectoryPath + "/dir2";
|
||||
|
||||
String dir2__sub_reg1_label = "dir2/sub_reg1";
|
||||
String dir2__sub_reg1_path = dir2_path + "/sub_reg1";
|
||||
|
||||
String dir2__sub_reg2_label = "dir2/sub_reg2";
|
||||
String dir2__sub_reg2_path = dir2_path + "/sub_reg2";
|
||||
|
||||
|
||||
String dir3_label = "dir3";
|
||||
String dir3_path = testRootDirectoryPath + "/dir3";
|
||||
|
||||
String dir4_label = "dir4";
|
||||
String dir4_path = testRootDirectoryPath + "/dir4";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Create or clear test root directory file
|
||||
label = "testRootDirectoryPath";
|
||||
errmsg = FileUtils.clearDirectory(context, label, testRootDirectoryPath);
|
||||
assertEqual("Failed to create " + label + " directory file", null, errmsg);
|
||||
|
||||
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);
|
||||
|
||||
// Create dir2 directory file
|
||||
errmsg = FileUtils.createDirectoryFile(context, dir2_label, dir2_path);
|
||||
assertEqual("Failed to create " + dir2_label + " directory file", null, errmsg);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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))
|
||||
throwException("The " + label + " symlink file does not point to " + dir2_label);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
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);
|
||||
if(!FileUtils.regularFileExists(path, false))
|
||||
throwException("The " + label + " regular file does not exist as expected after copying it from " + dir2__sub_reg1_label);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
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);
|
||||
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);
|
||||
if(!FileUtils.directoryFileExists(path, false))
|
||||
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir3_label);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
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);
|
||||
if(!FileUtils.symlinkFileExists(path))
|
||||
throwException("The " + label + " dangling symlink file does not exist as expected after creation");
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
if(FileUtils.fileExists(path, false))
|
||||
throwException("The " + label + " symlink file still exist after deletion");
|
||||
|
||||
// Check if dir2 directory file still exists after deletion of dir1/sub_sym2 since it was a symlink to dir2
|
||||
// When deleting a symlink file, its target must not be deleted
|
||||
label = dir2_label; path = dir2_path;
|
||||
if(!FileUtils.directoryFileExists(path, false))
|
||||
throwException("The " + label + " directory file has unexpectedly been deleted after deletion of " + dir1__sub_sym2_label);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
if(FileUtils.fileExists(path, false))
|
||||
throwException("The " + label + " directory file still exist after deletion");
|
||||
|
||||
|
||||
// Check if dir2 directory file and dir2/sub_reg1 regular file still exist after deletion of
|
||||
// dir1 since there was a dir1/sub_sym1 symlink to dir2 in it
|
||||
// When deleting a directory, any targets of symlinks must not be deleted when deleting symlink files
|
||||
label = dir2_label; path = dir2_path;
|
||||
if(!FileUtils.directoryFileExists(path, false))
|
||||
throwException("The " + label + " directory file has unexpectedly been deleted after deletion of " + dir1_label);
|
||||
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
|
||||
if(!FileUtils.fileExists(path, false))
|
||||
throwException("The " + label + " regular file has unexpectedly been deleted after deletion of " + dir1_label);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
if(FileUtils.fileExists(path, false))
|
||||
throwException("The " + label + " regular file still exist after deletion");
|
||||
|
||||
FileUtils.getFileType("/dev/ptmx", false);
|
||||
FileUtils.getFileType("/dev/null", false);
|
||||
}
|
||||
|
||||
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 + "\"");
|
||||
}
|
||||
|
||||
private static boolean equalsRegardingNull(final String expected, final String actual) {
|
||||
if (expected == null) {
|
||||
return actual == null;
|
||||
}
|
||||
|
||||
return isEquals(expected, actual);
|
||||
}
|
||||
|
||||
private static boolean isEquals(String expected, String actual) {
|
||||
return expected.equals(actual);
|
||||
}
|
||||
|
||||
public static void throwException(@NonNull final String message) throws Exception {
|
||||
throw new Exception(message);
|
||||
}
|
||||
|
||||
}
|
@@ -1,376 +0,0 @@
|
||||
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.error_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.error_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.error_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.error_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.error_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.error_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.error_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.error_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.error_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.error_null_or_empty_path);
|
||||
|
||||
if (!isValidPermissingString(permissionsToCheck)) {
|
||||
Logger.logError(LOG_TAG, "Invalid permissionsToCheck passed to checkMissingFilePermissions: \"" + permissionsToCheck + "\"");
|
||||
return context.getString(R.string.error_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.error_file_not_readable, fileType);
|
||||
}
|
||||
|
||||
// If file is not writable
|
||||
if (permissionsToCheck.contains("w") && !file.canWrite()) {
|
||||
return context.getString(R.string.error_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.error_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();
|
||||
}
|
||||
|
||||
}
|
@@ -104,21 +104,50 @@
|
||||
|
||||
<!-- Termux FileUtils -->
|
||||
<string name="error_executable_required">Executable required.</string>
|
||||
<string name="error_null_or_empty_path">The path is null or empty.</string>
|
||||
<string name="error_null_or_empty_file">The file is null or empty.</string>
|
||||
<string name="error_null_or_empty_executable">The executable is null or empty.</string>
|
||||
<string name="error_null_or_empty_directory">The directory is null or empty.</string>
|
||||
<string name="error_null_or_empty_parameter">The %1$s is to \"%2$s\" null or empty.</string>
|
||||
<string name="error_null_or_empty_regular_file_path">The regular file path is null or empty.</string>
|
||||
<string name="error_null_or_empty_regular_file">The regular file is null or empty.</string>
|
||||
<string name="error_null_or_empty_executable_file_path">The executable file path is null or empty.</string>
|
||||
<string name="error_null_or_empty_executable_file">The executable file is null or empty.</string>
|
||||
<string name="error_null_or_empty_directory_file_path">The directory file path is null or empty.</string>
|
||||
<string name="error_null_or_empty_directory_file">The directory file is null or empty.</string>
|
||||
|
||||
<string name="error_file_not_found_at_path">The %1$s is not found at path \"%2$s\".</string>
|
||||
<string name="error_no_regular_file_found">Regular file not found at %1$s path.</string>
|
||||
<string name="error_not_a_regular_file">The %1$s at path \"%2$s\" is not a regular file.</string>
|
||||
<string name="error_non_regular_file_found">Non-regular file found at %1$s path.</string>
|
||||
<string name="error_non_directory_file_found">Non-directory file found at %1$s path.</string>
|
||||
<string name="error_non_symlink_file_found">Non-symlink file found at %1$s path.</string>
|
||||
<string name="error_file_not_an_allowed_file_type">The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".</string>
|
||||
|
||||
<string name="error_validate_file_existence_and_permissions_failed_with_exception">Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
||||
<string name="error_validate_directory_existence_and_permissions_failed_with_exception">Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
||||
|
||||
<string name="error_creating_file_failed">Creating %1$s at path \"%2$s\" failed.</string>
|
||||
<string name="error_creating_file_failed_with_exception">Creating %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
||||
|
||||
<string name="error_cannot_overwrite_a_non_symlink_file_type">Cannot overwrite %1$s while creating symlink at \"%2$s\" to \"%3$s\" since destination file type \"%4$s\" is not a symlink.</string>
|
||||
<string name="error_creating_symlink_file_failed_with_exception">Creating %1$s at path \"%2$s\" to \"%3$s\" failed.\nException: %4$s</string>
|
||||
|
||||
<string name="error_copying_or_moving_file_failed_with_exception">%1$s from \"%2$s\" to \"%3$s\" failed.\nException: %4$s</string>
|
||||
<string name="error_copying_or_moving_file_to_same_path">%1$s from \"%2$s\" to \"%3$s\" cannot be done since they point to the same path.</string>
|
||||
<string name="error_cannot_overwrite_a_different_file_type">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\".</string>
|
||||
<string name="error_cannot_move_directory_to_sub_directory_of_itself">Cannot move %1$s from \"%2$s\" to \"%3$s\" since destination is a subdirectory of the source.</string>
|
||||
|
||||
<string name="error_file_still_exists_after_deleting">The %1$s still exists after deleting it from \"%2$s\".</string>
|
||||
<string name="error_deleting_file_failed">Deleting %1$s at path \"%2$s\" failed.</string>
|
||||
<string name="error_deleting_file_failed_with_exception">Deleting %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
||||
<string name="error_clearing_directory_failed_with_exception">Clearing %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
||||
|
||||
<string name="error_reading_string_to_file_failed_with_exception">Reading string from %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
||||
<string name="error_writing_string_to_file_failed_with_exception">Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
||||
<string name="error_unsupported_charset">Unsupported charset \"%1$s\"</string>
|
||||
<string name="error_checking_if_charset_supported_failed">Checking if charset \"%1$s\" is suppoted failed.\nException: %2$s</string>
|
||||
|
||||
<string name="error_invalid_file_permissions_string_to_check">The file permission string to check is invalid.</string>
|
||||
<string name="error_no_regular_file_found">Regular file not found at path.</string>
|
||||
<string name="error_no_directory_found">Directory not found at path.</string>
|
||||
<string name="error_file_not_readable">%1$s at path is not readable. Permission Denied.</string>
|
||||
<string name="error_file_not_writable">%1$s at path is not writable. Permission Denied.</string>
|
||||
<string name="error_file_not_executable">%1$s at path is not executable. Permission Denied.</string>
|
||||
<string name="error_non_regular_file_found">Non-regular file found at path.</string>
|
||||
<string name="error_non_directory_file_found">Non-directory file found at path.</string>
|
||||
<string name="error_creating_missing_directory_failed">Failed to create missing directory at path: \"%1$s\"</string>
|
||||
<string name="error_validate_file_existence_and_permissions_failed_with_exception">Validating file existence and permissions fafiled: \"%1$s\"\nException: %2$s</string>
|
||||
<string name="error_validate_directory_existence_and_permissions_failed_with_exception">Validating directory existence and permissions fafiled: \"%1$s\"\nException: %2$s</string>
|
||||
<string name="error_file_not_readable">The %1$s at path is not readable. Permission Denied.</string>
|
||||
<string name="error_file_not_writable">The %1$s at path is not writable. Permission Denied.</string>
|
||||
<string name="error_file_not_executable">The %1$s at path is not executable. Permission Denied.</string>
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user