package com.termux.shared.file.filesystem; import android.system.Os; import androidx.annotation.NonNull; import com.termux.shared.logger.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; } }