[ Android实战 ] 判断文件是否为软链接或硬链接
尊重原创,转载请注明出处!
创作不易,如有帮助请点赞支持~
背景
最近项目遇到了一个需求,出于安全方面的考虑,需要在操作文件前判断文件是否为软链接,做一些处理。于是研究了一下,对比尝试了多种解决方案,记录下来。
硬链接和软链接的区别
搜索相关资料的过程中,了解了一下软链接和硬链接的区别,这里先介绍下(只是简单科普一下,虽然不准确,但是方便理解,具体的解释可以自行百度):
硬链接:假设 A 是 B 的硬链接,可以理解为 A 和 B 指向同一个文件 C。修改 A 或 B 时,都会进行同步,影响另一个文件的内容;删除 A 或 B,都不会影响另一个文件,只是节点链接数会减 1。
软链接:假设 A 是 B 的软链接,可以理解为 A 是 B 的快捷方式。修改 A 时,实际修改的是 B 的内容;删除 A 对 B 没有任何的影响;而如果删除 B,虽然 A 仍然存在,但是会导致 A 指向一个无效路径。
对硬链接和软链接的区别有一个简单的认识后,我们来实践一下,分别创建软链接和硬链接:
(PS:这里是有 root 权限的,但是显示问题,为了格式好看一点,把 # 改成了 $)
XXX:/data/test $ echo "file1" > file1 # 创建file1
XXX:/data/test $ echo "file2" > file2 # 创建file2
XXX:/data/test $ ln -s file1 soft_link # 创建一个soft_link指向file1的软链接
XXX:/data/test $ ln file2 hard_link # 创建一个hard_link指向file2的硬链接
XXX:/data/test $ ln -s file3 invalid_link # 创建一个指向无效链接的软链接
XXX:/data/test $ ls -al
total 48
drwxrwxrwx 2 root root 4096 2022-03-03 19:35 .
drwxrwx--x 53 system system 4096 2022-03-03 19:19 ..
-rw-rw-rw- 1 root root 6 2022-03-03 19:34 file1
-rw-rw-rw- 2 root root 6 2022-03-03 19:34 file2
-rw-rw-rw- 2 root root 6 2022-03-03 19:34 hard_link
lrwxrwxrwx 1 root root 5 2022-03-04 16:19 invalid_link -> file3
lrwxrwxrwx 1 root root 5 2022-03-03 19:34 soft_link -> file1
从以上指令可以更加直观地看出两种链接方式的差异:
软链接通过以下指令创建(即使 file 不存在,一样可以创建 link):ln -s file link
硬链接通过以下指令创建(file 必须存在,才能创建 link):ln file link
软链接能够很直观地看到 soft_link -> file1 的提示,并且在属性那列,可以看出它的文件类型是 l(link file)。
而相比起来,硬链接则无法看到它的指向,并且文件类型也只是 -(file),只能看到硬链接和原文件的 inode 节点数都变成了 2。
file.getCanonicalPath
如何判断文件是否为软链接,网上找到最多的一种方法如下:
public static boolean isSymlink(String path) throws IOException {
File file = new File(path);
File canon;
if (file.getParent() == null) {
canon = file;
} else {
File canonDir = file.getParentFile().getCanonicalFile();
canon = new File(canonDir, file.getName());
}
return !canon.getCanonicalFile().equals(canon.getAbsoluteFile());
}
其实,就是根据 file.getAbsoluteFile
获取到文件本身的路径,file.getCanonicalFile
获取文件真正指向的路径,来实现是否为软链接的判断。
这个方案,在 Android 7.1、8.1、10.0 上均测试通过,但是只能用于判断有效链接的软链接,无法区分是否为硬链接或无效链接的软链接。
日志如下:
03-04 16:23:39.616 6247 6264 D DEBUG : path =========== /data/test/soft_link
03-04 16:23:39.617 6247 6264 D DEBUG : getAbsolutePath: /data/test/soft_link, getCanonicalPath: /data/test/file1
03-04 16:23:39.618 6247 6264 D DEBUG : isSymlink: true
03-04 16:23:39.637 6247 6264 D DEBUG : path =========== /data/test/hard_link
03-04 16:23:39.637 6247 6264 D DEBUG : getAbsolutePath: /data/test/hard_link, getCanonicalPath: /data/test/hard_link
03-04 16:23:39.638 6247 6264 D DEBUG : isSymlink: false
03-04 16:23:39.639 6247 6264 D DEBUG : path =========== /data/test/invalid_link
03-04 16:23:39.639 6247 6264 D DEBUG : getAbsolutePath: /data/test/invalid_link, getCanonicalPath: /data/test/invalid_link
03-04 16:23:39.639 6247 6264 D DEBUG : isSymlink: false
注意:如果某些路径下发现该方法失效,很可能是 selinux 策略限制的问题,可以执行 adb shell setenforce 0
暂时关闭 selinux 进行验证。
Files.isSymbolicLink
除了第一种方案,Files.isSymbolicLink
也是网上找到的另一个实现方案,Files 是 java.nio.file
包下的一个类。
实现代码很简单,如下:
public static boolean isSymbolicLink(String path) throws IOException {
Path path1 = Paths.get(path);
return Files.isSymbolicLink(path1);
}
但是这种方案,在 Android 7.1 上直接崩溃,在 Android 8.1、10.0 则有效,测试通过。
相比上一种方案,Files.isSymbolicLink
的优势是即使指向无效链接,依然可以知道它是软链接。
Android 7.1 上报错如下:
E AndroidRuntime: java.lang.NoSuchMethodError: No virtual method toPath()Ljava/nio/file/Path; in class Ljava/io/File;
or its super classes (declaration of 'java.io.File' appears in /system/framework/core-oj.jar)
搜索了一下 Android 7.1 的代码,在 libcore\ojluni\src\main\java\java\nio
下确实没有找到 file
这个包,自然也就不支持 Files 的调用了。
Android 8.1、10.0 上日志如下:
03-04 16:36:45.409 6631 6648 D DEBUG : path =========== /data/test/soft_link
03-04 16:36:45.438 6631 6648 D DEBUG : isSymbolicLink: true
03-04 16:36:45.438 6631 6648 D DEBUG : path =========== /data/test/hard_link
03-04 16:36:45.439 6631 6648 D DEBUG : isSymbolicLink: false
03-04 16:36:45.439 6631 6648 D DEBUG : path =========== /data/test/invalid_link
03-04 16:36:45.440 6631 6648 D DEBUG : isSymbolicLink: true
path.toRealPath
path.toRealPath
返回的是文件的真实路径,它同样是 java.nio.file
包里的一个方法,因此跟上一种方案一样,不适用于 Android 7.1,但是在 Android 8.1、10.0 上能测试通过。
实现代码也很简单:
public static boolean isSymbolicLink2(String path) throws IOException {
Path path1 = Paths.get(path);
return !path1.equals(path1.toRealPath());
}
Android 8.1、10.0 上日志如下:
03-04 17:07:14.973 10782 10818 D DEBUG : path =========== /data/test/soft_link
03-04 17:07:14.983 10782 10818 D DEBUG : toRealPath1: /data/test/soft_link
03-04 17:07:14.989 10782 10818 D DEBUG : toRealPath2: /data/test/file1
03-04 17:07:14.990 10782 10818 D DEBUG : isSymbolicLink2: true
03-04 17:07:14.991 10782 10818 D DEBUG : path =========== /data/test/hard_link
03-04 17:07:14.991 10782 10818 D DEBUG : toRealPath1: /data/test/hard_link
03-04 17:07:14.992 10782 10818 D DEBUG : toRealPath2: /data/test/hard_link
03-04 17:07:14.992 10782 10818 D DEBUG : isSymbolicLink2: false
03-04 17:07:14.992 10782 10818 D DEBUG : path =========== /data/test/invalid_link
03-04 17:07:14.993 10782 10818 D DEBUG : toRealPath1: /data/test/invalid_link
03-04 17:07:14.993 10782 10818 E DEBUG : IOException: /data/test/invalid_link
03-04 17:07:14.994 10782 10818 W System.err: java.nio.file.NoSuchFileException: /data/test/invalid_link
03-04 17:07:14.994 10782 10818 W System.err: at sun.nio.fs.UnixPath.toRealPath(UnixPath.java:837)
03-04 17:07:14.994 10782 10818 W System.err: at com.example.demo.SimpleActivity$1.run(SimpleActivity.java:191)
03-04 17:07:14.994 10782 10818 W System.err: at java.lang.Thread.run(Thread.java:919)
注意,path.toRealPath
函数定义有一个 LinkOption
的不定参数,LinkOption
是一个枚举,只有一个常量 NOFOLLOW_LINKS
,表示不指向符号链接。
Path toRealPath(LinkOption... options) throws IOException;
上面的日志中,toRealPath1
对应 path.toRealPath(LinkOption.NOFOLLOW_LINKS)
的返回值,而 toRealPath2
对应 path.toRealPath()
的返回值。
可以发现,当 path.toRealPath
传入 LinkOption.NOFOLLOW_LINKS
,获取到的路径是文件本身的真实路径。
而如果不传参数,如果操作的是软链接,则能获取到软链接指向的文件的真实路径。
如果该软链接是一个无效连接,则会抛出异常。
相比上一种方案,这种方案能区分出有效软链接和无效软链接,这也算是它的一个优势吧。
lstat
除了 java 的实现方案,通过 JNI 调用 C 去实现也是一种思路。C 提供了 fstat
和 lstat
函数来获取文件的信息。
fstat
直接操作的是软链接指向的文件,因此无法判断是否为软链接,在这种场景下不使用它,而使用 lstat
来实现。
JNI 接口实现方式如下:
JNIEXPORT jint JNICALL Java_com_example_jni_Test_isSymbolicLinkNative
(JNIEnv *env, jobject obj, jstring jstr)
{
char *filename = (char*) env->GetStringUTFChars(jstr, JNI_FALSE);
struct stat _stat;
int ret = -1;
if(lstat(filename, &_stat) < 0) {
LOGE("%s lstat error", filename);
return -1;
}
// 也可以通过 _stat.st_mode & S_IFLNK 进行判断
ret = S_ISLNK(_stat.st_mode);
env->ReleaseStringUTFChars(jstr, filename);
return ret;
}
JNIEXPORT jint JNICALL Java_com_example_jni_Test_getNLinkNative
(JNIEnv *env, jobject obj, jstring jstr)
{
char *filename = (char*) env->GetStringUTFChars(jstr, JNI_FALSE);
struct stat _stat;
int ret = -1;
if(lstat(filename, &_stat) < 0) {
LOGE("%s lstat error", filename);
return -1;
}
ret = _stat.st_nlink;
env->ReleaseStringUTFChars(jstr, filename);
return ret;
}
在 Android 7.1、8.1、10.0 上均测试通过,日志如下:
03-04 18:15:55.536 12317 12352 D DEBUG : path =========== /data/test/soft_link
03-04 18:15:55.538 12317 12352 D DEBUG : isSymbolicLinkNative: 1
03-04 18:15:55.539 12317 12352 D DEBUG : getNLinkNative: 1
03-04 18:15:55.539 12317 12352 D DEBUG : path =========== /data/test/hard_link
03-04 18:15:55.539 12317 12352 D DEBUG : isSymbolicLinkNative: 0
03-04 18:15:55.539 12317 12352 D DEBUG : getNLinkNative: 2
03-04 18:15:55.539 12317 12352 D DEBUG : path =========== /data/test/invalid_link
03-04 18:15:55.539 12317 12352 D DEBUG : isSymbolicLinkNative: 1
03-04 18:15:55.539 12317 12352 D DEBUG : getNLinkNative: 1
可以发现,只要是软链接,不管链接文件是否有效,都可以通过 stat.st_mode
进行判断。
另外,我上面还用到了 stat.st_nlink
,它可以获取到文件的硬链接数目,用于判断是否为硬链接。
总结
对上面几种方案的总结如下:
1、file.getCanonicalPath
适用于不同 Android 平台,能获取到有效软链接的文件路径,但是无法判断无效软连接和硬链接
2、Files.isSymbolicLink
只适用于 Android 8 以后的平台,能用于判断是否为软链接(不管软链接是否有效),无法判断是否为硬链接
3、Path.toRealPath
同样只适用于 Android 8 以后的平台,能用于获取到有效软链接的文件路径,虽然无法直接判断无效软连接,但可以通过捕获异常的方式进行处理,无法判断是否为硬链接
4、lstat
是通过 JNI 调用 C 代码实现,适用于不同 Android 平台,因为是直接读取文件属性,能用于判断是否为软链接(不管软链接是否有效),也可以通过硬链接节点数判断是否为硬链接
PS:在高版本上进行测试的时候,随着文件路径的改变,可能会发现方法执行失效的问题,这很有可能是因为 selinux 策略的限制导致的,这时就需要考虑是否修改对应的 selinux 策略了。