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


117

외부 프로세스에서 문자열을 받고 있습니다. 해당 문자열을 사용하여 파일 이름을 만든 다음 해당 파일에 쓰고 싶습니다. 이를 수행하는 코드 스 니펫은 다음과 같습니다.

    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 가이 목적에 대해 신뢰할 수 있는지 확실하지 않습니다.


1
문자열을 인코딩하는 목적은 무엇입니까?
Stephen C

3
@Stephen C : 문자열을 인코딩하는 목적은 java.net.URLEncoder가 URL에 사용하는 것처럼 파일 이름으로 사용하기에 적합하도록 만드는 것입니다.
Steve McLeod

1
아, 알겠습니다. 인코딩을 되돌릴 수 있어야합니까?
Stephen C

@Stephen C : 아니요, 되돌릴 필요는 없지만 결과가 가능한 한 원래 문자열과 비슷하도록하고 싶습니다.
Steve McLeod

1
인코딩이 원래 이름을 가려야합니까? 일대일이어야합니까? 즉, 충돌은 괜찮습니까?
Stephen C

답변:


17

결과가 원본 파일과 유사하도록하려면 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 작동하지만 합법적 인 파일 이름 문자를 많이 인코딩한다는 단점이 있습니다.

되돌릴 수없는 보장되지 않는 솔루션을 원한다면 '나쁜'문자를 이스케이프 시퀀스로 바꾸지 말고 제거하면됩니다.


위의 인코딩의 반대는 구현하기 똑같이 간단해야합니다.


105

내 제안은 "화이트리스트"접근 방식을 취하는 것입니다. 즉, 잘못된 문자를 걸러 내려고하지 마십시오. 대신 무엇이 괜찮은지 정의하십시오. 파일 이름을 거부하거나 필터링 할 수 있습니다. 필터링하려는 경우 :

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

이것이하는 일은 숫자, 문자 또는 밑줄 이 아닌 모든 문자를 아무것도 바꾸지 않는 것입니다. 또는 다른 문자 (예 : 밑줄)로 바꿀 수 있습니다.

문제는 이것이 공유 디렉토리라면 파일 이름 충돌을 원하지 않는다는 것입니다. 사용자 저장 영역이 사용자별로 분리되어 있어도 잘못된 문자를 필터링하여 충돌하는 파일 이름으로 끝날 수 있습니다. 사용자가 입력 한 이름은 다운로드를 원할 때 유용합니다.

이런 이유로 사용자가 원하는 것을 입력하고 내가 선택한 스키마 (예 : userId_fileId)에 따라 파일 이름을 저장 한 다음 사용자의 파일 이름을 데이터베이스 테이블에 저장하는 경향이 있습니다. 이렇게하면 사용자에게 다시 표시하고 원하는 방식으로 저장할 수 있으며 보안을 손상 시키거나 다른 파일을 지우지 않아도됩니다.

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

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


나쁜 해결책을 먼저 제공하는 것은 좋은 생각이 아니라고 생각합니다. 또한 MD5는 거의 크랙 된 해시 알고리즘입니다. 적어도 SHA-1 이상을 권장합니다.
vog 2010-07-26

19
고유 한 파일 이름을 만들 목적으로 알고리즘이 "깨 졌는지"신경 쓰나요?
cletus

3
@cletus : 문제는 다른 문자열이 동일한 파일 이름에 매핑된다는 것입니다. 즉 충돌.
Stephen C

3
충돌은 의도적이어야합니다. 원래의 질문은 공격자가이 문자열을 선택하는 것에 대해 이야기하지 않습니다.
tialaramex

8
"\\W+"Java에서 정규 표현식 을 사용해야 합니다. 백 슬래시는 먼저 문자열 자체에 적용되며 \W유효한 이스케이프 시퀀스가 ​​아닙니다. 나는 대답을 편집하려고했으나 사람 같은 외모는 내 편집 :( 거부
vadipp

35

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

거꾸로 할 수 있는

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!?");
    }
}


2
특수 문자가 무엇인지에 대한 URLEncoder의 아이디어가 올바르지 않을 수 있습니다.
Stephen C

4
@vog : "."에 대한 URLEncoder 실패 그리고 "..". 인코딩되지 않으면 $ HOME의 디렉토리 항목과 충돌합니다
Stephen C

6
@vog : "*"는 대부분의 Unix 기반 파일 시스템에서만 허용되며 NTFS 및 FAT32는이를 지원하지 않습니다.
Jonathan

1
"." 그리고 ".."은 문자열이 단지 점일 때 점을 % 2E로 이스케이프하여 처리 할 수 ​​있습니다 (이스케이프 시퀀스를 최소화하려는 경우). '*'는 "% 2A"로 대체 될 수도 있습니다.
viphe

1
파일 이름을 늘리는 방법 (단일 문자를 % 20 등으로 변경)은 길이 제한 (Unix 시스템의 경우 255 자)에 가까운 일부 파일 이름을 무효화합니다
smcg

24

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

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

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

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

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

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


6
또 다른 문제는 ASCII 문자를 사용하는 언어에만 해당된다는 것입니다. 다른 언어의 경우 밑줄로만 구성된 파일 이름이 생성됩니다.
앤디 토마스

13

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

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

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

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

한계

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


1
POSIX는 하이픈을 할 수 있습니다 - 당신이 패턴에 추가한다 -Pattern.compile("[^A-Za-z0-9_\\-]")
위해 mkdev

하이픈이 추가되었습니다. 감사합니다 :)
SharkAlley 2015-06-15

퍼센트 인코딩이 예약 된 문자라는 점을 감안할 때 Windows에서 친절하게 작동하지 않을 것이라고 생각합니다.
Amalgovinus 2017-10-06

1
영어가 아닌 언어는 고려하지 않습니다.
NateS

5

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

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

CLI에서는 공백이 끔찍합니다. _또는로 바꾸는 것을 고려하십시오 -.
sdgfsdh


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를 사용하는 사용자 지정 수집기를 만들어 개선 할 수 있으므로 각 경량 문자를 무거운 문자열로 캐스팅 할 필요가 없습니다.


-1

유효하지 않은 문자 ( '/', '\', '?', '*')를 제거한 다음 사용할 수 있습니다.


1
이것은 이름 충돌 가능성을 소개합니다. 즉, "tes? t", "tes * t"및 "test"는 동일한 파일 "test"로 이동합니다.
vog 2009-07-26

진실. 그런 다음 교체하십시오. 예를 들어 '/'-> 슬래시, '*'-> 별표 ... 또는 vog가 제안한대로 해시를 사용하십시오.
Burkhard

4
항상 이름 충돌의 가능성에 열려
브라이언 애그뉴

2
"?" 및 "*"는 파일 이름에 허용되는 문자입니다. 일반적으로 globbing이 사용되기 때문에 셸 명령에서만 이스케이프하면됩니다. 그러나 파일 API 수준에서는 문제가 없습니다.
vog 2009-07-26

2
@Brian Agnew : 사실이 아닙니다. 가역적 이스케이프 체계를 사용하여 유효하지 않은 문자를 인코딩하는 체계는 충돌을 일으키지 않습니다.
Stephen C
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.