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

Lesezeit: 10 Minuten

Benutzer-Avatar
Steve McLeod

Ich erhalte eine Zeichenfolge von einem externen Prozess. Ich möchte diesen String verwenden, um einen Dateinamen zu erstellen, und dann in diese Datei 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, wie z. B. “https://stackoverflow.com/” in einem Unix-basierten Betriebssystem, wird (zu Recht) eine java.io.FileNotFoundException geworfen.

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

Bearbeiten: Was ich hoffe, 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);

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

  • Was ist der Zweck der Codierung der Zeichenfolge?

    – Stefan C

    26. Juli 2009 um 10:18 Uhr

  • @Stephen C: Der Zweck der Codierung der Zeichenfolge besteht darin, sie für die Verwendung als Dateiname geeignet zu machen, wie es java.net.URLEncoder für URLs tut.

    – Steve McLeod

    26. Juli 2009 um 10:21 Uhr

  • Ach ich verstehe. Muss die Kodierung umkehrbar sein?

    – Stefan C

    26. Juli 2009 um 10:26 Uhr

  • @Stephen C: Nein, es muss nicht reversibel sein, aber ich möchte, dass das Ergebnis der ursprünglichen Zeichenfolge so ähnlich wie möglich ist.

    – Steve McLeod

    26. Juli 2009 um 10:29 Uhr

  • Muss die Codierung den ursprünglichen Namen verschleiern? Muss es 1-zu-1 sein? dh sind Kollisionen OK?

    – Stefan C

    26. Juli 2009 um 10:30 Uhr

Benutzer-Avatar
Cletus

Mein Vorschlag ist, einen “weißen Listen”-Ansatz zu wählen, was bedeutet, dass Sie nicht versuchen, schlechte Charaktere 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+", "");

Was dies tut, ist, dass jedes Zeichen ersetzt wird ist nicht eine Zahl, ein Buchstabe oder ein Unterstrich ohne nichts. Alternativ könnten Sie sie durch ein anderes Zeichen ersetzen (z. B. einen Unterstrich).

Das Problem ist, dass Sie, wenn dies ein gemeinsam genutztes Verzeichnis ist, keine Dateinamenkollision wünschen. Selbst wenn Benutzerspeicherbereiche nach Benutzern getrennt sind, kann es am Ende zu einem kollidierenden Dateinamen kommen, indem Sie nur fehlerhafte Zeichen herausfiltern. Der Name, den ein Benutzer eingibt, ist oft nützlich, wenn er ihn jemals herunterladen möchte.

Aus diesem Grund neige ich dazu, dem Benutzer zu erlauben, einzugeben, was er möchte, den Dateinamen basierend auf einem Schema meiner eigenen Wahl zu speichern (zB userId_fileId) und dann den Dateinamen des Benutzers in einer Datenbanktabelle zu speichern. Auf diese Weise können Sie es dem Benutzer wieder anzeigen, Dinge speichern, wie Sie möchten, und Sie gefährden nicht die Sicherheit oder löschen andere Dateien.

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

BEARBEITEN: Regex für Java korrigiert

  • Wen kümmert es, wenn der Algorithmus “kaputt” ist, um einen eindeutigen Dateinamen zu erstellen?

    – Kletus

    26. Juli 2009 um 11:07 Uhr

  • @cletus: Das Problem ist, dass verschiedene Zeichenfolgen demselben Dateinamen zugeordnet werden. dh Kollision.

    – Stefan C

    26. Juli 2009 um 11:19 Uhr

  • Eine Kollision müsste absichtlich sein, die ursprüngliche Frage spricht nicht davon, dass diese Saiten von einem Angreifer ausgewählt wurden.

    – Tialamex

    26. Juli 2009 um 12:33 Uhr

  • Sie müssen verwenden "\\W+" für den regulären Ausdruck in Java. Backslash gilt zuerst für die Zeichenfolge selbst und \W ist keine gültige Escape-Sequenz. Ich habe versucht, die Antwort zu bearbeiten, aber anscheinend hat jemand meine Bearbeitung abgelehnt 🙁

    – vadipp

    8. Mai 2013 um 9:29 Uhr


  • Wie können wir Zeichen aus der obigen Regex ausschließen? dh Leerzeichen, die für Dateinamen sicher sind.

    – alianos-

    23. Januar 2014 um 10:59 Uhr

Es hängt davon ab, ob die Codierung umkehrbar sein soll oder nicht.

Reversibel

URL-Codierung verwenden (java.net.URLEncoder) um Sonderzeichen zu ersetzen %xx. Beachten Sie, dass Sie sich um die kümmern Spezialfälle wobei die Zeichenfolge gleich ist .gleich .. oder ist leer!¹ Viele Programme verwenden URL-Kodierung, um Dateinamen zu erstellen, daher ist dies eine Standardtechnik, die jeder versteht.

Irreversibel

Verwenden Sie einen Hash (z. B. SHA-1) der angegebenen Zeichenfolge. Moderne Hash-Algorithmen (nicht MD5) kann als kollisionsfrei angesehen werden. Tatsächlich haben Sie einen Durchbruch in der Kryptografie, wenn Sie eine Kollision finden.


¹ Sie können alle 3 Sonderfälle elegant behandeln, indem Sie ein Präfix wie verwenden "myApp-". Wenn Sie die Datei direkt in $HOMEmüssen Sie das sowieso tun, um Konflikte mit bestehenden 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!?");
    }
}

  • Die Vorstellung von URLEncoder, was ein Sonderzeichen ist, ist möglicherweise nicht korrekt.

    – Stefan C

    26. Juli 2009 um 10:53 Uhr

  • @vog: URLEncoder schlägt fehl für “.” und “..”. Diese müssen verschlüsselt werden, sonst kollidieren Sie mit Verzeichniseinträgen in $HOME

    – Stefan C

    26. Juli 2009 um 11:12 Uhr

  • @vog: “*” ist nur in den meisten Unix-basierten Dateisystemen erlaubt, NTFS und FAT32 unterstützen es nicht.

    – Jonathan

    17. August 2009 um 18:26 Uhr

  • “.” und “..” können behandelt werden, indem Punkte in %2E maskiert werden, wenn die Zeichenfolge nur aus Punkten besteht (wenn Sie die Escape-Sequenzen minimieren möchten). ‘*’ kann auch durch “%2A” ersetzt werden.

    – viphe

    3. Januar 2013 um 18:48 Uhr

  • Beachten Sie, dass jeder Ansatz, der den Dateinamen verlängert (indem einzelne Zeichen in %20 oder was auch immer geändert werden), einige Dateinamen ungültig macht, die nahe an der Längenbegrenzung liegen (255 Zeichen für Unix-Systeme).

    – smg

    12. August 2014 um 15:43 Uhr

Benutzer-Avatar
Jonas Tschechisch

Folgendes verwende ich:

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

Dies ersetzt jedes Zeichen, das kein Buchstabe, keine Zahl, kein Unterstrich oder Punkt ist, mit einem Unterstrich, indem Regex verwendet wird.

Das bedeutet, dass etwas wie „How to convert £ to $“ zu „How_to_convert___to__“ wird. Zugegeben, dieses Ergebnis ist nicht sehr benutzerfreundlich, aber sicher und die resultierenden Verzeichnis-/Dateinamen funktionieren garantiert überall. In meinem Fall wird das Ergebnis dem Benutzer nicht angezeigt und ist daher kein Problem, aber Sie möchten vielleicht die Regex so ändern, dass sie freizügiger ist.

Erwähnenswert ist, dass ein weiteres Problem, auf das ich gestoßen bin, darin bestand, dass ich manchmal identische Namen erhielt (da es auf Benutzereingaben basiert), also sollten Sie 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 Datum vorangestellt und eine kurze zufällige Zeichenfolge, 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 anderweitig kürzen, da sie die Beschränkung von 255 Zeichen auf einigen Systemen überschreiten kann.

  • Ein weiteres Problem besteht darin, dass es spezifisch für Sprachen ist, die ASCII-Zeichen verwenden. Bei anderen Sprachen würde dies zu Dateinamen führen, die nur aus Unterstrichen bestehen.

    – Andi Thomas

    15. November 2017 um 4:54 Uhr

Benutzer-Avatar
Stefan C

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

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

char fileSep = "https://stackoverflow.com/"; // ... 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 ergibt eine umkehrbare 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 Menge legaler Zeichen für Dateinamen kodiert.

Wenn Sie eine garantiert nicht umkehrbare 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.

Benutzer-Avatar
SharkAlley

Für diejenigen, die nach einer allgemeinen Lösung suchen, könnten dies gängige Kriterien sein:

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

Um dies zu erreichen, können wir Regex verwenden, um illegale Zeichen abzugleichen, Prozentkodierung sie, und beschränken Sie dann die Länge der codierten Zeichenfolge.

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 nur vorsichtig bei Zeichenfolgen wie “.” und “..”

Wenn Sie Kollisionen in Dateisystemen vermeiden möchten, bei denen die Groß-/Kleinschreibung nicht beachtet wird, müssen Sie Großbuchstaben maskieren:

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

Oder Escape-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 Blacklist setzen. ZB Diese Regex passt zu FAT32-Dateisystemen:

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

Länge

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

Wenn Sie lieber das Ende 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

Um den Dateinamen wieder in die ursprüngliche Zeichenfolge umzuwandeln, verwenden Sie:

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.

  • Posix erlaubt Bindestriche – Sie sollten es zum Muster hinzufügen – Pattern.compile("[^A-Za-z0-9_\\-]")

    – mkdev

    13. Juni 2015 um 23:31 Uhr

  • Bindestriche hinzugefügt. Vielen Dank 🙂

    – SharkAlley

    15. Juni 2015 um 16:48 Uhr

  • Ich glaube nicht, dass die Prozentcodierung unter Windows gut funktionieren würde, da es sich um ein reserviertes Zeichen handelt.

    – Amalgowinus

    6. Oktober 2017 um 23:59 Uhr

  • Berücksichtigt keine nicht-englischen Sprachen.

    – NateS

    22. Oktober 2017 um 12:41 Uhr

Benutzer-Avatar
BullyWiiPlaza

Versuchen Sie, die folgende Regex zu verwenden, die jedes ungültige Zeichen im Dateinamen durch ein Leerzeichen ersetzt:

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

  • Posix erlaubt Bindestriche – Sie sollten es zum Muster hinzufügen – Pattern.compile("[^A-Za-z0-9_\\-]")

    – mkdev

    13. Juni 2015 um 23:31 Uhr

  • Bindestriche hinzugefügt. Vielen Dank 🙂

    – SharkAlley

    15. Juni 2015 um 16:48 Uhr

  • Ich glaube nicht, dass die Prozentcodierung unter Windows gut funktionieren würde, da es sich um ein reserviertes Zeichen handelt.

    – Amalgowinus

    6. Oktober 2017 um 23:59 Uhr

  • Berücksichtigt keine nicht-englischen Sprachen.

    – NateS

    22. Oktober 2017 um 12:41 Uhr

Dies ist wahrscheinlich nicht der effektivste Weg, zeigt aber, wie es mit Java 8-Pipelines geht:

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 Collector erstellt wird, der StringBuilder verwendet, sodass Sie nicht jedes leichte Zeichen in eine schwere Zeichenfolge umwandeln müssen.

1334030cookie-checkWie kann ich eine Zeichenfolge in Java sicher codieren, um sie als Dateinamen zu verwenden?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy