parent
71192cc2ee
commit
48eca3f5af
19 changed files with 115 additions and 654 deletions
|
|
@ -16,7 +16,7 @@ namespace FileManager
|
|||
public string IllegalCharacterReplacements { get; set; }
|
||||
|
||||
/// <summary>Generate a valid path for this file or directory</summary>
|
||||
public LongPath GetFilePath(ReplacementCharacters replacements, bool returnFirstExisting = false)
|
||||
public LongPath GetFilePath(bool returnFirstExisting = false)
|
||||
{
|
||||
|
||||
string fileName = Template.EndsWith(Path.DirectorySeparatorChar) ? Template[..^1] : Template;
|
||||
|
|
@ -43,7 +43,7 @@ namespace FileManager
|
|||
|
||||
pathParts.Reverse();
|
||||
|
||||
return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), replacements, returnFirstExisting);
|
||||
return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), IllegalCharacterReplacements, returnFirstExisting);
|
||||
}
|
||||
|
||||
private string replaceFileName(string filename, Dictionary<string,string> paramReplacements)
|
||||
|
|
|
|||
|
|
@ -46,12 +46,12 @@ namespace FileManager
|
|||
/// <br/>- ensure uniqueness
|
||||
/// <br/>- enforce max file length
|
||||
/// </summary>
|
||||
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, bool returnFirstExisting = false)
|
||||
public static LongPath GetValidFilename(LongPath path, string illegalCharacterReplacements = "", bool returnFirstExisting = false)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
|
||||
// remove invalid chars
|
||||
path = GetSafePath(path, replacements);
|
||||
path = GetSafePath(path, illegalCharacterReplacements);
|
||||
|
||||
// ensure uniqueness and check lengths
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
|
|
@ -77,45 +77,35 @@ namespace FileManager
|
|||
return fullfilename;
|
||||
}
|
||||
|
||||
// GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
|
||||
|
||||
/// <summary>Use with file name, not full path. Valid path charaters which are invalid file name characters will be replaced: ':', '\\', '/'</summary>
|
||||
public static string GetSafeFileName(string str, string illegalCharacterReplacements = "")
|
||||
=> string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars()));
|
||||
|
||||
/// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary>
|
||||
public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
|
||||
public static LongPath GetSafePath(LongPath path, string illegalCharacterReplacements = "")
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
|
||||
var pathNoPrefix = path.PathWithoutPrefix;
|
||||
|
||||
pathNoPrefix = replaceInvalidChars(pathNoPrefix, replacements);
|
||||
pathNoPrefix = replaceColons(pathNoPrefix, "꞉");
|
||||
pathNoPrefix = replaceIllegalWithUnicodeAnalog(pathNoPrefix);
|
||||
pathNoPrefix = replaceInvalidChars(pathNoPrefix, illegalCharacterReplacements);
|
||||
pathNoPrefix = removeDoubleSlashes(pathNoPrefix);
|
||||
|
||||
return pathNoPrefix;
|
||||
}
|
||||
|
||||
public static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] {
|
||||
'*', '?', ':',
|
||||
private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] {
|
||||
'*', '?',
|
||||
// these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included.
|
||||
// In live code, Path.GetInvalidPathChars() does not include them
|
||||
'"', '<', '>'
|
||||
}).ToArray();
|
||||
private static string replaceInvalidChars(string path, ReplacementCharacters replacements)
|
||||
{
|
||||
// replace all colons except within the first 2 chars
|
||||
var builder = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < path.Length; i++)
|
||||
{
|
||||
var c = path[i];
|
||||
|
||||
if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(path)))
|
||||
builder.Append(c);
|
||||
else
|
||||
{
|
||||
char preceding = i > 0 ? path[i - 1] : default;
|
||||
char succeeding = i < path.Length - 1 ? path[i + 1] : default;
|
||||
builder.Append(replacements.GetReplacement(c, preceding, succeeding));
|
||||
}
|
||||
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
private static string replaceInvalidChars(string path, string illegalCharacterReplacements)
|
||||
=> string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars));
|
||||
|
||||
private static string removeDoubleSlashes(string path)
|
||||
{
|
||||
|
|
@ -132,6 +122,60 @@ namespace FileManager
|
|||
return path[0] + remainder;
|
||||
}
|
||||
|
||||
private static string replaceIllegalWithUnicodeAnalog(string path)
|
||||
{
|
||||
char[] replaced = path.ToCharArray();
|
||||
|
||||
char GetQuote(int position)
|
||||
{
|
||||
if (
|
||||
position == 0
|
||||
|| (position > 0
|
||||
&& position < replaced.Length
|
||||
&& !char.IsLetter(replaced[position - 1])
|
||||
&& !char.IsNumber(replaced[position - 1])
|
||||
)
|
||||
) return '“';
|
||||
else if (
|
||||
position == replaced.Length - 1
|
||||
|| (position >= 0
|
||||
&& position < replaced.Length - 1
|
||||
&& !char.IsLetter(replaced[position + 1])
|
||||
&& !char.IsNumber(replaced[position + 1])
|
||||
)
|
||||
) return '”';
|
||||
else return '"';
|
||||
}
|
||||
|
||||
for (int i = 0; i < replaced.Length; i++)
|
||||
{
|
||||
replaced[i] = replaced[i] switch
|
||||
{
|
||||
'?' => '?',
|
||||
'*' => '✱',
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
'"' => GetQuote(i),
|
||||
_ => replaced[i]
|
||||
};
|
||||
}
|
||||
return new string(replaced);
|
||||
}
|
||||
|
||||
private static string replaceColons(string path, string illegalCharacterReplacements)
|
||||
{
|
||||
// replace all colons except within the first 2 chars
|
||||
var builder = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < path.Length; i++)
|
||||
{
|
||||
var c = path[i];
|
||||
if (i >= 2 && c == ':')
|
||||
builder.Append(illegalCharacterReplacements);
|
||||
else
|
||||
builder.Append(c);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*";
|
||||
private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
|
|
@ -162,9 +206,9 @@ namespace FileManager
|
|||
/// <br/>- Perform <see cref="SaferMove"/>
|
||||
/// <br/>- Return valid path
|
||||
/// </summary>
|
||||
public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements)
|
||||
public static string SaferMoveToValidPath(LongPath source, LongPath destination)
|
||||
{
|
||||
destination = GetValidFilename(destination, replacements);
|
||||
destination = GetValidFilename(destination);
|
||||
SaferMove(source, destination);
|
||||
return destination;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,155 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public class Replacement
|
||||
{
|
||||
[JsonIgnore]
|
||||
public bool Mandatory { get; set; }
|
||||
[JsonProperty]
|
||||
public char CharacterToReplace { get; init; }
|
||||
[JsonProperty]
|
||||
public string ReplacementString { get; set; }
|
||||
[JsonProperty]
|
||||
public string Description { get; set; }
|
||||
|
||||
public Replacement Clone() => new()
|
||||
{
|
||||
Mandatory = Mandatory,
|
||||
CharacterToReplace = CharacterToReplace,
|
||||
ReplacementString = ReplacementString,
|
||||
Description = Description
|
||||
};
|
||||
|
||||
public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})";
|
||||
|
||||
public static Replacement Colon(string replacement) => new Replacement { CharacterToReplace = ':', Description = "Colon", ReplacementString = replacement};
|
||||
public static Replacement Asterisk(string replacement) => new Replacement { CharacterToReplace = '*', Description = "Asterisk", ReplacementString = replacement };
|
||||
public static Replacement QuestionMark(string replacement) => new Replacement { CharacterToReplace = '?', Description = "Question Mark", ReplacementString = replacement };
|
||||
public static Replacement OpenAngleBracket(string replacement) => new Replacement { CharacterToReplace = '<', Description = "Open Angle Bracket", ReplacementString = replacement };
|
||||
public static Replacement CloseAngleBracket(string replacement) => new Replacement { CharacterToReplace = '>', Description = "Close Angle Bracket", ReplacementString = replacement };
|
||||
public static Replacement OpenQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Open Quote", ReplacementString = replacement };
|
||||
public static Replacement CloseQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Close Quote", ReplacementString = replacement };
|
||||
public static Replacement OtherQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Other Quote", ReplacementString = replacement };
|
||||
public static Replacement Pipe(string replacement) => new Replacement { CharacterToReplace = '|', Description = "Vertical Line", ReplacementString = replacement };
|
||||
public static Replacement OtherInvalid(string replacement) => new Replacement { CharacterToReplace = default, Description = "Any other invalid characters", ReplacementString = replacement };
|
||||
}
|
||||
|
||||
internal class ReplacementCharactersConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
=> objectType == typeof(ReplacementCharacters);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
var replaceArr = jObj[nameof(Replacement)];
|
||||
var dict = replaceArr
|
||||
.ToObject<Replacement[]>().ToList();
|
||||
|
||||
//Add any missing defaults and ensure they are in the expected order.
|
||||
for (int i = 0; i < ReplacementCharacters.Default.Replacements.Count; i++)
|
||||
{
|
||||
var rep = ReplacementCharacters.Default.Replacements[i].Clone();
|
||||
|
||||
if (i < dict.Count)
|
||||
{
|
||||
var replacementStr = dict[i].ReplacementString;
|
||||
dict[i] = rep;
|
||||
dict[i].ReplacementString = replacementStr;
|
||||
}
|
||||
else
|
||||
{
|
||||
dict.Insert(i, rep);
|
||||
}
|
||||
}
|
||||
|
||||
return new ReplacementCharacters { Replacements = dict };
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
ReplacementCharacters replacements = (ReplacementCharacters)value;
|
||||
|
||||
var propertyNames = replacements.Replacements
|
||||
.Select(c => JObject.FromObject(c)).ToList();
|
||||
|
||||
var prop = new JProperty(nameof(Replacement), new JArray(propertyNames));
|
||||
|
||||
var obj = new JObject();
|
||||
obj.AddFirst(prop);
|
||||
obj.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ReplacementCharactersConverter))]
|
||||
public class ReplacementCharacters
|
||||
{
|
||||
public static readonly ReplacementCharacters Default = new()
|
||||
{
|
||||
Replacements = new()
|
||||
{
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.OpenQuote("“"),
|
||||
Replacement.CloseQuote("”"),
|
||||
Replacement.OtherQuote("""),
|
||||
Replacement.Colon("꞉"),
|
||||
Replacement.Asterisk("✱"),
|
||||
Replacement.QuestionMark("?"),
|
||||
Replacement.OpenAngleBracket("<"),
|
||||
Replacement.CloseAngleBracket(">"),
|
||||
Replacement.Pipe("⏐"),
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly ReplacementCharacters LoFiDefault = new()
|
||||
{
|
||||
Replacements = new()
|
||||
{
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.OpenQuote("'"),
|
||||
Replacement.CloseQuote("'"),
|
||||
Replacement.OtherQuote("'"),
|
||||
Replacement.Colon("-"),
|
||||
Replacement.Asterisk(""),
|
||||
Replacement.QuestionMark(""),
|
||||
Replacement.OpenAngleBracket("["),
|
||||
Replacement.CloseAngleBracket("]"),
|
||||
Replacement.Pipe("_"),
|
||||
}
|
||||
};
|
||||
|
||||
public List<Replacement> Replacements { get; init; }
|
||||
public string DefaultReplacement => Replacements[0].ReplacementString;
|
||||
public string OpenQuote => Replacements[1].ReplacementString;
|
||||
public string CloseQuote => Replacements[2].ReplacementString;
|
||||
public string OtherQuote => Replacements[3].ReplacementString;
|
||||
|
||||
private const char QuoteMark = '"';
|
||||
|
||||
public string GetReplacement(char toReplace, char preceding, char succeding)
|
||||
{
|
||||
if (toReplace == QuoteMark)
|
||||
{
|
||||
if (preceding != default && !char.IsLetter(preceding) && !char.IsNumber(preceding))
|
||||
return OpenQuote;
|
||||
else if (succeding != default && !char.IsLetter(succeding) && !char.IsNumber(succeding))
|
||||
return CloseQuote;
|
||||
else
|
||||
return OtherQuote;
|
||||
}
|
||||
|
||||
for (int i = 4; i < Replacements.Count; i++)
|
||||
{
|
||||
var r = Replacements[i];
|
||||
if (r.CharacterToReplace == toReplace)
|
||||
return r.ReplacementString;
|
||||
}
|
||||
return DefaultReplacement;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue