ファイル名として使用するために、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が(正しく)スローされます。

ファイル名として使用できるように文字列を安全にエンコードするにはどうすればよいですか?

編集:私が期待しているのは、これを行う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
文字列をエンコードする目的は何ですか?
スティーブンC

3
@Stephen C:文字列をエンコードする目的は、java.net.URLEncoderがURLに対して行うように、ファイル名としての使用に適するようにすることです。
スティーブマクロード

1
ああなるほど。エンコーディングは可逆的である必要がありますか?
スティーブンC

@Stephen C:いいえ、リバーシブルである必要はありませんが、結果を元の文字列にできるだけ類似させたいです。
スティーブマクロード、

1
エンコーディングは元の名前を不明瞭にする必要がありますか?1対1である必要がありますか。つまり、衝突は大丈夫ですか?
スティーブン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ハッシュなど)が、ユーザーが入力したファイルをリストすることはできません(意味のある名前ではありません)。

編集:Javaの修正された正規表現


最初に悪い解決策を提供するのは良い考えではないと思います。さらに、MD5はほぼ解読されたハッシュアルゴリズムです。少なくともSHA-1以上をお勧めします。
VOG

19
アルゴリズムが「壊れている」かどうかを気にする一意のファイル名を作成するために?
cletus 2009

3
@cletus:問題は、異なる文字列が同じファイル名にマッピングされることです。すなわち衝突。
スティーブンC

3
衝突は慎重に行う必要があります。元の質問では、攻撃者がこれらの文字列を選択することについては触れていません。
tialaramex 2009

8
"\\W+"Javaの正規表現に使用する必要があります。バックスラッシュは最初に文字列自体に適用され、\W有効なエスケープシーケンスではありません。回答を編集しようとしましたが、誰かが私の編集を拒否したようです:(
vadipp

35

それはエンコーディングがリバーシブルであるべきかどうかに依存します。

可逆

URLエンコード(java.net.URLEncoder)を使用して、特殊文字をに置き換え%xxます。文字列が等しい、等しい、または空であるという特殊なケースに注意してください!¹多くのプログラムはURLエンコーディングを使用してファイル名を作成するため、これは誰もが理解できる標準的な手法です。...

不可逆

指定された文字列のハッシュ(SHA-1など)を使用します。最新のハッシュアルゴリズム(MD5ではない)は、衝突がないと見なすことができます。実際、衝突が見つかった場合は、暗号技術に突破口が開かれます。


3などの接頭辞を使用すると、3つの特殊なケースすべてをエレガントに処理でき"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の考えは正しくない場合があります。
スティーブンC

4
@vog:「。」のURLEncoderが失敗する そして「..」。これらはエンコードする必要があります。そうしないと、$ HOMEのディレクトリエントリと衝突します
Stephen C

6
@vog: "*"はほとんどのUnixベースのファイルシステムでのみ許可され、NTFSおよびFAT32ではサポートされていません。
ジョナサン

1
「」文字列がドットのみの場合、「..」は%2Eにドットをエスケープすることで処理できます(エスケープシーケンスを最小化する場合)。'*'は "%2A"で置き換えることもできます。
1

1
ファイル名を長くする(単一文字を%20などに変更する)と、長さの制限(UNIXシステムでは255文字)に近い一部のファイル名が無効になることに注意してください
smcg

24

これが私が使うものです:

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

これは、正規表現を使用して、文字、数字、アンダースコア、ドット以外のすべての文字をアンダースコアに置き換えることです。

つまり、「£を$に変換する方法」などは「How_to_convert___to__」になります。確かに、この結果はあまりユーザーフレンドリーではありませんが、安全であり、結果として得られるディレクトリ/ファイル名はどこでも機能することが保証されています。私の場合、結果はユーザーには表示されず、したがって問題にはなりませんが、正規表現をより許容度の高いものに変更することをお勧めします。

私が遭遇した別の問題は、(ユーザー入力に基づいているため)時々同一の名前を取得することでした。そのため、1つのディレクトリに同じ名前の複数のディレクトリ/ファイルを置くことはできないため、注意が必要です。 。私は現在の時刻と日付を追加し、それを回避するために短いランダム文字列を追加しました。(実際のランダムな文字列、ファイル名のハッシュではありません。同じファイル名は同じハッシュになるためです)

また、一部のシステムの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

私は、パーセントエンコーディングは、それが予約文字だということを考えると、窓に親切に働くだろうとは思いません...
Amalgovinus

1
英語以外の言語は考慮しません。
NateS 2017年

5

すべての無効なファイル名文字をスペースで置き換える次の正規表現を使用してみてください。

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

スペースはCLIにとって厄介です。_またはで置き換えることを検討してください-
sdgfsdh 2017年


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が提案するようにハッシュを使用します。
Burkhard

4
名前の競合の可能性に常にオープンです
ブライアンアグニュー

2
「?」および「*」は、ファイル名に使用できる文字です。通常はグロビングが使用されるため、シェルコマンドでエスケープする必要があるだけです。ただし、ファイルAPIレベルでは問題ありません。
ヴォグ

2
@Brian Agnew:実際にはそうではありません。リバーシブルエスケープスキームを使用して無効な文字をエンコードするスキームでは、衝突は発生しません。
スティーブンC
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.