[ Android实战 ] 判断文件是否为软链接或硬链接

本文介绍了在Android环境中如何判断文件是否为软链接或硬链接,通过`file.getCanonicalPath`、`Files.isSymbolicLink`、`Path.toRealPath`和JNI调用`lstat`等方法进行分析比较,探讨了各种方法的适用性和局限性,尤其是在不同Android版本上的表现。
摘要由CSDN通过智能技术生成

尊重原创,转载请注明出处!
创作不易,如有帮助请点赞支持~

背景

最近项目遇到了一个需求,出于安全方面的考虑,需要在操作文件前判断文件是否为软链接,做一些处理。于是研究了一下,对比尝试了多种解决方案,记录下来。

硬链接和软链接的区别

搜索相关资料的过程中,了解了一下软链接和硬链接的区别,这里先介绍下(只是简单科普一下,虽然不准确,但是方便理解,具体的解释可以自行百度):

硬链接:假设 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 提供了 fstatlstat 函数来获取文件的信息。

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 策略了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值