C#Sanitize File Name
validation path (11)
Recientemente, he estado moviendo un montón de archivos MP3 de varias ubicaciones a un repositorio. Había estado construyendo los nuevos nombres de archivo usando las etiquetas ID3 (gracias, TagLib-Sharp!), Y noté que estaba obteniendo una System.NotSupportedException
:
"El formato de la ruta dada no es compatible".
Esto fue generado por File.Copy()
o Directory.CreateDirectory()
.
No tardé en darme cuenta de que mis nombres de archivo debían ser desinfectados. Entonces hice lo obvio:
public static string SanitizePath_(string path, char replaceChar)
{
string dir = Path.GetDirectoryName(path);
foreach (char c in Path.GetInvalidPathChars())
dir = dir.Replace(c, replaceChar);
string name = Path.GetFileName(path);
foreach (char c in Path.GetInvalidFileNameChars())
name = name.Replace(c, replaceChar);
return dir + name;
}
Para mi sorpresa, seguí recibiendo excepciones. Resultó que '':'' no está en el conjunto de Path.GetInvalidPathChars()
, porque es válido en una ruta de acceso raíz. Supongo que tiene sentido, pero tiene que ser un problema bastante común. ¿Alguien tiene algún código corto que desinfecte un camino? Lo más exhaustivo que he encontrado es esto, pero parece que probablemente sea excesivo.
// replaces invalid characters with replaceChar
public static string SanitizePath(string path, char replaceChar)
{
// construct a list of characters that can''t show up in filenames.
// need to do this because ":" is not in InvalidPathChars
if (_BadChars == null)
{
_BadChars = new List<char>(Path.GetInvalidFileNameChars());
_BadChars.AddRange(Path.GetInvalidPathChars());
_BadChars = Utility.GetUnique<char>(_BadChars);
}
// remove root
string root = Path.GetPathRoot(path);
path = path.Remove(0, root.Length);
// split on the directory separator character. Need to do this
// because the separator is not valid in a filename.
List<string> parts = new List<string>(path.Split(new char[]{Path.DirectorySeparatorChar}));
// check each part to make sure it is valid.
for (int i = 0; i < parts.Count; i++)
{
string part = parts[i];
foreach (char c in _BadChars)
{
part = part.Replace(c, replaceChar);
}
parts[i] = part;
}
return root + Utility.Join(parts, Path.DirectorySeparatorChar.ToString());
}
Cualquier mejora para hacer que esta función sea más rápida y menos barroca sería muy apreciada.
Aquí hay un método de extensión de carga lenta y eficiente basado en el código de Andre:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LT
{
public static class Utility
{
static string invalidRegStr;
public static string MakeValidFileName(this string name)
{
if (invalidRegStr == null)
{
var invalidChars = System.Text.RegularExpressions.Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()));
invalidRegStr = string.Format(@"([{0}]*/.+$)|([{0}]+)", invalidChars);
}
return System.Text.RegularExpressions.Regex.Replace(name, invalidRegStr, "_");
}
}
}
Creo que el problema es que primero llama a Path.GetDirectoryName
en la cadena incorrecta. Si tiene caracteres que no son de nombre de archivo, .Net no puede decir qué partes de la cadena son directorios y arroja. Tienes que hacer comparaciones de cuerdas.
Suponiendo que solo el nombre de archivo es malo, no toda la ruta, intente esto:
public static string SanitizePath(string path, char replaceChar)
{
int filenamePos = path.LastIndexOf(Path.DirectorySeparatorChar) + 1;
var sb = new System.Text.StringBuilder();
sb.Append(path.Substring(0, filenamePos));
for (int i = filenamePos; i < path.Length; i++)
{
char filenameChar = path[i];
foreach (char c in Path.GetInvalidFileNameChars())
if (filenameChar.Equals(c))
{
filenameChar = replaceChar;
break;
}
sb.Append(filenameChar);
}
return sb.ToString();
}
Estoy usando el método System.IO.Path.GetInvalidFileNameChars()
para verificar los caracteres no válidos y no tengo problemas.
Estoy usando el siguiente código:
foreach( char invalidchar in System.IO.Path.GetInvalidFileNameChars())
{
filename = filename.Replace(invalidchar, ''_'');
}
He tenido éxito con esto en el pasado.
Agradable, corto y estático :-)
public static string returnSafeString(string s)
{
foreach (char character in Path.GetInvalidFileNameChars())
{
s = s.Replace(character.ToString(),string.Empty);
}
foreach (char character in Path.GetInvalidPathChars())
{
s = s.Replace(character.ToString(), string.Empty);
}
return (s);
}
Para limpiar un nombre de archivo, podrías hacer esto
private static string MakeValidFileName( string name )
{
string invalidChars = System.Text.RegularExpressions.Regex.Escape( new string( System.IO.Path.GetInvalidFileNameChars() ) );
string invalidRegStr = string.Format( @"([{0}]*/.+$)|([{0}]+)", invalidChars );
return System.Text.RegularExpressions.Regex.Replace( name, invalidRegStr, "_" );
}
Quería retener los personajes de alguna manera, no simplemente reemplazar el personaje con un guión bajo.
Una de las formas en que pensé era reemplazar los personajes con personajes de aspecto similar que (en mi situación) no se usan como personajes normales. Así que tomé la lista de caracteres inválidos y encontré look-a-likes.
Las siguientes son funciones para codificar y decodificar con look-a-likes.
Este código no incluye una lista completa de todos los caracteres System.IO.Path.GetInvalidFileNameChars (). Por lo tanto, depende de usted extender o utilizar el reemplazo del subrayado para los caracteres restantes.
private static Dictionary<string, string> EncodeMapping()
{
//-- Following characters are invalid for windows file and folder names.
//-- //:*?"<>|
Dictionary<string, string> dic = new Dictionary<string, string>();
dic.Add(@"/", "Ì"); // U+OOCC
dic.Add("/", "Í"); // U+OOCD
dic.Add(":", "¦"); // U+00A6
dic.Add("*", "¤"); // U+00A4
dic.Add("?", "¿"); // U+00BF
dic.Add(@"""", "ˮ"); // U+02EE
dic.Add("<", "«"); // U+00AB
dic.Add(">", "»"); // U+00BB
dic.Add("|", "│"); // U+2502
return dic;
}
public static string Escape(string name)
{
foreach (KeyValuePair<string, string> replace in EncodeMapping())
{
name = name.Replace(replace.Key, replace.Value);
}
//-- handle dot at the end
if (name.EndsWith(".")) name = name.CropRight(1) + "°";
return name;
}
public static string UnEscape(string name)
{
foreach (KeyValuePair<string, string> replace in EncodeMapping())
{
name = name.Replace(replace.Value, replace.Key);
}
//-- handle dot at the end
if (name.EndsWith("°")) name = name.CropRight(1) + ".";
return name;
}
Puede seleccionar su propio look-a-likes. Utilicé la aplicación Mapa de caracteres en Windows para seleccionar la mina %windir%/system32/charmap.exe
A medida que realice ajustes a través del descubrimiento, actualizaré este código.
Según la excelente respuesta de Andre, pero teniendo en cuenta el comentario de Spud sobre las palabras reservadas, hice esta versión:
/// <summary>
/// Strip illegal chars and reserved words from a candidate filename (should not include the directory path)
/// </summary>
/// <remarks>
/// http://.com/questions/309485/c-sharp-sanitize-file-name
/// </remarks>
public static string CoerceValidFileName(string filename)
{
var invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));
var invalidReStr = string.Format(@"[{0}]+", invalidChars);
var reservedWords = new []
{
"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4",
"COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4",
"LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
};
var sanitisedNamePart = Regex.Replace(filename, invalidReStr, "_");
foreach (var reservedWord in reservedWords)
{
var reservedWordPattern = string.Format("^{0}//.", reservedWord);
sanitisedNamePart = Regex.Replace(sanitisedNamePart, reservedWordPattern, "_reservedWord_.", RegexOptions.IgnoreCase);
}
return sanitisedNamePart;
}
Y estas son mis pruebas unitarias
[Test]
public void CoerceValidFileName_SimpleValid()
{
var filename = @"thisIsValid.txt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual(filename, result);
}
[Test]
public void CoerceValidFileName_SimpleInvalid()
{
var filename = @"thisIsNotValid/3//_3.txt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual("thisIsNotValid_3__3.txt", result);
}
[Test]
public void CoerceValidFileName_InvalidExtension()
{
var filename = @"thisIsNotValid.t/xt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual("thisIsNotValid.t_xt", result);
}
[Test]
public void CoerceValidFileName_KeywordInvalid()
{
var filename = "aUx.txt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual("_reservedWord_.txt", result);
}
[Test]
public void CoerceValidFileName_KeywordValid()
{
var filename = "auxillary.txt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual("auxillary.txt", result);
}
Su código sería más claro si anexó el directorio y el nombre del archivo juntos y los desinfectó en lugar de desinfectarlos de manera independiente. En cuanto a desinfectar el:, solo toma el segundo personaje de la cadena. Si es igual a "replacechar", reemplácelo con dos puntos. Dado que esta aplicación es para su propio uso, dicha solución debería ser perfectamente suficiente.
Una solución más corta:
var invalids = System.IO.Path.GetInvalidFileNameChars();
var newName = String.Join("_", origFileName.Split(invalids, StringSplitOptions.RemoveEmptyEntries) ).TrimEnd(''.'');
string clean = String.Concat(dirty.Split(Path.GetInvalidFileNameChars()));
using System;
using System.IO;
using System.Linq;
using System.Text;
public class Program
{
public static void Main()
{
try
{
var badString = "ABC//DEF/GHI<JKL>MNO:PQR/"STU/tVWX|YZA*BCD?EFG";
Console.WriteLine(badString);
Console.WriteLine(SanitizeFileName(badString, ''.''));
Console.WriteLine(SanitizeFileName(badString));
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
private static string SanitizeFileName(string fileName, char? replacement = null)
{
if (fileName == null) { return null; }
if (fileName.Length == 0) { return ""; }
var sb = new StringBuilder();
var badChars = Path.GetInvalidFileNameChars().ToList();
foreach (var @char in fileName)
{
if (badChars.Contains(@char))
{
if (replacement.HasValue)
{
sb.Append(replacement.Value);
}
continue;
}
sb.Append(@char);
}
return sb.ToString();
}
}