web-dev-qa-db-ja.com

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が(正しく)スローされます。

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

編集:私が望んでいるのは、私のためにこれを行う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がこの目的のために信頼できるかどうかはわかりません。

104
Steve McLeod

結果を元のファイルに似せたい場合は、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は機能しますが、多くの正当なファイル名文字をエンコードするという欠点があります。

可逆性を保証しないソリューションが必要な場合は、「悪い」文字をエスケープシーケンスで置き換えるのではなく、単に削除してください。

13
Stephen C

私の提案は、「ホワイトリスト」アプローチを採用することです。つまり、悪いキャラクターを除外しようとしないでください。代わりに、OKを定義します。ファイル名を拒否するか、フィルタリングすることができます。フィルタリングする場合:

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

これは、ではない数字、文字、またはアンダースコアを何もない文字に置き換えます。または、それらを別の文字(アンダースコアなど)に置き換えることもできます。

問題は、これが共有ディレクトリである場合、ファイル名の衝突が望ましくないことです。ユーザーのストレージ領域がユーザーごとに分離されている場合でも、不正な文字を除外するだけでファイル名が衝突する可能性があります。ユーザーが入力した名前は、ダウンロードしたい場合に役立ちます。

このため、ユーザーが必要なものを入力できるようにし、自分で選択したスキーム(例:userId_fileId)に基づいてファイル名を保存し、ユーザーのファイル名をデータベーステーブルに保存します。そうすれば、ユーザーにそれを表示したり、必要なものを保存したり、セキュリティを危険にさらしたり、他のファイルを消去したりすることはありません。

ファイルをハッシュすることもできます(MD5ハッシュなど)が、ユーザーが入力したファイルをリストすることはできません(とにかく意味のある名前ではありません)。

編集:Java用の固定正規表現

97
cletus

それは、エンコーディングが可逆的であるかどうかに依存します。

可逆

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

不可逆

指定された文字列のハッシュ(SHA-1など)を使用します。最新のハッシュアルゴリズム(not MD5)は衝突なしと見なすことができます。実際、衝突が見つかった場合、暗号化にブレークスルーがあります。


¹"myApp-"などのプレフィックスを使用すると、3つの特殊なケースすべてをエレガントに処理できます。ファイルを$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!?");
    }
}
34
vog

私が使用するものは次のとおりです。

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

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

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

私が遭遇した別の問題は、(ユーザー入力に基づいているため)時々同じ名前を取得することであったことに注意する必要があります。 。また、システムによっては255文字の制限を超える可能性があるため、結果の文字列を切り捨てたり、短くしたりする必要がある場合があります。

19
JonasCz

一般的な解決策を探している人にとって、これらは一般的な基準です。

  • ファイル名は文字列に似ている必要があります。
  • エンコードは、可能な場合は可逆的である必要があります。
  • 衝突の可能性を最小限に抑える必要があります。

これを実現するために、正規表現を使用して不正な文字、 percent-encode を照合し、エンコードされた文字列の長さを制限できます。

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

ホワイトリストを使用する代わりに、特定のファイルシステムの予約文字をブラックリストに登録することもできます。例えば。この正規表現は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");

制限事項

長い文字列は切り捨てられるため、エンコード時に名前が衝突したり、デコード時に破損したりする可能性があります。

14
SharkAlley

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

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

commons-codecで提示されるオプション から毒を選びます。例:

String safeFileName = DigestUtils.sha(filename);
4
hd1

これはおそらく最も効果的な方法ではありませんが、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を使用するカスタムコレクターを作成することで改善できるため、各軽量文字を重い文字列にキャストする必要はありません。

2
voho

無効な文字(「/」、「\」、「?」、「*」)を削除してから使用できます。

0
Burkhard