我如何安全地编码Java中的string作为文件名?

我从外部进程收到一个string。 我想使用该string来创build一个文件名,然后写入该文件。 这是我的代码片段来做到这一点:

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是否可靠。

如果你想要的结果类似于原始文件,SHA-1或任何其他散列scheme不是答案。 相反,你想要这样的东西。

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

这个解决scheme给出了一个可逆的编码(没有冲突),在大多数情况下编码的string与原始string相似。 我假设你正在使用8位字符。

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

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

我的build议是采取“白名单”的方法,这意味着不要试图过滤出不好的字符。 相反,定义什么是好的。 您可以拒绝文件名或过滤它。 如果你想过滤它:

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

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

问题是,如果这是一个共享目录,那么你不希望文件名冲突。 即使用户存储区域被用户隔离,也可能只是通过筛选出不好的字符而导致冲突的文件名。 用户input的名字通常也是有用的,如果他们也想下载它的话。

出于这个原因,我倾向于允许用户input他们想要的,根据我自己select的scheme(例如userId_fileId)存储文件名,然后将用户的文件名存储在数据库表中。 这样,您可以将其显示回给用户,存储您想要的东西,而且不会危害安全性或清除其他文件。

您也可以散列文件(例如MD5散列),但是不能列出用户放入的文件(无论如何也不要使用有意义的名称)。

编辑:修正了Java的正则expression式

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

可逆

使用URL编码( java.net.URLEncoder )将特殊字符replace为%xx 。 请注意,您要注意string相等的特殊情况 . ,等于或为空!¹许多程序使用URL编码来创build文件名,所以这是每个人都能理解的标准技术。

不可逆

使用给定string的散列(例如SHA-1)。 现代哈希algorithm( 不是 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!?"); } } 

对于那些寻求一般解决scheme的人来说,这些可能是常见的标准:

  • 文件名应该类似于string。
  • 编码在可能的情况下应该是可逆的。
  • 碰撞的可能性应该最小化。

为了实现这一点,我们可以使用正则expression式来匹配非法字符, 对它们进行百分比编码 ,然后限制编码string的长度。

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

模式

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

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

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

只是要警惕像“”的string。 和“..”

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

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

或者转义小写字母:

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

您可以select将特定文件系统的保留字符黑名单,而不是使用白名单。 EG这个正则expression式适合FAT32文件系统:

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

长度

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

如果你喜欢保留尾巴,而不是你的string的头部,使用:

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

解码

要将文件名转换回原始string,请使用:

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

限制

由于较长的string被截断,编码时可能会发生名称冲突,或者在解码时出现错误。

以下是我使用的:

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

这样做是使用正则expression式replace每个不是字母,数字,下划线或点的字符。

这意味着像“如何将英镑转换为$”这样的东西将变成“How_to_convert__to__”。 不可否认的是,这个结果不是非常用户友好的,但它是安全的,所产生的目录/文件名保证可以在任何地方工作。 在我的情况下,结果不显示给用户,因此不是一个问题,但你可能想要改变正则expression式更宽容。

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

尝试使用下面的正则expression式,用空格replace每个无效的文件名字符:

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

从commons-codec提供的选项中select你的毒药,例如:

 String safeFileName = DigestUtils.sha(filename); 

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

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

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

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

只需使用:

IOHelper.toFileSystemSafeName (“Iblabla / blabla”);

将变成“Iblablablabla”