我从外部进程收到一个字符串。我想使用该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是否可靠用于此目的。
答案 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.