diff --git a/SrcMod/Shell/Valve/VdfConvert.cs b/SrcMod/Shell/Valve/VdfConvert.cs index 156ae9f..209d854 100644 --- a/SrcMod/Shell/Valve/VdfConvert.cs +++ b/SrcMod/Shell/Valve/VdfConvert.cs @@ -17,11 +17,161 @@ public static class VdfConvert { "\v", @"\v" } }; + #region DeserializeNode + public static VdfNode? DeserializeNode(StreamReader reader, VdfOptions options) => + DeserializeNode(reader, options, out _, null); + + private static VdfNode? DeserializeNode(StreamReader reader, VdfOptions options, out string name, + string? first) + { + string? header = first ?? (reader.ReadLine()?.Trim()); + if (header is null || string.IsNullOrEmpty(header)) + { + name = string.Empty; + return null; + } + + string[] parts = SplitContent(header, options); + if (parts.Length > 2) throw new VdfSerializationException("Too many values in node."); + + VdfNode node; + + name = DeserializeString(parts[0], options); + if (parts.Length == 2) + { + object value = DeserializeObject(DeserializeString(parts[1], options)); + node = new VdfSingleNode(value); + } + else + { + string? next = reader.ReadLine()?.Trim(); + if (next is null) throw new VdfSerializationException("Expected starting '{', found end-of-file."); + else if (next != "{") throw new VdfSerializationException($"Expected starting '{{', found \"{next}\"."); + VdfTreeNode tree = new(); + string? current; + while ((current = reader.ReadLine()?.Trim()) is not null) + { + if (current == "}") break; + VdfNode? output = DeserializeNode(reader, options, out string subName, current); + if (output is null) throw new VdfSerializationException("Error deserializing sub-node."); + tree[subName] = output; + } + if (current is null) throw new VdfSerializationException("Reached end-of-file while deserializing group."); + node = tree; + } + + return node; + } + + private static object DeserializeObject(string content) => + TypeParsers.ParseAll(content); + private static string DeserializeString(string content, VdfOptions options) + { + if (options.useQuotes) + { + if (!content.StartsWith('\"') || !content.EndsWith('\"')) + throw new VdfSerializationException("No quotes found around content."); + content = content[1..^1]; + } + if (options.useEscapeCodes) + { + foreach (KeyValuePair escapeCode in p_escapeCodes.Reverse()) + content = content.Replace(escapeCode.Value, escapeCode.Key); + } + return content; + } + + private static string[] SplitContent(string content, VdfOptions options) + { + content = content.Replace('\t', ' '); + if (options.useQuotes) + { + List values = new(); + string current = string.Empty; + bool inQuote = false; + for (int i = 0; i < content.Length; i++) + { + char c = content[i]; + if (c == '\"' && !(i > 0 && content[i - 1] == '\\')) inQuote = !inQuote; + + if (c == ' ' && !inQuote) + { + if (!string.IsNullOrEmpty(current)) values.Add(current); + current = string.Empty; + } + else current += c; + } + if (inQuote) throw new VdfSerializationException("Reached end-of-line while inside quotations."); + if (!string.IsNullOrEmpty(current)) values.Add(current); + return values.ToArray(); + } + else return content.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + } + #endregion + + #region SerializeNode public static void SerializeNode(StreamWriter writer, VdfNode? node, string name, VdfOptions options) => SerializeNode(writer, node, name, options, 0); public static void SerializeNode(StreamWriter writer, VdfNode? node, string name) => SerializeNode(writer, node, name, VdfOptions.Default, 0); + private static void SerializeNode(StreamWriter writer, VdfNode? node, string name, + VdfOptions options, int indentLevel) + { + if (node is null) return; + else if (node is VdfSingleNode single) SerializeSingleNode(writer, single, name, options, indentLevel); + else if (node is VdfTreeNode tree) SerializeTreeNode(writer, tree, name, options, indentLevel); + else throw new("Unknown node type."); + } + + private static void SerializeSingleNode(StreamWriter writer, VdfSingleNode node, string name, + VdfOptions options, int indentLevel) + { + string? serializedValue = SerializeObject(node.value); + if (serializedValue is null) return; + + writer.Write(new string(' ', indentLevel)); + writer.Write(SerializeString(name, options)); + writer.Write(' '); + + serializedValue = SerializeString(serializedValue, options); + writer.WriteLine(serializedValue); + } + private static void SerializeTreeNode(StreamWriter writer, VdfTreeNode node, string name, + VdfOptions options, int indentLevel) + { + if (node.SubNodeCount <= 0) return; + + writer.Write(new string(' ', indentLevel)); + writer.WriteLine(SerializeString(name, options)); + writer.WriteLine(new string(' ', indentLevel) + '{'); + + foreach (KeyValuePair subNode in node) + SerializeNode(writer, subNode.Value, subNode.Key, options, indentLevel + options.indentSize); + + writer.WriteLine(new string(' ', indentLevel) + '}'); + } + + private static string? SerializeObject(object? obj) + { + if (obj is null) return null; + return obj.ToString() ?? string.Empty; + } + + private static string SerializeString(string content, VdfOptions options) + { + if (options.useEscapeCodes) + { + foreach (KeyValuePair escapeCode in p_escapeCodes) + content = content.Replace(escapeCode.Key, escapeCode.Value); + } + if (options.useQuotes) content = $"\"{content}\""; + return content; + } + + #endregion + + #region ToNodeTree public static VdfNode? ToNodeTree(object? obj) => ToNodeTree(obj, VdfOptions.Default); public static VdfNode? ToNodeTree(object? obj, VdfOptions options) { @@ -43,7 +193,7 @@ public static class VdfConvert dictionary.Values.CopyTo(values, 0); for (int i = 0; i < dictionary.Count; i++) { - tree[SerializeObject(keys.GetValue(i), options)!] = ToNodeTree(values.GetValue(i), options); + tree[SerializeObject(keys.GetValue(i))!] = ToNodeTree(values.GetValue(i), options); } return tree; } @@ -52,7 +202,7 @@ public static class VdfConvert int index = 0; foreach (object item in enumerable) { - tree[SerializeObject(index, options)!] = ToNodeTree(item, options); + tree[SerializeObject(index)!] = ToNodeTree(item, options); index++; } return tree; @@ -93,56 +243,5 @@ public static class VdfConvert return tree; } - - private static void SerializeNode(StreamWriter writer, VdfNode? node, string name, - VdfOptions options, int indentLevel) - { - if (node is null) return; - else if (node is VdfSingleNode single) SerializeSingleNode(writer, single, name, options, indentLevel); - else if (node is VdfTreeNode tree) SerializeTreeNode(writer, tree, name, options, indentLevel); - else throw new("Unknown node type."); - } - - private static void SerializeSingleNode(StreamWriter writer, VdfSingleNode node, string name, - VdfOptions options, int indentLevel) - { - string? serializedValue = SerializeObject(node.value, options); - if (serializedValue is null) return; - - writer.Write(new string(' ', indentLevel)); - writer.Write(SerializeString(name, options)); - writer.Write(' '); - - serializedValue = SerializeString(serializedValue, options); - writer.WriteLine(serializedValue); - } - private static void SerializeTreeNode(StreamWriter writer, VdfTreeNode node, string name, - VdfOptions options, int indentLevel) - { - if (node.SubNodeCount <= 0) return; - - writer.Write(new string(' ', indentLevel)); - writer.WriteLine(SerializeString(name, options)); - writer.WriteLine(new string(' ', indentLevel) + '{'); - - foreach (KeyValuePair subNode in node) - SerializeNode(writer, subNode.Value, subNode.Key, options, indentLevel + options.indentSize); - - writer.WriteLine(new string(' ', indentLevel) + '}'); - } - - private static string? SerializeObject(object? obj, VdfOptions options) - { - if (obj is null) return null; - return obj.ToString() ?? string.Empty; - } - - private static string SerializeString(string content, VdfOptions options) - { - if (options.useEscapeCodes) - foreach (KeyValuePair escapeCode in p_escapeCodes) - content = content.Replace(escapeCode.Key, escapeCode.Value); - if (options.useQuotes) content = $"\"{content}\""; - return content; - } + #endregion } diff --git a/SrcMod/Shell/Valve/VdfSerializationException.cs b/SrcMod/Shell/Valve/VdfSerializationException.cs new file mode 100644 index 0000000..fa7c764 --- /dev/null +++ b/SrcMod/Shell/Valve/VdfSerializationException.cs @@ -0,0 +1,8 @@ +namespace SrcMod.Shell.Valve; + +public class VdfSerializationException : Exception +{ + public VdfSerializationException() : base() { } + public VdfSerializationException(string message) : base(message) { } + public VdfSerializationException(string message, Exception inner) : base(message, inner) { } +} diff --git a/SrcMod/Shell/Valve/VdfSerializer.cs b/SrcMod/Shell/Valve/VdfSerializer.cs index 4ae766b..fd61521 100644 --- a/SrcMod/Shell/Valve/VdfSerializer.cs +++ b/SrcMod/Shell/Valve/VdfSerializer.cs @@ -12,6 +12,17 @@ public class VdfSerializer p_options = options; } + public VdfNode? Deserialize(Stream stream) + { + long pos = stream.Position; + StreamReader reader = new(stream, leaveOpen: !p_options.closeWhenFinished); + VdfNode? result = VdfConvert.DeserializeNode(reader, p_options); + reader.Close(); + + if (!p_options.closeWhenFinished && p_options.resetStreamPosition) stream.Seek(pos, SeekOrigin.Begin); + return result; + } + public void Serialize(Stream stream, object? value, string parentNodeName) { VdfNode? nodeTree = VdfConvert.ToNodeTree(value, p_options); @@ -19,10 +30,11 @@ public class VdfSerializer } public void Serialize(Stream stream, VdfNode? parentNode, string parentNodeName) { + long pos = stream.Position; StreamWriter writer = new(stream, leaveOpen: !p_options.closeWhenFinished); VdfConvert.SerializeNode(writer, parentNode, parentNodeName, p_options); writer.Close(); - if (!p_options.closeWhenFinished && p_options.resetStreamPosition) stream.Seek(0, SeekOrigin.Begin); + if (!p_options.closeWhenFinished && p_options.resetStreamPosition) stream.Seek(pos, SeekOrigin.Begin); } }