Come posso codificare in modo sicuro una stringa in Java da utilizzare come nome file?


117

Ricevo una stringa da un processo esterno. Voglio usare quella stringa per creare un nome file e poi scrivere su quel file. Ecco il mio snippet di codice per farlo:

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

Se s contiene un carattere non valido, come "/" in un sistema operativo basato su Unix, viene generata (giustamente) un'eccezione java.io.FileNotFoundException.

Come posso codificare in sicurezza la stringa in modo che possa essere utilizzata come nome di file?

Modifica: ciò che spero è una chiamata API che lo faccia per me.

Posso farlo:

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

Ma non sono sicuro che URLEncoder sia affidabile per questo scopo.


1
Qual è lo scopo della codifica della stringa?
Stephen C

3
@Stephen C: Lo scopo della codifica della stringa è di renderla adatta all'uso come nome file, come java.net.URLEncoder fa per gli URL.
Steve McLeod

1
Oh, capisco. La codifica deve essere reversibile?
Stephen C

@Stephen C: No, non è necessario che sia reversibile, ma vorrei che il risultato somigliasse il più possibile alla stringa originale.
Steve McLeod

1
La codifica deve oscurare il nome originale? Deve essere 1 a 1? cioè le collisioni sono a posto?
Stephen C

Risposte:


17

Se vuoi che il risultato assomigli al file originale, SHA-1 o qualsiasi altro schema di hashing non è la risposta. Se è necessario evitare le collisioni, anche la semplice sostituzione o rimozione di caratteri "cattivi" non è la risposta.

Invece vuoi qualcosa di simile. (Nota: questo dovrebbe essere trattato come un esempio illustrativo, non come qualcosa da copiare e incollare.)

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

Questa soluzione fornisce una codifica reversibile (senza collisioni) in cui le stringhe codificate assomigliano alle stringhe originali nella maggior parte dei casi. Presumo che tu stia utilizzando caratteri a 8 bit.

URLEncoder funziona, ma ha lo svantaggio di codificare un sacco di caratteri legali per i nomi di file.

Se desideri una soluzione reversibile non garantita, rimuovi semplicemente i caratteri "cattivi" invece di sostituirli con sequenze di escape.


Il contrario della codifica sopra dovrebbe essere altrettanto semplice da implementare.


105

Il mio suggerimento è di adottare un approccio "white list", ovvero non cercare di filtrare i caratteri cattivi. Definisci invece cosa è OK. Puoi rifiutare il nome del file o filtrarlo. Se vuoi filtrarlo:

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

Ciò che fa è sostituire qualsiasi carattere che non sia un numero, una lettera o un trattino basso con niente. In alternativa puoi sostituirli con un altro carattere (come un trattino basso).

Il problema è che se si tratta di una directory condivisa, non si desidera la collisione del nome del file. Anche se le aree di archiviazione dell'utente sono separate dall'utente, potresti finire con un nome file in conflitto semplicemente filtrando i caratteri cattivi. Il nome inserito da un utente è spesso utile se vogliono scaricarlo.

Per questo motivo tendo a consentire all'utente di inserire ciò che desidera, memorizzare il nome del file in base a uno schema di mia scelta (ad esempio userId_fileId) e quindi memorizzare il nome del file dell'utente in una tabella di database. In questo modo puoi visualizzarlo di nuovo all'utente, archiviare le cose come desideri e non compromettere la sicurezza o cancellare altri file.

Puoi anche eseguire l'hash del file (es. Hash MD5) ma non puoi elencare i file inseriti dall'utente (non con un nome significativo).

EDIT: corretto regex per java


Non credo sia una buona idea fornire prima la cattiva soluzione. Inoltre, MD5 è un algoritmo hash quasi crackato. Raccomando almeno SHA-1 o migliore.
vog

19
Ai fini della creazione di un nome file univoco a chi importa se l'algoritmo è "rotto"?
cletus

3
@cletus: il problema è che stringhe diverse verranno mappate sullo stesso nome di file; cioè collisione.
Stephen C

3
Una collisione dovrebbe essere deliberata, la domanda originale non parla di queste stringhe scelte da un attaccante.
tialaramex

8
È necessario utilizzare "\\W+"per la regexp in Java. La barra rovesciata si applica prima alla stringa stessa e \Wnon è una sequenza di escape valida. Ho provato a modificare la risposta, ma sembra che qualcuno abbia rifiutato la mia modifica :(
vadipp

35

Dipende dal fatto che la codifica debba essere reversibile o meno.

Reversibile

Usa la codifica URL ( java.net.URLEncoder) per sostituire i caratteri speciali con %xx. Nota che ti prendi cura dei casi speciali in cui la stringa è uguale ., uguale ..o vuota! ¹ Molti programmi usano la codifica URL per creare nomi di file, quindi questa è una tecnica standard che tutti capiscono.

Irreversibile

Usa un hash (ad esempio SHA-1) della stringa data. I moderni algoritmi hash ( non MD5) possono essere considerati privi di collisioni. In effetti, avrai una svolta nella crittografia se trovi una collisione.


¹ Puoi gestire tutti e 3 i casi speciali in modo elegante utilizzando un prefisso come "myApp-". Se metti il ​​file direttamente in $HOME, dovrai farlo comunque per evitare conflitti con file esistenti come ".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
L'idea di URLEncoder di cosa sia un carattere speciale potrebbe non essere corretta.
Stephen C

4
@vog: URLEncoder fallisce per "." e "..". Questi devono essere codificati altrimenti entrerai in collisione con le voci della directory in $ HOME
Stephen C

6
@vog: "*" è consentito solo nella maggior parte dei filesystem basati su Unix, NTFS e FAT32 non lo supportano.
Jonathan

1
"" e ".." può essere risolto eseguendo l'escape dei punti in% 2E quando la stringa è composta da soli punti (se si desidera ridurre al minimo le sequenze di escape). "*" può anche essere sostituito da "% 2A".
viphe

1
nota che qualsiasi approccio che allunghi il nome del file (cambiando i singoli caratteri in% 20 o qualsiasi altra cosa) invaliderà alcuni nomi di file che sono vicini al limite di lunghezza (255 caratteri per i sistemi Unix)
smcg

24

Ecco cosa utilizzo:

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

Ciò che fa è sostituire ogni carattere che non sia una lettera, un numero, un trattino basso o un punto con un trattino basso, usando regex.

Ciò significa che qualcosa come "Come convertire £ in $" diventerà "How_to_convert___to__". Certo, questo risultato non è molto facile da usare, ma è sicuro e i nomi di directory / file risultanti sono garantiti per funzionare ovunque. Nel mio caso, il risultato non viene mostrato all'utente e quindi non è un problema, ma potresti voler modificare la regex per essere più permissivo.

Vale la pena notare che un altro problema che ho riscontrato è stato che a volte ottenevo nomi identici (poiché si basa sull'input dell'utente), quindi dovresti esserne consapevole, poiché non puoi avere più directory / file con lo stesso nome in una singola directory . Ho appena aggiunto l'ora e la data correnti e una breve stringa casuale per evitarlo. (una stringa casuale effettiva, non un hash del nome del file, poiché nomi di file identici risulteranno in hash identici)

Inoltre, potrebbe essere necessario troncare o accorciare in altro modo la stringa risultante, poiché potrebbe superare il limite di 255 caratteri di alcuni sistemi.


6
Un altro problema è che è specifico per le lingue che utilizzano caratteri ASCII. Per altre lingue, risulterebbe in nomi di file costituiti da nient'altro che trattini bassi.
Andy Thomas

13

Per coloro che cercano una soluzione generale, questi potrebbero essere criteri comuni:

  • Il nome del file dovrebbe assomigliare alla stringa.
  • La codifica dovrebbe essere reversibile ove possibile.
  • La probabilità di collisioni dovrebbe essere ridotta al minimo.

Per ottenere ciò, possiamo usare regex per abbinare caratteri illegali, codificarli in percentuale , quindi limitare la lunghezza della stringa codificata.

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

Patterns

Il modello sopra si basa su un sottoinsieme conservativo di caratteri consentiti nelle specifiche POSIX .

Se vuoi consentire il carattere punto, usa:

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

Fai solo attenzione alle stringhe come "." e ".."

Se vuoi evitare collisioni su filesystem senza distinzione tra maiuscole e minuscole, dovrai evitare le maiuscole:

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

Oppure esci dalle lettere minuscole:

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

Invece di utilizzare una whitelist, puoi scegliere di inserire nella blacklist i caratteri riservati per il tuo file system specifico. EG Questa regex si adatta ai filesystem FAT32:

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

Lunghezza

Su Android, 127 caratteri è il limite sicuro. Molti filesystem consentono 255 caratteri.

Se preferisci mantenere la coda, piuttosto che l'estremità della corda, usa:

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

decodifica

Per riconvertire il nome del file nella stringa originale, utilizzare:

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

limitazioni

Poiché le stringhe più lunghe vengono troncate, esiste la possibilità di un conflitto di nomi durante la codifica o di un danneggiamento durante la decodifica.


1
Posix consente i trattini - dovresti aggiungerlo al modello -Pattern.compile("[^A-Za-z0-9_\\-]")
mkdev

Trattini aggiunti. Grazie :)
SharkAlley

Non credo che la codifica percentuale funzionerebbe bene su Windows, dato che è un carattere riservato ..
Amalgovinus

1
Non considera le lingue diverse dall'inglese.
NateS

5

Prova a utilizzare la seguente regex che sostituisce ogni carattere di nome file non valido con uno spazio:

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

Gli spazi sono nocivi per CLI; considera la sostituzione con _o -.
sdgfsdh


2

Questo probabilmente non è il modo più efficace, ma mostra come farlo utilizzando le pipeline 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());
}

La soluzione potrebbe essere migliorata creando un raccoglitore personalizzato che utilizza StringBuilder, quindi non è necessario eseguire il cast di ogni carattere leggero su una stringa pesante.


-1

Potresti rimuovere i caratteri non validi ("/", "\", "?", "*") E quindi utilizzarli.


1
Ciò introdurrebbe la possibilità di denominare i conflitti. Cioè, "tes? T", "tes * t" e "test" andrebbero nello stesso file "test".
vog

Vero. Quindi sostituiscili. Ad esempio, '/' -> slash, '*' -> star ... o usa un hash come suggerito da vog.
Burkhard

4
Sei sempre aperto alla possibilità di nominare conflitti
Brian Agnew

2
"?" e "*" sono caratteri consentiti nei nomi di file. Devono solo essere sottoposti a escape nei comandi della shell, perché di solito viene utilizzato il globbing. Tuttavia, a livello di API dei file non ci sono problemi.
vog

2
@ Brian Agnew: non è proprio vero. Gli schemi che codificano caratteri non validi utilizzando uno schema di escape reversibile non daranno collisioni.
Stephen C
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.