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

时间:2009-07-26 09:54:22

标签: java string file encoding

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

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

如果s包含无效字符,例如基于Unix的操作系统中的'/',则会(正确地)抛出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);

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

10 个答案:

答案 0 :(得分:94)

我的建议是采用“白名单”方法,这意味着不要尝试过滤掉不良字符。而是定义什么是好的。您可以拒绝文件名或过滤它。如果你想过滤它:

String name = s.replaceAll("\\W+", "");

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

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

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

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

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

答案 1 :(得分:34)

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

双向

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

不可逆

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

<小时/> <子> ¹您可以使用"myApp-"等前缀优雅地处理所有3个特殊情况。如果您将文件直接放入$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!?");
    }
}

答案 2 :(得分:18)

以下是我使用的内容:

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

这样做是使用正则表达式替换每个不是字母,数字,下划线或带下划线的点的字符。

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

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

答案 3 :(得分:14)

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

  • 文件名应该类似于字符串。
  • 编码应尽可能可逆。
  • 应尽量减少碰撞的可能性。

为实现这一目标,我们可以使用正则表达式匹配非法字符percent-encode,然后约束编码字符串的长度。

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);
}

<强>模式

上述模式基于conservative subset of allowed characters in the POSIX spec

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

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

要警惕像“。”这样的字符串。和“..”

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

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

或逃避小写字母:

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

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

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

<强>长度

On Android, 127 characters是安全限制。 Many filesystems allow 255 characters.

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

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

<强>解码

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

URLDecoder.decode(filename, "UTF-8");

<强>限制

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

答案 4 :(得分:13)

如果您希望结果与原始文件类似,则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);

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

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

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

答案 5 :(得分:4)

options presented by commons-codec中选择你的毒药,例如:

String safeFileName = DigestUtils.sha(filename);

答案 6 :(得分:4)

尝试使用以下正则表达式,用空格替换每个无效的文件名字符:

public static String toValidFileName(String input)
{
    return input.replaceAll("[:\\\\/*\"?|<>']", " ");
}

答案 7 :(得分:2)

这可能不是最有效的方法,但展示了如何使用Java 8管道进行操作:

private static String sanitizeFileName(String name) {
    return name
            .chars()
            .mapToObj(i -> (char) i)
            .map(c -> Character.isWhitespace(c) ? '_' : c)
            .filter(c -> Character.isLetterOrDigit(c) || c == '-' || c == '_')
            .map(String::valueOf)
            .collect(Collectors.joining());
}

可以通过创建使用StringBuilder的自定义收集器来改进解决方案,因此您不必将每个轻量级字符强制转换为重量级字符串。

答案 8 :(得分:0)

您可以删除无效字符('/','\','?','*'),然后使用它。

答案 9 :(得分:0)

o如果您不关心可逆性,但希望在大多数情况下拥有与 cross platform 兼容的好名字,这是我的方法。

//: and ? into .
name = name.replaceAll("[\\?:]", ".");

//" into '
name = name.replaceAll("[\"]", "'");

//\, / and | into ,
name = name.replaceAll("[\\\\/|]", ",");

//<, > and * int _
name = name.replaceAll("[<>*]", "_");
return name;

这变成了:

This is a **Special** "Test": A\B/C is <BETTER> than D|E|F! Or?

进入:

This is a __Special__ 'Test'. A,B,C is _BETTER_ than D,E,F! Or.