1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2024-10-17 09:42:45 +02:00
TweetDuck/lib/TweetLib.Utils/Serialization/SimpleObjectSerializer.cs

168 lines
4.7 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using TweetLib.Utils.Serialization.Converters;
using TweetLib.Utils.Static;
namespace TweetLib.Utils.Serialization {
public sealed class SimpleObjectSerializer<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[index..nextIndex]);
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[index..]).ToString();
}
private readonly TypeConverterRegistry converterRegistry;
private readonly Dictionary<string, PropertyInfo> props;
public SimpleObjectSerializer(TypeConverterRegistry converterRegistry) {
this.converterRegistry = converterRegistry;
this.props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(static prop => prop.CanWrite).ToDictionary(static prop => prop.Name);
}
public void Write(string file, T obj) {
if (props.Count == 0) {
File.Delete(file);
return;
}
var 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) {
var type = prop.Value.PropertyType;
var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance;
if (converter.TryWriteType(type, prop.Value.GetValue(obj), 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.");
}
var errors = new LinkedList<string>();
int currentPos = 0;
do {
string line;
int nextPos = contents.IndexOf(NewLineReal, currentPos);
if (nextPos == -1) {
line = contents[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[..space];
string value = UnescapeLine(line[(space + 1)..]);
if (props.TryGetValue(property, out var info)) {
var type = info.PropertyType;
var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance;
if (converter.TryReadType(type, 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) {
// ignore
} catch (DirectoryNotFoundException) {
// ignore
}
}
}
}