Fuse不支持特殊字符路径 - Android

1,158 阅读2分钟

1. 问题描述

Android 13版本,应用在外卡目录创建文件时,如果文件名中包含特殊字符,创建失败,返回EPERM

2. 源码分析

Andorid系统自Android 11版本以来,使用Fuse文件系统管理外部存储,其由两部分构成,第一部分位于Linux内核,第二部分位于进程MediaProvdier的用户态空间。分析此问题,首先需要明确拦截特殊字符的动作,是发生在内核空间还是用户空间,分析Fuse内核代码后,未发现有处理特殊字符的相关代码,所以拦截动作应发生在用户空间。

创建文件场景中,MediaProvider函数执行流程如下图所示:

页-1(1).png

  1. 内核转发创建文件操作至用户态空间的MediaProvdier进程
  2. FuseDaemon接受请求,调用MediaProvdierWrapper的InsertFile
  3. InsetFile首先判断调用者的uid,如果该用户为root,则直接返回,否则执行内部调用insertFileInternal
  4. insertFileInternal通过Jni调用至Java类MediaProvider的insertFileNecessaryForFuse
  5. 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. 注意事项

  1. 如果进程uid为0, 即Root进程,没有该限制。因为判断uid==0,InsertFile函数将提前返回。
  2. 如果应用访问的是外卡的沙箱目录,即/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