web-dev-qa-db-fra.com

Comment puis-je coder en toute sécurité une chaîne dans Java pour l'utiliser comme nom de fichier?)

Je reçois une chaîne d'un processus externe. Je veux utiliser cette chaîne pour créer un nom de fichier, puis écrire dans ce fichier. Voici mon extrait de code pour le faire:

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

Si s contient un caractère non valide, tel que '/' dans un système d'exploitation basé sur Unix, une exception Java.io.FileNotFoundException est levée (à juste titre).

Comment puis-je encoder la chaîne en toute sécurité afin qu'elle puisse être utilisée comme nom de fichier?

Edit: Ce que j'espère, c'est un appel d'API qui le fait pour moi.

Je peux le faire:

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

Mais je ne suis pas sûr que URLEncoder soit fiable à cette fin.

104
Steve McLeod

Si vous voulez que le résultat ressemble au fichier d'origine, SHA-1 ou tout autre schéma de hachage n'est pas la solution. Si les collisions doivent être évitées, le simple remplacement ou la suppression des "mauvais" caractères ne constitue pas non plus la solution.

Au lieu de cela, vous voulez quelque chose comme ça.

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

Cette solution donne un codage réversible (sans collisions) où les chaînes codées ressemblent dans la plupart des cas aux chaînes originales. Je suppose que vous utilisez des caractères 8 bits.

URLEncoder fonctionne, mais il présente l'inconvénient de coder de nombreux caractères de nom de fichier légaux.

Si vous souhaitez une solution irréversible non garantie, supprimez simplement les "mauvais" caractères plutôt que de les remplacer par des séquences d'échappement.

13
Stephen C

Ma suggestion est d'adopter une approche de "liste blanche", ce qui signifie que vous n'essayez pas de filtrer les mauvais caractères. Au lieu de définir ce qui est OK. Vous pouvez soit rejeter le nom de fichier, soit le filtrer. Si vous voulez le filtrer:

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

Cela remplace tout caractère qui n'est pas un chiffre, une lettre ou un trait de soulignement sans rien. Sinon, vous pouvez les remplacer par un autre caractère (comme un trait de soulignement).

Le problème est que s'il s'agit d'un répertoire partagé, vous ne voulez pas de collision de nom de fichier. Même si les zones de stockage utilisateur sont séparées par utilisateur, vous pouvez vous retrouver avec un nom de fichier en collision simplement en filtrant les caractères incorrects. Le nom d'un utilisateur est souvent utile s'il souhaite également le télécharger.

Pour cette raison, j'ai tendance à permettre à l'utilisateur de saisir ce qu'il veut, de stocker le nom de fichier en fonction d'un schéma de mon choix (par exemple, userId_fileId), puis de stocker le nom de fichier de l'utilisateur dans une table de base de données. De cette façon, vous pouvez l'afficher à l'utilisateur, stocker les choses comme vous le souhaitez, sans compromettre la sécurité ni effacer d'autres fichiers.

Vous pouvez également hacher le fichier (par exemple, hachage MD5), mais vous ne pouvez alors pas lister les fichiers que l'utilisateur a insérés (de toute façon pas avec un nom explicite).

EDIT: regex fixe pour Java

97
cletus

Cela dépend si le codage doit être réversible ou non.

Réversible

Utiliser le codage URL (Java.net.URLEncoder) pour remplacer les caractères spéciaux par %xx. Notez que vous vous occupez des cas spéciaux où la chaîne est égale à ., équivaut à .. ou est vide! ¹ De nombreux programmes utilisent le codage d’URL pour créer des noms de fichier. Il s’agit donc d’une technique standard que tout le monde comprend.

Irréversible

Utilisez un hachage (par exemple SHA-1) de la chaîne donnée. Les algorithmes de hachage modernes (pas MD5) peuvent être considérés comme étant sans collision. En fait, vous aurez une percée dans la cryptographie si vous trouvez une collision.


¹ Vous pouvez gérer les 3 cas spéciaux avec élégance en utilisant un préfixe tel que "myApp-". Si vous mettez le fichier directement dans $HOME, vous devrez quand même le faire pour éviter les conflits avec des fichiers existants tels que ".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

Voici ce que j'utilise:

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

Cela permet de remplacer chaque caractère qui n’est pas une lettre, un chiffre, un trait de soulignement ou un point par un trait de soulignement, à l’aide de regex.

Cela signifie que quelque chose comme "Comment convertir £ en $" deviendra "How_to_convert___to__". Certes, ce résultat n’est pas très convivial, mais il est sûr et les noms de fichiers/répertoires qui en résultent ont la garantie de fonctionner partout. Dans mon cas, le résultat n'est pas affiché à l'utilisateur et ne pose donc pas de problème, mais vous voudrez peut-être modifier l'expression rationnelle pour qu'elle soit plus permissive.

Il est à noter qu'un autre problème que j'ai rencontré est que j'obtiens parfois des noms identiques (car ils sont basés sur une entrée utilisateur). Vous devez donc en être conscient, car vous ne pouvez pas avoir plusieurs répertoires/fichiers avec le même nom dans un seul répertoire. . En outre, vous devrez peut-être tronquer ou raccourcir la chaîne résultante, car elle peut dépasser la limite de 255 caractères définie par certains systèmes.

19
JonasCz

Pour ceux qui recherchent une solution générale, ces critères peuvent être communs:

  • Le nom de fichier doit ressembler à la chaîne.
  • Le codage doit être réversible dans la mesure du possible.
  • La probabilité de collision devrait être minimisée.

Pour ce faire, nous pouvons utiliser regex pour faire correspondre des caractères non autorisés, encoder en pourcentage eux, puis contraindre la longueur de la chaîne codée.

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

Modèles

Le modèle ci-dessus est basé sur un sous-ensemble conservateur de caractères autorisés dans la spécification POSIX .

Si vous souhaitez autoriser le caractère de point, utilisez:

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

Méfiez-vous des chaînes comme "." et ".."

Si vous souhaitez éviter les collisions sur des systèmes de fichiers insensibles à la casse, vous devez échapper aux majuscules:

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

Ou échapper aux lettres minuscules:

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

Plutôt que d'utiliser une liste blanche, vous pouvez choisir de mettre en liste noire les caractères réservés pour votre système de fichiers spécifique. PAR EXEMPLE. Cette expression régulière convient aux systèmes de fichiers FAT32:

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

Longueur

Sur Android, 127 caractères est la limite de sécurité. De nombreux systèmes de fichiers autorisent 255 caractères.

Si vous préférez conserver la queue plutôt que la tête de votre chaîne, utilisez:

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

Décodage

Pour reconvertir le nom de fichier en chaîne d'origine, utilisez:

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

Limitations

Étant donné que les chaînes plus longues sont tronquées, il est possible qu'une collision de noms survienne lors du codage ou une corruption lors du décodage.

14
SharkAlley

Essayez d’utiliser la regex suivante qui remplace chaque caractère de nom de fichier invalide par un espace:

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

Choisissez votre poison parmi options présentées par commons-codec , exemple:

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

Ce n'est probablement pas le moyen le plus efficace, mais il montre comment le faire en utilisant Java 8 pipelines:

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 solution pourrait être améliorée en créant un collecteur personnalisé qui utilise StringBuilder. Vous n'avez donc pas besoin de transtyper chaque caractère léger en chaîne lourde.

2
voho

Vous pouvez supprimer les caractères non valides ('/', '\', '?', '*') Puis les utiliser.

0
Burkhard