using System.Text; using FluentResults; using static System.Net.Mime.MediaTypeNames; namespace pgLabII.PgUtils.ConnectionStrings; public class PqConnectionStringTokenizer : IPqConnectionStringTokenizer { private readonly string input; private int position = 0; public bool IsEof { get { ConsumeWhitespace(); return position >= input.Length; } } public PqConnectionStringTokenizer(string input) { this.input = input; position = 0; } public Result GetKeyword() { if (IsEof) return Result.Fail($"Unexpected end of file was expecting a keyword at position {position}"); return GetString(forKeyword: true); } public Result ConsumeEquals() { ConsumeWhitespace(); if (position < input.Length && input[position] == '=') { position++; return Result.Ok(); } else return Result.Fail($"Was expecting '=' after keyword at position {position}"); } public Result GetValue() { if (IsEof) return Result.Fail($"Unexpected end of file was expecting a keyword at position {position}"); return GetString(forKeyword: false); } private Result GetString(bool forKeyword) { if (forKeyword && input[position] == '=') return Result.Fail($"Unexpected '=' was expecting keyword at position {position}"); if (input[position] == '\'') return ParseQuotedText(); return UnquotedString(forKeyword); } private void ConsumeWhitespace() { while (position < input.Length && char.IsWhiteSpace(input[position])) position++; } private string UnquotedString(bool forKeyword) { int start = position; while (++position < input.Length) { char c = input[position]; // Libpq syntax does not use semicolons as pair separators; treat ';' as invalid here if (c == ';') { // Force tokenizer to stop and later cause a parse error by making GetValue/keyword incomplete break; } if (char.IsWhiteSpace(c)) break; if (forKeyword && c == '=') break; } return input.Substring(start, position - start); } private Result ParseQuotedText() { bool escape = false; StringBuilder sb = new(); int start = position; while (++position < input.Length) { char c = input[position]; if (escape) { switch (c) { case '\'': case '\\': sb.Append(c); escape = false; break; default: return Result.Fail($"Invalid escape sequence at position {position}"); } } else { if (c == '\'') { ++position; return sb.ToString(); } else if (c == '\\') { escape = true; } else { sb.Append(c); } } } return Result.Fail($"Missing end quote on value starting at {start}"); } }