Programing

파일 이름으로 사용하기 위해 Java에서 문자열을 안전하게 인코딩하는 방법은 무엇입니까?

crosscheck 2020. 8. 12. 07:41
반응형

파일 이름으로 사용하기 위해 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.

참고URL : https://stackoverflow.com/questions/1184176/how-can-i-safely-encode-a-string-in-java-to-use-as-a-filename

반응형