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

问题描述 投票:99回答:9

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

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

如果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);

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

java string file encoding
9个回答
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工作,但它的缺点是它编码了许多合法的文件名字符。

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


93
投票

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

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

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

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

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

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

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


34
投票

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

可逆

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

不可逆

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


¹ You can handle all 3 special cases elegantly by using a prefix such as "myApp-". If you put the file directly into $HOME, you'll have to do that anyway to avoid conflicts with existing files such as ".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!?");
    }
}

17
投票

这是我使用的:

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

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

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

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


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
投票

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

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

4
投票

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

String safeFileName = DigestUtils.sha(filename);

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的自定义收集器来改进解决方案,因此您不必将每个轻量级字符强制转换为重量级字符串。


0
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.