Wie kann ich eine Zeichenfolge in Java sicher codieren, um sie als Dateinamen zu verwenden?

116

Ich erhalte eine Zeichenfolge von einem externen Prozess. Ich möchte diesen String verwenden, um einen Dateinamen zu erstellen und dann in diese Datei zu schreiben. Hier ist mein Code-Snippet, um dies zu tun:

    String s = ... // comes from external source
    File currentFile = new File(System.getProperty("user.home"), s);
    PrintWriter currentWriter = new PrintWriter(currentFile);

Wenn s ein ungültiges Zeichen enthält, z. B. '/' in einem Unix-basierten Betriebssystem, wird (zu Recht) eine java.io.FileNotFoundException ausgelöst.

Wie kann ich den String sicher codieren, damit er als Dateiname verwendet werden kann?

Bearbeiten: Was ich mir erhoffe, ist ein API-Aufruf, der dies für mich erledigt.

Ich kann dies tun:

    String s = ... // comes from external source
    File currentFile = new File(System.getProperty("user.home"), URLEncoder.encode(s, "UTF-8"));
    PrintWriter currentWriter = new PrintWriter(currentFile);

Ich bin mir aber nicht sicher, ob URLEncoder für diesen Zweck zuverlässig ist.

Steve McLeod
quelle
1
Was ist der Zweck der Codierung der Zeichenfolge?
Stephen C
3
@Stephen C: Der Zweck der Codierung der Zeichenfolge besteht darin, sie als Dateinamen zu verwenden, wie dies bei java.net.URLEncoder für URLs der Fall ist.
Steve McLeod
1
Oh ich verstehe. Muss die Codierung reversibel sein?
Stephen C
@ Stephen C: Nein, es muss nicht reversibel sein, aber ich möchte, dass das Ergebnis der ursprünglichen Zeichenfolge so nahe wie möglich kommt.
Steve McLeod
1
Muss die Codierung den ursprünglichen Namen verdecken? Muss es 1 zu 1 sein? dh sind Kollisionen in Ordnung?
Stephen C

Antworten:

17

Wenn Sie möchten, dass das Ergebnis der Originaldatei ähnelt, ist SHA-1 oder ein anderes Hashing-Schema nicht die Antwort. Wenn Kollisionen vermieden werden müssen, ist das einfache Ersetzen oder Entfernen von "schlechten" Zeichen ebenfalls nicht die Antwort.

Stattdessen willst du so etwas. (Hinweis: Dies sollte als veranschaulichendes Beispiel betrachtet werden und nicht zum Kopieren und Einfügen.)

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

Diese Lösung bietet eine reversible Codierung (ohne Kollisionen), bei der die codierten Zeichenfolgen in den meisten Fällen den ursprünglichen Zeichenfolgen ähneln. Ich gehe davon aus, dass Sie 8-Bit-Zeichen verwenden.

URLEncoder funktioniert, hat aber den Nachteil, dass es eine ganze Reihe von legalen Dateinamenzeichen codiert.

Wenn Sie eine nicht garantierte, reversible Lösung wünschen, entfernen Sie einfach die "schlechten" Zeichen, anstatt sie durch Escape-Sequenzen zu ersetzen.


Die Umkehrung der obigen Codierung sollte ebenso einfach zu implementieren sein.

Stephen C.
quelle
105

Mein Vorschlag ist, eine "weiße Liste" zu wählen, dh nicht zu versuchen, schlechte Zeichen herauszufiltern. Definieren Sie stattdessen, was in Ordnung ist. Sie können den Dateinamen entweder ablehnen oder filtern. Wenn Sie es filtern möchten:

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

Dies ersetzt jedes Zeichen, das keine Zahl, kein Buchstabe oder Unterstrich ist, durch nichts. Alternativ können Sie sie durch ein anderes Zeichen ersetzen (z. B. einen Unterstrich).

Das Problem ist, dass Sie, wenn dies ein freigegebenes Verzeichnis ist, keine Kollision mit Dateinamen möchten. Selbst wenn die Speicherbereiche der Benutzer nach Benutzern getrennt sind, kann dies zu einem kollidierenden Dateinamen führen, indem nur fehlerhafte Zeichen herausgefiltert werden. Der Name, den ein Benutzer eingibt, ist oft nützlich, wenn er ihn jemals herunterladen möchte.

Aus diesem Grund erlaube ich dem Benutzer, das einzugeben, was er möchte, den Dateinamen basierend auf einem Schema meiner Wahl (z. B. userId_fileId) zu speichern und dann den Dateinamen des Benutzers in einer Datenbanktabelle zu speichern. Auf diese Weise können Sie es dem Benutzer wieder anzeigen, die gewünschten Informationen speichern und die Sicherheit nicht beeinträchtigen oder andere Dateien löschen.

Sie können die Datei auch hashen (z. B. MD5-Hash), aber dann können Sie die vom Benutzer eingegebenen Dateien nicht auflisten (ohnehin nicht mit einem aussagekräftigen Namen).

EDIT: Regex für Java behoben

Cletus
quelle
Ich denke nicht, dass es eine gute Idee ist, zuerst die schlechte Lösung bereitzustellen. Darüber hinaus ist MD5 ein nahezu geknackter Hash-Algorithmus. Ich empfehle mindestens SHA-1 oder besser.
Vog
19
Um einen eindeutigen Dateinamen zu erstellen, wen interessiert es, ob der Algorithmus "defekt" ist?
Cletus
3
@cletus: Das Problem ist, dass verschiedene Zeichenfolgen demselben Dateinamen zugeordnet werden. dh Kollision.
Stephen C
3
Eine Kollision müsste absichtlich sein, die ursprüngliche Frage spricht nicht davon, dass diese Zeichenfolgen von einem Angreifer ausgewählt werden.
Tialaramex
8
Sie müssen "\\W+"für den regulären Ausdruck in Java verwenden. Backslash gilt zuerst für die Zeichenfolge selbst und \Wist keine gültige Escape-Sequenz. Ich habe versucht, die Antwort zu bearbeiten, aber es sieht so aus, als hätte jemand meine Bearbeitung abgelehnt :(
vadipp
35

Dies hängt davon ab, ob die Codierung reversibel sein soll oder nicht.

Reversibel

Verwenden Sie die URL-Codierung ( java.net.URLEncoder), um Sonderzeichen durch zu ersetzen %xx. Beachten Sie, dass Sie sich um die Sonderfälle kümmern, in denen die Zeichenfolge gleich ., gleich ..oder leer ist! ¹ Viele Programme verwenden URL-Codierung, um Dateinamen zu erstellen. Dies ist also eine Standardtechnik, die jeder versteht.

Irreversibel

Verwenden Sie einen Hash (z. B. SHA-1) der angegebenen Zeichenfolge. Moderne Hash-Algorithmen ( nicht MD5) können als kollisionsfrei angesehen werden. In der Tat haben Sie einen Durchbruch in der Kryptographie, wenn Sie eine Kollision finden.


¹ Sie können alle 3 Sonderfälle elegant behandeln, indem Sie ein Präfix wie z "myApp-". Wenn Sie die Datei direkt in $HOMEablegen, müssen Sie dies trotzdem tun, um Konflikte mit vorhandenen Dateien wie ".bashrc" zu vermeiden.
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!?");
    }
}

Vog
quelle
2
URLEncoders Vorstellung von einem Sonderzeichen ist möglicherweise nicht korrekt.
Stephen C
4
@vog: URLEncoder schlägt für "." fehl. und "..". Diese müssen verschlüsselt sein, sonst kollidieren Sie mit Verzeichniseinträgen in $ HOME
Stephen C
6
@vog: "*" ist nur in den meisten Unix-basierten Dateisystemen zulässig, NTFS und FAT32 unterstützen dies nicht.
Jonathan
1
"." und ".." können behandelt werden, indem Punkte auf% 2E maskiert werden, wenn der String nur aus Punkten besteht (wenn Sie die Escape-Sequenzen minimieren möchten). '*' kann auch durch "% 2A" ersetzt werden.
Viphe
1
Beachten Sie, dass jeder Ansatz, der den Dateinamen verlängert (indem einzelne Zeichen auf% 20 oder was auch immer
geändert werden
24

Folgendes verwende ich:

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

Dadurch wird jedes Zeichen, das kein Buchstabe, keine Zahl, kein Unterstrich oder kein Punkt ist, durch einen Unterstrich durch Regex ersetzt.

Dies bedeutet, dass etwas wie "Wie man £ in $ umwandelt" zu "How_to_convert___to__" wird. Zugegeben, dieses Ergebnis ist nicht sehr benutzerfreundlich, aber es ist sicher und die resultierenden Verzeichnis- / Dateinamen funktionieren garantiert überall. In meinem Fall wird das Ergebnis dem Benutzer nicht angezeigt und ist daher kein Problem. Möglicherweise möchten Sie jedoch den regulären Ausdruck so ändern, dass er zulässiger ist.

Es ist erwähnenswert, dass ein weiteres Problem darin bestand, dass ich manchmal identische Namen erhielt (da dies auf Benutzereingaben basiert). Sie sollten sich dessen bewusst sein, da Sie nicht mehrere Verzeichnisse / Dateien mit demselben Namen in einem einzigen Verzeichnis haben können . Ich habe nur die aktuelle Uhrzeit und das aktuelle Datum sowie eine kurze zufällige Zeichenfolge vorangestellt, um dies zu vermeiden. (Eine tatsächliche zufällige Zeichenfolge, kein Hash des Dateinamens, da identische Dateinamen zu identischen Hashes führen.)

Außerdem müssen Sie möglicherweise die resultierende Zeichenfolge abschneiden oder auf andere Weise kürzen, da sie möglicherweise die 255-Zeichen-Grenze überschreitet, die einige Systeme haben.

JonasCz - Monica wieder einsetzen
quelle
5
Ein weiteres Problem besteht darin, dass es spezifisch für Sprachen ist, die ASCII-Zeichen verwenden. Für andere Sprachen würde dies zu Dateinamen führen, die nur aus Unterstrichen bestehen.
Andy Thomas
13

Für diejenigen, die nach einer allgemeinen Lösung suchen, können dies häufige Kriterien sein:

  • Der Dateiname sollte der Zeichenfolge ähneln.
  • Die Codierung sollte nach Möglichkeit reversibel sein.
  • Die Wahrscheinlichkeit von Kollisionen sollte minimiert werden.

Um dies zu erreichen, können wir Regex verwenden, um unzulässige Zeichen abzugleichen, sie in Prozent zu codieren und dann die Länge der codierten Zeichenfolge zu beschränken.

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

Muster

Das obige Muster basiert auf einer konservativen Teilmenge zulässiger Zeichen in der POSIX-Spezifikation .

Wenn Sie das Punktzeichen zulassen möchten, verwenden Sie:

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

Seien Sie vorsichtig mit Saiten wie "." und ".."

Wenn Sie Kollisionen in Dateisystemen ohne Berücksichtigung der Groß- und Kleinschreibung vermeiden möchten, müssen Sie Großbuchstaben umgehen:

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

Oder entkommen Sie Kleinbuchstaben:

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

Anstatt eine Whitelist zu verwenden, können Sie reservierte Zeichen für Ihr spezifisches Dateisystem auf die schwarze Liste setzen. EG Dieser reguläre Ausdruck passt zu FAT32-Dateisystemen:

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

Länge

Unter Android sind 127 Zeichen die sichere Grenze. Viele Dateisysteme erlauben 255 Zeichen.

Wenn Sie lieber den Schwanz als den Kopf Ihrer Schnur behalten möchten, verwenden Sie:

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

Dekodierung

Verwenden Sie Folgendes, um den Dateinamen wieder in die ursprüngliche Zeichenfolge zu konvertieren:

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

Einschränkungen

Da längere Zeichenfolgen abgeschnitten werden, besteht die Möglichkeit einer Namenskollision beim Codieren oder einer Beschädigung beim Decodieren.

SharkAlley
quelle
1
Posix erlaubt Bindestriche - Sie sollten es dem Muster hinzufügen -Pattern.compile("[^A-Za-z0-9_\\-]")
mkdev
Bindestriche hinzugefügt. Danke :)
SharkAlley
Ich glaube nicht, dass die prozentuale Codierung unter Windows gut funktioniert, da es sich um ein reserviertes Zeichen handelt.
Amalgovinus
1
Berücksichtigt keine nicht englischen Sprachen.
NateS
5

Verwenden Sie den folgenden regulären Ausdruck, der jedes ungültige Dateinamenzeichen durch ein Leerzeichen ersetzt:

public static String toValidFileName(String input)
{
    return input.replaceAll("[:\\\\/*\"?|<>']", " ");
}
BullyWiiPlaza
quelle
Leerzeichen sind für CLIs unangenehm. Erwägen Sie, durch _oder zu ersetzen -.
SDGFSDH
3

Wählen Sie Ihr Gift aus den Optionen des Commons-Codecs aus . Beispiel:

String safeFileName = DigestUtils.sha1(filename);
hd1
quelle
Richtig, es wurde jetzt behoben
HD1
1
Verwenden Sie sha1; shaist veraltet.
Daniel
2

Dies ist wahrscheinlich nicht der effektivste Weg, zeigt jedoch, wie dies mit Java 8-Pipelines gemacht wird:

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());
}

Die Lösung könnte verbessert werden, indem ein benutzerdefinierter Kollektor erstellt wird, der StringBuilder verwendet, sodass Sie nicht jedes leichte Zeichen in eine schwere Zeichenfolge umwandeln müssen.

voho
quelle
-1

Sie können die ungültigen Zeichen ('/', '\', '?', '*') Entfernen und dann verwenden.

Burkhard
quelle
1
Dies würde die Möglichkeit von Namenskonflikten einführen. Das heißt, "tes? T", "tes * t" und "test" würden dieselbe Datei "test" verwenden.
Vog
Wahr. Dann ersetzen Sie sie. Zum Beispiel '/' -> Schrägstrich, '*' -> Stern ... oder verwenden Sie einen Hash, wie von vog vorgeschlagen.
Burkhard
4
Sie sind immer offen für die Möglichkeit, Konflikte zu benennen
Brian Agnew
2
"?" und "*" sind zulässige Zeichen in Dateinamen. Sie müssen nur in Shell-Befehlen maskiert werden, da normalerweise Globbing verwendet wird. Auf der Ebene der Datei-API gibt es jedoch kein Problem.
Vog
2
@ Brian Agnew: nicht wirklich wahr. Schemata, die ungültige Zeichen mithilfe eines umkehrbaren Escape-Schemas codieren, führen nicht zu Kollisionen.
Stephen C