Existe uma maneira de tornar o caminho do arquivo de strings seguro em c #?

92

Meu programa pega strings arbitrárias da Internet e as usa para nomes de arquivos. Existe uma maneira simples de remover os caracteres inválidos dessas strings ou preciso escrever uma função personalizada para isso?

Martin Doms
fonte

Respostas:

171

Ugh, odeio quando as pessoas tentam adivinhar quais caracteres são válidos. Além de serem completamente não portáteis (sempre pensando em Mono), ambos os comentários anteriores perderam mais 25 caracteres inválidos.

'Clean just a filename
Dim filename As String = "salmnas dlajhdla kjha;dmas'lkasn"
For Each c In IO.Path.GetInvalidFileNameChars
    filename = filename.Replace(c, "")
Next

'See also IO.Path.GetInvalidPathChars
Jonathan Allen
fonte
83
A versão C #: foreach (var c in Path.GetInvalidFileNameChars ()) {fileName = fileName.Replace (c, '-'); }
jcollum
8
Como esta solução lidaria com conflitos de nomes? Parece que mais de uma string pode corresponder a um único nome de arquivo ("Inferno?" E "Inferno *", por exemplo). Se você está ok para remover apenas caracteres ofensivos, tudo bem; caso contrário, você precisa ter cuidado ao lidar com conflitos de nomes.
Stefano Ricciardi
2
e quanto aos limites de tamanho do nome (e caminho) do sistema de arquivos? e os nomes de arquivo reservados (PRN CON)? Se precisar armazenar os dados e o nome original, você pode usar 2 arquivos com nomes de Guid: guid.txt e guid.dat
Jack
6
Uma linha, para diversão resultado = Path.GetInvalidFileNameChars (). Agregar (resultado, (corrente, c) => corrente.Replace (c, '-'));
Paul Knopf
1
@PaulKnopf, você tem certeza de que o JetBrain não possui direitos autorais sobre esse código;)
Marcus
36

Para retirar caracteres inválidos:

static readonly char[] invalidFileNameChars = Path.GetInvalidFileNameChars();

// Builds a string out of valid chars
var validFilename = new string(filename.Where(ch => !invalidFileNameChars.Contains(ch)).ToArray());

Para substituir caracteres inválidos:

static readonly char[] invalidFileNameChars = Path.GetInvalidFileNameChars();

// Builds a string out of valid chars and an _ for invalid ones
var validFilename = new string(filename.Select(ch => invalidFileNameChars.Contains(ch) ? '_' : ch).ToArray());

Para substituir caracteres inválidos (e evitar conflito de nome em potencial, como Inferno * vs Inferno $):

static readonly IList<char> invalidFileNameChars = Path.GetInvalidFileNameChars();

// Builds a string out of valid chars and replaces invalid chars with a unique letter (Moves the Char into the letter range of unicode, starting at "A")
var validFilename = new string(filename.Select(ch => invalidFileNameChars.Contains(ch) ? Convert.ToChar(invalidFileNameChars.IndexOf(ch) + 65) : ch).ToArray());
Esquilo
fonte
33

Esta pergunta já foi feita muitas vezes antes e, como apontado muitas vezes antes, IO.Path.GetInvalidFileNameCharsnão é adequada.

Primeiro, existem muitos nomes como PRN e CON que são reservados e não são permitidos para nomes de arquivos. Existem outros nomes não permitidos apenas na pasta raiz. Nomes que terminam em ponto também não são permitidos.

Em segundo lugar, há uma variedade de limitações de comprimento. Leia a lista completa de NTFS aqui .

Terceiro, você pode anexar a sistemas de arquivos que possuem outras limitações. Por exemplo, os nomes de arquivo ISO 9660 não podem começar com "-", mas podem contê-lo.

Quarto, o que você faria se dois processos escolhessem "arbitrariamente" o mesmo nome?

Em geral, usar nomes gerados externamente para nomes de arquivos é uma má ideia. Eu sugiro gerar seus próprios nomes de arquivo privados e armazenar nomes legíveis internamente.

Dour High Arch
fonte
13
Embora você seja tecnicamente preciso, GetInvalidFileNameChars é bom para 80% + das situações em que você o usaria, portanto, é uma boa resposta. Sua resposta teria sido mais apropriada como um comentário à resposta aceita, eu acho.
CubanX
4
Eu concordo com DourHighArch. Salve o arquivo internamente como um guia, comparando-o com o "nome amigável" que está armazenado em um banco de dados. Não deixe que os usuários controlem seus caminhos no site ou eles tentarão roubar seu web.config. Se você incorporar a reescrita de url para torná-la limpa, ela só funcionará para urls amigáveis ​​correspondentes no banco de dados.
rtpHarry
22

Eu concordo com Grauenwolf e recomendo fortemente o Path.GetInvalidFileNameChars()

Aqui está minha contribuição C #:

string file = @"38?/.\}[+=n a882 a.a*/|n^%$ ad#(-))";
Array.ForEach(Path.GetInvalidFileNameChars(), 
      c => file = file.Replace(c.ToString(), String.Empty));

ps - isso é mais enigmático do que deveria ser - eu estava tentando ser conciso.

Aaron Wagner
fonte
3
Por que no mundo você usaria em Array.ForEachvez de apenas foreachaqui
BlueRaja - Danny Pflughoeft
9
Se você quiser ser ainda mais conciso / enigmático:Path.GetInvalidFileNameChars().Aggregate(file, (current, c) => current.Replace(c, '-'))
Michael Petito
@ BlueRaja-DannyPflughoeft Porque você quer torná-lo mais lento?
Jonathan Allen
@Johnathan Allen, o que o faz pensar que foreach é mais rápido do que Array.ForEach?
Ryan Buddicom
5
@rbuddicom Array.ForEach recebe um delegado, o que significa que ele precisa invocar uma função que não pode ser embutida. Para strings curtas, você pode acabar gastando mais tempo na sobrecarga da chamada de função do que na lógica real. O .NET Core está procurando maneiras de "desvirtualizar" as chamadas, reduzindo a sobrecarga.
Jonathan Allen
13

Aqui está minha versão:

static string GetSafeFileName(string name, char replace = '_') {
  char[] invalids = Path.GetInvalidFileNameChars();
  return new string(name.Select(c => invalids.Contains(c) ? replace : c).ToArray());
}

Não tenho certeza de como o resultado de GetInvalidFileNameChars é calculado, mas o "Get" sugere que não é trivial, por isso coloco os resultados em cache. Além disso, isso só percorre a string de entrada uma vez em vez de várias vezes, como as soluções acima que iteram sobre o conjunto de caracteres inválidos, substituindo-os na string de origem, um de cada vez. Além disso, gosto das soluções baseadas em Where, mas prefiro substituir caracteres inválidos em vez de removê-los. Por fim, minha substituição é exatamente um caractere para evitar a conversão de caracteres em strings à medida que faço a iteração na string.

Eu digo tudo isso sem fazer o perfil - este apenas "pareceu" bom para mim. :)

csells
fonte
1
Você poderia fazer new HashSet<char>(Path.GetInvalidFileNameChars())para evitar a enumeração O (n) - micro-otimização.
TrueWill
12

Esta é a função que estou usando agora (obrigado jcollum pelo exemplo C #):

public static string MakeSafeFilename(string filename, char replaceChar)
{
    foreach (char c in System.IO.Path.GetInvalidFileNameChars())
    {
        filename = filename.Replace(c, replaceChar);
    }
    return filename;
}

Acabei de colocar isso em uma classe de "Ajudantes" por conveniência.

cara lateral
fonte
7

Se você quiser remover rapidamente todos os caracteres especiais, o que às vezes é mais legível pelo usuário para nomes de arquivo, isso funciona bem:

string myCrazyName = "q`w^e!r@t#y$u%i^o&p*a(s)d_f-g+h=j{k}l|z:x\"c<v>b?n[m]q\\w;e'r,t.y/u";
string safeName = Regex.Replace(
    myCrazyName,
    "\W",  /*Matches any nonword character. Equivalent to '[^A-Za-z0-9_]'*/
    "",
    RegexOptions.IgnoreCase);
// safeName == "qwertyuiopasd_fghjklzxcvbnmqwertyu"
Keith
fonte
1
na verdade, \Wcorresponde a mais do que não-alfa-numéricos ( [^A-Za-z0-9_]). Todos os caracteres de 'palavra' Unicode (русский 中文 ..., etc.) também não serão substituídos. Mas isso é uma coisa boa.
Ishmael
A única desvantagem é que isso também remove, .então você precisa extrair a extensão primeiro e adicioná-la novamente depois.
incrível
5
static class Utils
{
    public static string MakeFileSystemSafe(this string s)
    {
        return new string(s.Where(IsFileSystemSafe).ToArray());
    }

    public static bool IsFileSystemSafe(char c)
    {
        return !Path.GetInvalidFileNameChars().Contains(c);
    }
}
Ronnie Overby
fonte
5

Por que não converter a string em um equivalente em Base64 como este:

string UnsafeFileName = "salmnas dlajhdla kjha;dmas'lkasn";
string SafeFileName = Convert.ToBase64String(Encoding.UTF8.GetBytes(UnsafeFileName));

Se você quiser convertê-lo de volta para que possa lê-lo:

UnsafeFileName = Encoding.UTF8.GetString(Convert.FromBase64String(SafeFileName));

Usei isso para salvar arquivos PNG com um nome exclusivo de uma descrição aleatória.

Bart Vanseer
fonte
5

Aqui está o que eu adicionei para (de ClipFlair http://github.com/Zoomicon/ClipFlair ) StringExtensions classe estática (projeto Utils.Silverlight), com base em informações recolhidas a partir dos links para outras questões relacionadas stackoverflow postados por Dour High Arch acima:

public static string ReplaceInvalidFileNameChars(this string s, string replacement = "")
{
  return Regex.Replace(s,
    "[" + Regex.Escape(new String(System.IO.Path.GetInvalidPathChars())) + "]",
    replacement, //can even use a replacement string of any length
    RegexOptions.IgnoreCase);
    //not using System.IO.Path.InvalidPathChars (deprecated insecure API)
}
George Birbilis
fonte
2
private void textBoxFileName_KeyPress(object sender, KeyPressEventArgs e)
{
   e.Handled = CheckFileNameSafeCharacters(e);
}

/// <summary>
/// This is a good function for making sure that a user who is naming a file uses proper characters
/// </summary>
/// <param name="e"></param>
/// <returns></returns>
internal static bool CheckFileNameSafeCharacters(System.Windows.Forms.KeyPressEventArgs e)
{
    if (e.KeyChar.Equals(24) || 
        e.KeyChar.Equals(3) || 
        e.KeyChar.Equals(22) || 
        e.KeyChar.Equals(26) || 
        e.KeyChar.Equals(25))//Control-X, C, V, Z and Y
            return false;
    if (e.KeyChar.Equals('\b'))//backspace
        return false;

    char[] charArray = Path.GetInvalidFileNameChars();
    if (charArray.Contains(e.KeyChar))
       return true;//Stop the character from being entered into the control since it is non-numerical
    else
        return false;            
}
ecklerpa
fonte
1

Acho que usar isso é rápido e fácil de entender:

<Extension()>
Public Function MakeSafeFileName(FileName As String) As String
    Return FileName.Where(Function(x) Not IO.Path.GetInvalidFileNameChars.Contains(x)).ToArray
End Function

Isso funciona porque a stringé IEnumerableuma charmatriz e há uma stringstring de construtor que recebe uma charmatriz.

cjbarth
fonte
1

Em meus projetos mais antigos, encontrei essa solução, que está funcionando perfeitamente há 2 anos. Estou substituindo chars ilegais por "!", E então checo por !! 's, use seu próprio char.

    public string GetSafeFilename(string filename)
    {
        string res = string.Join("!", filename.Split(Path.GetInvalidFileNameChars()));

        while (res.IndexOf("!!") >= 0)
            res = res.Replace("!!", "!");

        return res;
    }
Roni Tovi
fonte
0

Muitas respostas sugerem o uso, o Path.GetInvalidFileNameChars()que parece uma solução ruim para mim. Eu encorajo você a usar a lista de permissões em vez de lista negra porque os hackers sempre encontrarão uma maneira de contornar isso.

Aqui está um exemplo de código que você pode usar:

    string whitelist = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.";
    foreach (char c in filename)
    {
        if (!whitelist.Contains(c))
        {
            filename = filename.Replace(c, '-');
        }
    }
AnonBird
fonte