using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using TweetLib.Core.Serialization.Converters; using TweetLib.Core.Utils; namespace TweetLib.Core.Serialization{ public sealed class FileSerializer<T>{ private const string NewLineReal = "\r\n"; private const string NewLineCustom = "\r~\n"; private static string EscapeLine(string input) => input.Replace("\\", "\\\\").Replace(Environment.NewLine, "\\\r\n"); private static string UnescapeLine(string input) => input.Replace(NewLineCustom, Environment.NewLine); private static string UnescapeStream(StreamReader reader){ string data = reader.ReadToEnd(); StringBuilder build = new StringBuilder(data.Length); int index = 0; while(true){ int nextIndex = data.IndexOf('\\', index); if (nextIndex == -1 || nextIndex+1 >= data.Length){ break; } else{ build.Append(data.Substring(index, nextIndex-index)); char next = data[nextIndex+1]; if (next == '\\'){ // convert double backslash to single backslash build.Append('\\'); index = nextIndex+2; } else if (next == '\r' && nextIndex+2 < data.Length && data[nextIndex+2] == '\n'){ // convert backslash followed by CRLF to custom new line build.Append(NewLineCustom); index = nextIndex+3; } else{ // single backslash build.Append('\\'); index = nextIndex+1; } } } return build.Append(data.Substring(index)).ToString(); } private readonly Dictionary<string, PropertyInfo> props; private readonly Dictionary<Type, ITypeConverter> converters; public FileSerializer(){ this.props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(prop => prop.CanWrite).ToDictionary(prop => prop.Name); this.converters = new Dictionary<Type, ITypeConverter>(); } public void RegisterTypeConverter(Type type, ITypeConverter converter){ converters[type] = converter; } public void Write(string file, T obj){ LinkedList<string> errors = new LinkedList<string>(); FileUtils.CreateDirectoryForFile(file); using(StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))){ foreach(KeyValuePair<string, PropertyInfo> prop in props){ Type type = prop.Value.PropertyType; object value = prop.Value.GetValue(obj); if (!converters.TryGetValue(type, out ITypeConverter serializer)){ serializer = ClrTypeConverter.Instance; } if (serializer.TryWriteType(type, value, out string? converted)){ if (converted != null){ writer.Write(prop.Key); writer.Write(' '); writer.Write(EscapeLine(converted)); writer.Write(NewLineReal); } } else{ errors.AddLast($"Missing converter for type: {type}"); } } } if (errors.First != null){ throw new SerializationSoftException(errors.ToArray()); } } public void Read(string file, T obj){ string contents; using(StreamReader reader = new StreamReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))){ contents = UnescapeStream(reader); } if (string.IsNullOrWhiteSpace(contents)){ throw new FormatException("File is empty."); } else if (contents[0] <= (char)1){ throw new FormatException("Input appears to be a binary file."); } LinkedList<string> errors = new LinkedList<string>(); int currentPos = 0; do{ string line; int nextPos = contents.IndexOf(NewLineReal, currentPos); if (nextPos == -1){ line = contents.Substring(currentPos); currentPos = -1; if (string.IsNullOrEmpty(line)){ break; } } else{ line = contents.Substring(currentPos, nextPos-currentPos); currentPos = nextPos+NewLineReal.Length; } int space = line.IndexOf(' '); if (space == -1){ errors.AddLast($"Missing separator on line: {line}"); continue; } string property = line.Substring(0, space); string value = UnescapeLine(line.Substring(space+1)); if (props.TryGetValue(property, out PropertyInfo info)){ if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)){ serializer = ClrTypeConverter.Instance; } if (serializer.TryReadType(info.PropertyType, value, out object? converted)){ info.SetValue(obj, converted); } else{ errors.AddLast($"Failed reading property {property} with value: {value}"); } } }while(currentPos != -1); if (errors.First != null){ throw new SerializationSoftException(errors.ToArray()); } } public void ReadIfExists(string file, T obj){ try{ Read(file, obj); }catch(FileNotFoundException){ }catch(DirectoryNotFoundException){} } } }