如何在Java中安全地编码字符串以用作文件名?

Ste*_*eod 108 java string encoding file

我从外部进程收到一个字符串.我想使用该String来创建文件名,然后写入该文件.这是我的代码片段:

    String s = ... // comes from external source
    File currentFile = new File(System.getProperty("user.home"), s);
    PrintWriter currentWriter = new PrintWriter(currentFile);
Run Code Online (Sandbox Code Playgroud)

如果s包含无效字符,例如基于Unix的OS中的"/",则会(正确地)抛出java.io.FileNotFoundException.

如何安全地编码String以便它可以用作文件名?

编辑:我希望的是一个API调用,它为我做这个.

我可以做这个:

    String s = ... // comes from external source
    File currentFile = new File(System.getProperty("user.home"), URLEncoder.encode(s, "UTF-8"));
    PrintWriter currentWriter = new PrintWriter(currentFile);
Run Code Online (Sandbox Code Playgroud)

但我不确定URLEncoder是否可靠用于此目的.

cle*_*tus 98

我的建议是采用"白名单"方法,这意味着不要尝试过滤掉不良角色.而是定义什么是好的.您可以拒绝文件名或过滤它.如果要过滤它:

String name = s.replaceAll("\\W+", "");
Run Code Online (Sandbox Code Playgroud)

这样做是替换任何不是数字,字母或下划线的字符.或者,您可以用另一个字符(如下划线)替换它们.

问题是如果这是一个共享目录,那么你不希望文件名冲突.即使用户隔离了用户存储区域,也可能只是通过过滤掉不良字符而导致文件冲突.如果用户想要下载它,那么用户输入的名称通常很有用.

出于这个原因,我倾向于允许用户输入他们想要的内容,根据我自己选择的方案存储文件名(例如userId_fileId),然后将用户的文件名存储在数据库表中.这样,您可以将其显示回用户,存储您想要的内容,并且不会危及安全性或消除其他文件.

您也可以对文件进行哈希处理(例如MD5哈希),但是您无法列出用户输入的文件(无论如何都没有有意义的名称).

编辑:修复了java的正则表达式

  • 为了创建一个关心算法是否"被破坏"的唯一文件名? (16认同)
  • 你需要在Java中使用`"\\ W +"`作为regexp.反斜杠首先应用于字符串本身,而`\ W`不是有效的转义序列.我试着编辑答案,但看起来有人拒绝了我的编辑:( (8认同)
  • @cletus:问题是不同的字符串会映射到相同的文件名; 即碰撞. (3认同)
  • 碰撞必须是故意的,原始问题不会谈论攻击者选择的这些字符串. (3认同)

vog*_*vog 34

这取决于编码是否应该是可逆的.

可逆

使用URL encoding(java.net.URLEncoder)替换特殊字符%xx.请注意,您要处理字符串等于,等于或为空的特殊情况!¹许多程序使用URL编码来创建文件名,因此这是每个人都能理解的标准技术....

不可逆

使用给定字符串的哈希值(例如SHA-1).现代哈希算法(不是 MD5)可以被认为是无冲突的.事实上,如果发现碰撞,您将在密码学方面取得突破.


¹您可以使用诸如的前缀优雅地处理所有3个特殊情况"myApp-".如果直接放入文件$HOME,则必须这样做,以避免与现有文件(如".bashrc")冲突.
public static String encodeFilename(String s)
{
    try
    {
        return "myApp-" + java.net.URLEncoder.encode(s, "UTF-8");
    }
    catch (java.io.UnsupportedEncodingException e)
    {
        throw new RuntimeException("UTF-8 is an unknown encoding!?");
    }
}

  • @vog:"*"只允许在大多数基于Unix的文件系统中使用,NTFS和FAT32不支持它. (6认同)
  • @vog:URLEncoder因"."而失败.和"......".这些必须编码,否则您将与$ HOME中的目录条目冲突 (4认同)
  • URLEncoder 关于什么是特殊字符的想法可能不正确。 (2认同)
  • 请注意,任何延长文件名的方法(通过将单个字符更改为 %20 或其他)都会使一些接近长度限制(对于 Unix 系统为 255 个字符)的文件名无效 (2认同)

Jon*_*ica 21

这是我使用的:

public String sanitizeFilename(String inputName) {
    return inputName.replaceAll("[^a-zA-Z0-9-_\\.]", "_");
}
Run Code Online (Sandbox Code Playgroud)

它的作用是使用正则表达式替换每个不是字母,数字,下划线或带下划线的点的字符.

这意味着"如何将£转换为$"之类的内容将变为"How_to_convert___to__".不可否认,这个结果不是非常用户友好,但它是安全的,并且保证生成的目录/文件名在任何地方都可以使用.在我的情况下,结果不会显示给用户,因此不是问题,但您可能希望将正则表达式更改为更宽松.

值得注意的是,我遇到的另一个问题是我有时会得到相同的名称(因为它基于用户输入),所以你应该知道这一点,因为你不能在一个目录中有多个同名的目录/文件.此外,您可能需要截断或缩短生成的字符串,因为它可能超过某些系统具有的255个字符限制.

  • 另一个问题是它特定于使用ASCII字符的语言.对于其他语言,它将导致文件名只包含下划线. (4认同)

Ste*_*n C 14

如果您希望结果与原始文件类似,则SHA-1或任何其他哈希方案不是答案.如果必须避免碰撞,那么简单地替换或删除"坏"字符也不是答案.

相反,你想要这样的东西.

char fileSep = '/'; // ... or do this portably.
char escape = '%'; // ... or some other legal char.
String s = ...
int len = s.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
    char ch = s.charAt(i);
    if (ch < ' ' || ch >= 0x7F || ch == fileSep || ... // add other illegal chars
        || (ch == '.' && i == 0) // we don't want to collide with "." or ".."!
        || ch == escape) {
        sb.append(escape);
        if (ch < 0x10) {
            sb.append('0');
        }
        sb.append(Integer.toHexString(ch));
    } else {
        sb.append(ch);
    }
}
File currentFile = new File(System.getProperty("user.home"), sb.toString());
PrintWriter currentWriter = new PrintWriter(currentFile);
Run Code Online (Sandbox Code Playgroud)

该解决方案提供可逆编码(没有冲突),其中编码的字符串在大多数情况下类似于原始字符串.我假设您使用的是8位字符.

URLEncoder 有效,但它的缺点是它编码了大量合法的文件名字符.

如果您想要一个不保证可逆的解决方案,那么只需删除"坏"字符,而不是用转义序列替换它们.


Sha*_*ley 14

对于那些寻求通用解决方案的人来说,这些可能是常见的标准:

  • 文件名应该类似于字符串.
  • 在可能的情况下,编码应该是可逆的.
  • 应尽量减少碰撞的可能性.

为此,我们可以使用正则表达式匹配非法字符,它们进行百分比编码,然后约束编码字符串的长度.

private static final Pattern PATTERN = Pattern.compile("[^A-Za-z0-9_\\-]");

private static final int MAX_LENGTH = 127;

public static String escapeStringAsFilename(String in){

    StringBuffer sb = new StringBuffer();

    // Apply the regex.
    Matcher m = PATTERN.matcher(in);

    while (m.find()) {

        // Convert matched character to percent-encoded.
        String replacement = "%"+Integer.toHexString(m.group().charAt(0)).toUpperCase();

        m.appendReplacement(sb,replacement);
    }
    m.appendTail(sb);

    String encoded = sb.toString();

    // Truncate the string.
    int end = Math.min(encoded.length(),MAX_LENGTH);
    return encoded.substring(0,end);
}
Run Code Online (Sandbox Code Playgroud)

模式

上面的模式基于POSIX规范中允许字符保守子集.

如果要允许点字符,请使用:

private static final Pattern PATTERN = Pattern.compile("[^A-Za-z0-9_\\-\\.]");
Run Code Online (Sandbox Code Playgroud)

只要警惕像"."这样的字符串.和"......"

如果要避免在不区分大小写的文件系统上发生冲突,则需要转义大写:

private static final Pattern PATTERN = Pattern.compile("[^a-z0-9_\\-]");
Run Code Online (Sandbox Code Playgroud)

或者逃避小写字母:

private static final Pattern PATTERN = Pattern.compile("[^A-Z0-9_\\-]");
Run Code Online (Sandbox Code Playgroud)

您可以选择将特定文件系统的保留字符列入黑名单,而不是使用白名单.EG此正则表达式适合FAT32文件系统:

private static final Pattern PATTERN = Pattern.compile("[%\\.\"\\*/:<>\\?\\\\\\|\\+,\\.;=\\[\\]]");
Run Code Online (Sandbox Code Playgroud)

长度

在Android上,127个字符是安全限制.许多文件系统允许255个字符.

如果您更喜欢保留尾部,而不是字符串的头部,请使用:

// Truncate the string.
int start = Math.max(0,encoded.length()-MAX_LENGTH);
return encoded.substring(start,encoded.length());
Run Code Online (Sandbox Code Playgroud)

解码

要将文件名转换回原始字符串,请使用:

URLDecoder.decode(filename, "UTF-8");
Run Code Online (Sandbox Code Playgroud)

限制

由于较长的字符串被截断,因此编码时可能会发生名称冲突,或者解码时可能会出现损坏.