From e39bbcc61d626e4845379c672d155198e75662ff Mon Sep 17 00:00:00 2001 From: MathiasL Date: Tue, 24 May 2022 10:40:57 +0200 Subject: [PATCH] Add CFG-parser --- .../SteamShared/SourceConfig/SourceCFG.cs | 172 +++++++++++++++++- .../SourceConfig/SourceCFGCommand.cs | 17 +- .../SourceConfig/SourceCFGCommandBase.cs | 12 -- .../SourceConfig/SourceCFGCommandValue.cs | 62 ++++++- .../SteamShared/SourceConfig/test_source.cfg | 7 +- .../SteamShared/SteamShared.csproj | 5 + 6 files changed, 258 insertions(+), 17 deletions(-) delete mode 100644 SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommandBase.cs diff --git a/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFG.cs b/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFG.cs index f944d6c..0171732 100644 --- a/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFG.cs +++ b/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFG.cs @@ -12,6 +12,176 @@ namespace SteamShared.SourceConfig /// /// Gets or sets a list of concommands or convars in this CFG-file. /// - public List Commands { get; set; } + public List? Commands { get; set; } + + public static SourceCFG? FromFile(string path) + { + if (!File.Exists(path)) + { + return null; + } + + var config = new SourceCFG(); + var commands = new List(); + + using (var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + fs.Position = 0; + + int curByte; + int nextByte = 0x20; // Placeholder value + char curChar = ' '; + char nextChar; + + bool wasInQuoted = false; + bool inQuoted = false; + bool inComment = false; + bool cmdNameParsed = false; + bool endOfFile = false; + + while (true) + { + // Start of the file and every new command or line (after line break or unquoted semicolon) + var cmd = new SourceCFGCommand(); + StringBuilder cmdString = new StringBuilder(); + StringBuilder valueString = new StringBuilder(); + + if (endOfFile) + // end of stream + break; + + while (true) + { + // in this loop breaking means going into next line or next command + // continuing means going to next character + // READ NEXT CHAR + + curByte = nextByte < 0 ? -1 : fs.ReadByte(); + + try + { + curChar = Convert.ToChar(curByte); + } + catch + { } + + nextByte = fs.ReadByte(); + + // Since we just sneakily peeked a byte, move it back a notch + if (fs.Position > 0) + fs.Position--; + else + { + // File is probably empty, because the first ReadByte didn't advance the stream position + endOfFile = true; + break; + } + + try + { + nextChar = Convert.ToChar(nextByte); + } + catch + { + // Probably end of file after this one (AFAIK mostly negative numbers can't be parsed so it should be end of stream) + nextChar = ' '; + } + + // PARSE NEXT CHAR... man I could've used regexes or some shit but nvm + + if (inComment && !isEndOfLine(curChar, nextChar)) + // Ends of lines come before this because those are the only ones that cannot be commented out + continue; + + if (curByte < 1 || curChar == '\"' || (curChar == ';' || char.IsWhiteSpace(curChar) && !inQuoted) || isEndOfLine(curChar, nextChar)) + { + // Newlines are also whitespace + // We've reached end of line, a gap between commands or just padding, a quote or the end of command, always break at newlines or semis + + // We might be at the first unquoted space after a command as well, this would mean... oh yeah! we finally finished fetching that command name! + cmdNameParsed = true; + + wasInQuoted = false; + if (curChar == '\"') + { + // Toggle quoted and continue with next character + wasInQuoted = inQuoted; + inQuoted = !inQuoted; + } + + // Create and save current value, if quoted or not empty + if (curByte < 1 || wasInQuoted || !string.IsNullOrWhiteSpace(valueString.ToString())) + { + if (cmd.CommandValues == null) + // Only create it if there are values, otherwise it will be null, for example when executing commands that have no values + cmd.CommandValues = new List(); + + var newVal = new SourceCFGCommandValue(); + newVal.Value = valueString.ToString(); + cmd.CommandValues.Add(newVal); + valueString.Clear(); + + if (curByte < 1) + { + // end of stream or null byte + endOfFile = true; + break; + } + } + + if (curChar == ';' || isEndOfLine(curChar, nextChar)) + { + if (isEndOfLine(curChar, nextChar) && curChar == '\r') + // Windows-Style line breaks need an extra character, so skip that one + fs.Position++; + + // is line break or unquoted semicolon so go to next line, + // if it was just a different whitespace, leave it be + break; + } + + // oh it was a different white space or a quote... so just skip it ig lmao + continue; + } + + if (!inQuoted && curChar == '/' && nextChar == '/') + { + // We've encountered the beginning of a comment (double slashes inside of quoted values are allowed I guess) + inComment = true; + continue; + } + + if (cmdNameParsed) + // We already have the command or convar, so just assume everything else is a value + valueString.Append(curChar); + else + // We still at the beginning of the line bro, awesome! + cmdString.Append(curChar); + } + + // Here is the end of line or command separated by newlines or semicolons (It's late and I've misspelt this thrice) + // Just save the command name, the values should've been added already in zhe process of life Bruder + cmd.CommandName = cmdString.ToString(); + cmdString.Clear(); // Prepare for next line, make it tidy and clean like a baby's bottom :) + + if (!string.IsNullOrWhiteSpace(cmd.CommandName)) + { + commands.Add(cmd); + } + + inQuoted = false; // In case quotes were forgotten but end of line has been reached, just make sure. In the ideal case this should already be false + inComment = false; // We don't want to ignore everything in the file, only everything in that line + cmdNameParsed = false; // Next line will probably have another command so prepare dinner here ;) + } + } + + config.Commands = commands; + return config; + } + + private static bool isEndOfLine(char curChar, char nextChar) + { + return curChar == '\n' || (curChar == '\r' && nextChar == '\n'); + } } } diff --git a/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommand.cs b/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommand.cs index 54d9993..a5b4b62 100644 --- a/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommand.cs +++ b/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommand.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace SteamShared.SourceConfig { - public class SourceCFGCommand : SourceCFGCommandBase + public class SourceCFGCommand { public string? CommandName { get; set; } @@ -14,6 +14,19 @@ namespace SteamShared.SourceConfig /// Gets or sets the values of this command. /// This can be a value or another command, for example when using the 'alias' command. /// - public List CommandValues { get; set; } + public List? CommandValues { get; set; } + + /// + /// Gets all values joined with spaces, + /// use for example when having an echo command with multiple words that's missing quotes + /// + /// + public string? GetValuesAsOne() + { + if (this.CommandValues == null || this.CommandValues.Count < 1) + return null; + + return string.Join(' ', this.CommandValues.Select(val => val.Value)); + } } } diff --git a/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommandBase.cs b/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommandBase.cs deleted file mode 100644 index 360a22b..0000000 --- a/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommandBase.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SteamShared.SourceConfig -{ - public class SourceCFGCommandBase - { - } -} diff --git a/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommandValue.cs b/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommandValue.cs index 5cdc730..cd5c525 100644 --- a/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommandValue.cs +++ b/SteamShared/SteamShared/SteamShared/SourceConfig/SourceCFGCommandValue.cs @@ -6,8 +6,68 @@ using System.Threading.Tasks; namespace SteamShared.SourceConfig { - public class SourceCFGCommandValue : SourceCFGCommandBase + public class SourceCFGCommandValue { public string? Value { get; set; } + + public int? GetInt() + { + if (int.TryParse(this.Value, out int parsed)) + return parsed; + else + return null; + } + + public float? GetFloat() + { + if (float.TryParse(this.Value, out float parsed)) + return parsed; + else + return null; + } + + public List? GetInts() + { + if (this.Value == null) + return null; + + var res = new List(); + + string[] values = this.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + for(int i = 0; i < values.Length; i++) + { + if(int.TryParse(values[i], out int parsed)) + { + res.Add(parsed); + } + } + + if(res.Count > 0) + return res; + + return null; + } + + public List? GetFloats() + { + if (this.Value == null) + return null; + + var res = new List(); + + string[] values = this.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + for (int i = 0; i < values.Length; i++) + { + if (float.TryParse(values[i], out float parsed)) + { + res.Add(parsed); + } + } + + if (res.Count > 0) + return res; + + return null; + } } } diff --git a/SteamShared/SteamShared/SteamShared/SourceConfig/test_source.cfg b/SteamShared/SteamShared/SteamShared/SourceConfig/test_source.cfg index c87b542..51428ff 100644 --- a/SteamShared/SteamShared/SteamShared/SourceConfig/test_source.cfg +++ b/SteamShared/SteamShared/SteamShared/SourceConfig/test_source.cfg @@ -4,4 +4,9 @@ test_command // some comment test_command2; test_command3 // another comment test_cvar "test_cvar4 8; test_cvar5 9";test_command -//test comment \ No newline at end of file +//test comment + +// first, set flags like inQuoted and incomment correctly +// semicolon OR end line if \n or \r\n and in latter case advance stream position an extra byte +// if a different whitespace and not inQuoted just skip it +// if cmdString for command already fetched, and we have non-blank values, or were quoted up until now, add it to a new valuestring, otherwise add it to the cmdString \ No newline at end of file diff --git a/SteamShared/SteamShared/SteamShared/SteamShared.csproj b/SteamShared/SteamShared/SteamShared/SteamShared.csproj index 5624e47..0146f25 100644 --- a/SteamShared/SteamShared/SteamShared/SteamShared.csproj +++ b/SteamShared/SteamShared/SteamShared/SteamShared.csproj @@ -13,5 +13,10 @@ + + + Never + +