[java] 파일 이름으로 사용하기 위해 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)에 따라 파일 이름을 저장 한 다음 사용자의 파일 이름을 데이터베이스 테이블에 저장하는 경향이 있습니다. 이렇게하면 사용자에게 다시 표시하고 원하는 방식으로 저장할 수 있으며 보안을 손상 시키거나 다른 파일을 지우지 않아도됩니다.

파일을 해시 할 수도 있지만 (예 : MD5 해시) 사용자가 넣은 파일을 나열 할 수 없습니다 (어쨌든 의미있는 이름이 아님).

편집 : 자바에 대한 고정 정규식


답변

인코딩을 되돌릴 수 있는지 여부에 따라 다릅니다.

거꾸로 할 수 있는

URL 인코딩 ( java.net.URLEncoder)을 사용하여 특수 문자를 %xx. 문자열이 같 거나 같 거나 비어 있는 특수한 경우를 주의하십시오 ! ¹ 많은 프로그램이 URL 인코딩을 사용하여 파일 이름을 생성하므로 이는 모두가 이해할 수있는 표준 기술입니다....

뒤집을 수 없는

주어진 문자열의 해시 (예 : SHA-1)를 사용합니다. MD5가 아닌 최신 해시 알고리즘 은 충돌이없는 것으로 간주 될 수 있습니다. 실제로 충돌을 발견하면 암호화에 돌파구를 갖게됩니다.



¹와 같은 접두사를 사용하여 세 가지 특수 사례를 모두 우아하게 처리 할 수 ​​있습니다 "myApp-". 파일을에 직접 넣는 경우 $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!?");
    }
}


답변

내가 사용하는 것은 다음과 같습니다.

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

이것이하는 일은 정규식을 사용하여 문자, 숫자, 밑줄 또는 점이 아닌 모든 문자를 밑줄로 바꾸는 것입니다.

즉, “£를 $로 변환하는 방법”과 같은 항목이 “How_to_convert___to__”가됩니다. 물론이 결과는 사용자 친화적 인 것은 아니지만 안전하며 결과 디렉토리 / 파일 이름은 모든 곳에서 작동합니다. 제 경우에는 결과가 사용자에게 표시되지 않으므로 문제가되지 않지만 정규식을 더 관대하게 변경할 수 있습니다.

내가 만난 또 다른 문제는 (사용자 입력을 기반으로하기 때문에) 때때로 동일한 이름을 얻을 수 있다는 점에 주목해야합니다. 따라서 단일 디렉토리에 동일한 이름을 가진 여러 디렉토리 / 파일을 가질 수 없기 때문에이를 알고 있어야합니다. . 나는 단지 현재 시간과 날짜, 그리고 그것을 피하기 위해 짧은 임의의 문자열을 앞에 추가했습니다. (동일한 파일 이름이 동일한 해시를 생성하므로 파일 이름의 해시가 아닌 실제 임의의 문자열)

또한 일부 시스템의 255 자 제한을 초과 할 수 있으므로 결과 문자열을 자르거나 줄여야 할 수 있습니다.


답변

일반적인 솔루션을 찾는 사람들에게는 다음과 같은 일반적인 기준이있을 수 있습니다.

  • 파일 이름은 문자열과 유사해야합니다.
  • 인코딩은 가능한 경우 되돌릴 수 있어야합니다.
  • 충돌 가능성을 최소화해야합니다.

이를 달성하기 위해 정규식을 사용하여 잘못된 문자를 일치 시키고 퍼센트 인코딩 한 다음 인코딩 된 문자열의 길이를 제한 수 있습니다.

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_\\-\\.]");

“.”와 같은 문자열에주의하십시오. 그리고 “..”

대소 문자를 구분하지 않는 파일 시스템에서 충돌을 피하려면 대문자를 이스케이프해야합니다.

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

또는 소문자 이스케이프 :

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

화이트리스트를 사용하는 대신 특정 파일 시스템에 대해 예약 된 문자를 블랙리스트로 지정할 수 있습니다. EG이 정규식은 FAT32 파일 시스템에 적합합니다.

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

길이

Android에서는 127자가 안전 한도입니다. 많은 파일 시스템에서 255자를 허용합니다.

문자열의 머리보다 꼬리를 유지하려면 다음을 사용하십시오.

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

디코딩

파일 이름을 원래 문자열로 다시 변환하려면 다음을 사용하십시오.

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

한계

긴 문자열은 잘 리기 때문에 인코딩시 이름 충돌이 발생하거나 디코딩시 손상 될 수 있습니다.


답변

모든 유효하지 않은 파일 이름 문자를 공백으로 바꾸는 다음 정규식을 사용해보십시오.

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


답변

commons-codec 에서 제공 하는 옵션 에서 독을 선택하십시오 . 예 :

String safeFileName = DigestUtils.sha1(filename);