파일 이름으로 사용하기 위해 Java에서 문자열을 안전하게 인코딩하는 방법은 무엇입니까?
외부 프로세스에서 문자열을 받고 있습니다. 해당 문자열을 사용하여 파일 이름을 만든 다음 해당 파일에 쓰고 싶습니다. 이를 수행하는 코드 스 니펫은 다음과 같습니다.
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이 (올바르게) throw됩니다.
파일 이름으로 사용할 수 있도록 문자열을 안전하게 인코딩하려면 어떻게해야합니까?
편집 : 내가 바라는 것은 나를 위해 이것을 수행하는 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 또는 다른 해싱 체계가 답이 아닙니다. 충돌을 피해야하는 경우 "불량"문자를 간단히 교체하거나 제거하는 것도 답이 아닙니다.
대신 이와 같은 것을 원합니다.
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
작동하지만 합법적 인 파일 이름 문자를 많이 인코딩한다는 단점이 있습니다.
되돌릴 수없는 보장되지 않는 솔루션을 원한다면 '나쁜'문자를 이스케이프 시퀀스로 바꾸지 말고 제거하면됩니다.
내 제안은 "화이트리스트"접근 방식을 취하는 것입니다. 즉, 잘못된 문자를 걸러 내려고하지 마십시오. 대신 무엇이 괜찮은지 정의하십시오. 파일 이름을 거부하거나 필터링 할 수 있습니다. 필터링하려는 경우 :
String name = s.replaceAll("\\W+", "");
이것이하는 일은 숫자, 문자 또는 밑줄 이 아닌 모든 문자를 아무것도 바꾸지 않는 것입니다. 또는 다른 문자 (예 : 밑줄)로 바꿀 수 있습니다.
문제는 이것이 공유 디렉토리라면 파일 이름 충돌을 원하지 않는다는 것입니다. 사용자 저장 영역이 사용자별로 분리되어 있어도 잘못된 문자를 필터링하여 충돌하는 파일 이름으로 끝날 수 있습니다. 사용자가 입력 한 이름은 다운로드를 원할 때 유용합니다.
이런 이유로 사용자가 원하는 것을 입력하고 내가 선택한 스키마 (예 : userId_fileId)에 따라 파일 이름을 저장 한 다음 사용자의 파일 이름을 데이터베이스 테이블에 저장하는 경향이 있습니다. 이렇게하면 사용자에게 다시 표시하고 원하는 방식으로 저장할 수 있으며 보안을 손상 시키거나 다른 파일을 지우지 않아도됩니다.
You can also hash the file (eg MD5 hash) but then you can't list the files the user put in (not with a meaningful name anyway).
EDIT:Fixed regex for java
It depends on whether the encoding should be reversible or not.
Reversible
Use URL encoding (java.net.URLEncoder
) to replace special characters with %xx
. Note that you take care of the special cases where the string equals .
, equals ..
or is empty!¹ Many programs use URL encoding to create file names, so this is a standard technique which everybody understands.
Irreversible
Use a hash (e.g. SHA-1) of the given string. Modern hash algorithms (not MD5) can be considered collision-free. In fact, you'll have a break-through in cryptography if you find a collision.
¹ 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!?");
}
}
Here's what I use:
public String sanitizeFilename(String inputName) {
return inputName.replaceAll("[^a-zA-Z0-9-_\\.]", "_");
}
What this does is is replace every character which is not a letter, number, underscore or dot with an underscore, using regex.
This means that something like "How to convert £ to $" will become "How_to_convert___to__". Admittedly, this result is not very user-friendly, but it is safe and the resulting directory /file names are guaranteed to work everywhere. In my case, the result is not shown to the user, and is thus not a problem, but you may want to alter the regex to be more permissive.
Worth noting that another problem I encountered was that I would sometimes get identical names (since it's based on user input), so you should be aware of that, since you can't have multiple directories / files with the same name in a single directory. Also, you may need to truncate or otherwise shorten the resulting string, since it may exceed the 255 character limit some systems have.
For those looking for a general solution, these might be common critera:
- The filename should resemble the string.
- The encoding should be reversible where possible.
- The probability of collisions should be minimized.
To achieve this we can use regex to match illegal characters, percent-encode them, then constrain the length of the encoded 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);
}
Patterns
The pattern above is based on a conservative subset of allowed characters in the POSIX spec.
If you want to allow the dot character, use:
private static final Pattern PATTERN = Pattern.compile("[^A-Za-z0-9_\\-\\.]");
Just be wary of strings like "." and ".."
If you want to avoid collisions on case insensitive filesystems, you'll need to escape capitals:
private static final Pattern PATTERN = Pattern.compile("[^a-z0-9_\\-]");
Or escape lower case letters:
private static final Pattern PATTERN = Pattern.compile("[^A-Z0-9_\\-]");
Rather than using a whitelist, you may choose to blacklist reserved characters for your specific filesystem. E.G. This regex suits FAT32 filesystems:
private static final Pattern PATTERN = Pattern.compile("[%\\.\"\\*/:<>\\?\\\\\\|\\+,\\.;=\\[\\]]");
Length
On Android, 127 characters is the safe limit. Many filesystems allow 255 characters.
If you prefer to retain the tail, rather than the head of your string, use:
// Truncate the string.
int start = Math.max(0,encoded.length()-MAX_LENGTH);
return encoded.substring(start,encoded.length());
Decoding
To convert the filename back to the original string, use:
URLDecoder.decode(filename, "UTF-8");
Limitations
Because longer strings are truncated, there is the possibility of a name collision when encoding, or corruption when decoding.
Try using the following regex which replaces every invalid file name character with a space:
public static String toValidFileName(String input)
{
return input.replaceAll("[:\\\\/*\"?|<>']", " ");
}
Pick your poison from the options presented by commons-codec, example:
String safeFileName = DigestUtils.sha(filename);
This is probably not the most effective way, but shows how to do it using Java 8 pipelines:
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());
}
The solution could be improved by creating custom collector which uses StringBuilder, so you do not have to cast each light-weight character to a heavy-weight string.
You could remove the invalid chars ( '/', '\', '?', '*') and then use it.
'Programing' 카테고리의 다른 글
버전 제어에 Xcode 작업 공간 체계 추가 (0) | 2020.08.12 |
---|---|
제거 / 무시하는 방법 : 터치 장치에서 CSS 스타일을 호버 (0) | 2020.08.12 |
자녀 수를 어떻게 셀 수 있습니까? (0) | 2020.08.12 |
Firebase FCM은 onTokenRefresh ()를 강제로 호출합니다. (0) | 2020.08.12 |
게터와 세터는 어떻게 작동합니까? (0) | 2020.08.12 |