1. 问题描述
Android 13版本,应用在外卡目录创建文件时,如果文件名中包含特殊字符,创建失败,返回EPERM
2. 源码分析
Andorid系统自Android 11版本以来,使用Fuse文件系统管理外部存储,其由两部分构成,第一部分位于Linux内核,第二部分位于进程MediaProvdier的用户态空间。分析此问题,首先需要明确拦截特殊字符的动作,是发生在内核空间还是用户空间,分析Fuse内核代码后,未发现有处理特殊字符的相关代码,所以拦截动作应发生在用户空间。
创建文件场景中,MediaProvider函数执行流程如下图所示:
- 内核转发创建文件操作至用户态空间的MediaProvdier进程
- FuseDaemon接受请求,调用MediaProvdierWrapper的InsertFile
- InsetFile首先判断调用者的uid,如果该用户为root,则直接返回,否则执行内部调用insertFileInternal
- insertFileInternal通过Jni调用至Java类MediaProvider的insertFileNecessaryForFuse
- insertFileNecessaryForFuse内部调用FileUtil的getAbsoluteSanitizedPath,完成路径的预处理,然后将原始路径与预处理后的路径进行比较,如果二者不相等,则返回错误码EPERM
if (!path.equals(getAbsoluteSanitizedPath(path))) {
Log.e(TAG, "File name contains invalid characters");
return OsConstants.EPERM;
}
通过上述分析,特殊字符的拦截动作发生在MediaProvider
接下来将分析,类FileUtils预处理路径的流程, getAbsoluteSanitizedPath方法是第一步将路径按照路径分割符切割,然后将路径的每一个部分预处理,最后拼接成一个完整的路径并返回。
预处理代码如下:
/**
* Mutate the given filename to make it valid for a FAT filesystem,
* replacing any invalid characters with "_".
*
* @hide
*/
public static String buildValidFatFilename(String name) {
if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
return "(invalid)";
}
final StringBuilder res = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) {
final char c = name.charAt(i);
if (isValidFatFilenameChar(c)) {
res.append(c);
} else {
res.append('_');
}
}
trimFilename(res, MAX_FILENAME_BYTES);
return res.toString();
}
通过上述代码不难看出,预处理过程通过函数 isValidFatFilenameChar 判断字符是否合法,如果是非法字符,则使用字符 “_” 代替
isValidFatFilenameChar实现如下:
private static boolean isValidFatFilenameChar(char c) {
if ((0x00 <= c && c <= 0x1f)) {
return false;
}
switch (c) {
case '"':
case '*':
case '/':
case ':':
case '<':
case '>':
case '?':
case '\\':
case '|':
case 0x7F:
return false;
default:
return true;
}
}
综上所述,文件路径中如果包含 “*” 等特殊字符,创建文件失败
3. 注意事项
- 如果进程uid为0, 即Root进程,没有该限制。因为判断uid==0,InsertFile函数将提前返回。
- 如果应用访问的是外卡的沙箱目录,即/sdcard/Android/data, /sdcard/Android/obb等目录,也没有此限制,因为此路径是F2FS文件系统管辖, 而不是Fuse文件系统
4. 参考
AOSP源码,分支:android-13.0.0_r35
FuseDaemon:packages/providers/MediaProvider/jni/FuseDaemon.cpp
MediaProviderWrapper:packages/providers/MediaProvider/jni/MediaProviderWrapper.cpp
MediaProvider:packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
FileUtils:packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java