From df0737e55afeb5c01eb39909605970d1d3a73ffc Mon Sep 17 00:00:00 2001 From: Melodi <62381002+Melodi17@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:22:20 +1000 Subject: refactor: cleanup; feat: connection to agent --- src/marlin.core/util/SecureTokenGenerator.cs | 16 +++ src/marlin.core/util/VariableExpander.cs | 140 +++++++++++++++++++++ src/marlin.core/util/VariableExpansionException.cs | 56 +++++++++ 3 files changed, 212 insertions(+) create mode 100644 src/marlin.core/util/SecureTokenGenerator.cs create mode 100644 src/marlin.core/util/VariableExpander.cs create mode 100644 src/marlin.core/util/VariableExpansionException.cs (limited to 'src/marlin.core/util') diff --git a/src/marlin.core/util/SecureTokenGenerator.cs b/src/marlin.core/util/SecureTokenGenerator.cs new file mode 100644 index 0000000..f99918c --- /dev/null +++ b/src/marlin.core/util/SecureTokenGenerator.cs @@ -0,0 +1,16 @@ +namespace marlin.core.util; + +using System.Security.Cryptography; + +public class SecureTokenGenerator +{ + private const string ALLOWED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + private static string Create(int length = 32) + { + return RandomNumberGenerator.GetString(ALLOWED_CHARS, length); + } + + public static string CreateAgentToken() => "ag-" + Create(); + public static string CreateClaimToken() => "cl-" + Create(); +} \ No newline at end of file diff --git a/src/marlin.core/util/VariableExpander.cs b/src/marlin.core/util/VariableExpander.cs new file mode 100644 index 0000000..3c3636e --- /dev/null +++ b/src/marlin.core/util/VariableExpander.cs @@ -0,0 +1,140 @@ +namespace marlin.core.util; + +using System.Text; +using System.Text.RegularExpressions; + +public class VariableExpander +{ + public static string Expand(string format, Dictionary variables) + { + StringBuilder output = new(); + + for (int i = 0; i < format.Length; i++) + { + char c = format[i]; + if (c == '{') + { + if (i + 1 >= format.Length) + throw new UnmatchedBraceException("Trailing expression '{'", i, format[i..]); + + if (format[i + 1] == '{') + { + output.Append('{'); + i++; + } + else + { + string expr = GetExpression(format, ref i); + string value = EvaluateExpression(expr, variables); + output.Append(value); + } + } + else if (c == '}') + { + if (i + 1 >= format.Length) + throw new UnmatchedBraceException("Uninitiated expression at '}'", i, format[i..]); + + if (format[i + 1] == '}') + { + output.Append('}'); + i++; + } + else + { + throw new UnmatchedBraceException("Uninitiated expression at '}'", i, format[i..]); + } + } + else + output.Append(c); + } + + string result = output.ToString(); + + // Clean up extra spaces: collapse multiple consecutive spaces (but preserve other whitespace) + result = Regex.Replace(result, @" {2,}", " "); + result = result.Trim(); + + return result; + } + + private static string GetExpression(string format, ref int pos) + { + StringBuilder part = new(); + + if (format[pos] != '{') + throw new InvalidExpressionException(format[pos..], "Expected '{' at start of expression"); + + pos++; + + for (; pos < format.Length; pos++) + { + if (format[pos] == '}') + break; + + part.Append(format[pos]); + } + + if (pos >= format.Length || format[pos] != '}') + throw new UnmatchedBraceException($"Trailing expression {format[pos..]}", pos, format[pos..]); + + return part.ToString(); + } + + private static string EvaluateExpression(string expr, Dictionary variables) + { + Match match; + + const string identifier = "[a-zA-Z0-9-_.]+"; + + // Variable value + match = Regex.Match(expr, $"^{identifier}$"); + if (match.Success) + return GetVariableOrThrow(expr, variables); + + // Conditional argument: only included if variable is set. + // --hello var + match = Regex.Match(expr, $"^(--?{identifier})\\s+({identifier})$"); + if (match.Success) + { + if (variables.TryGetValue(match.Groups[2].Value, out var value)) + return $"{match.Groups[1].Value} {value}"; + + else + return ""; + } + + // Default value, if value is unset, uses default + match = Regex.Match(expr, $@"^({identifier})\s*=\s*(.*)$"); + if (match.Success) + { + if (variables.TryGetValue(match.Groups[1].Value, out var value)) + return value; + else + return match.Groups[2].Value; + } + + // Use value if set + match = Regex.Match(expr, $@"^({identifier})\?\s*(.*)$"); + if (match.Success) + { + if (variables.TryGetValue(match.Groups[1].Value, out var value)) + { + if (string.IsNullOrEmpty(value) || value == "null" || value == "false") + return ""; + + return match.Groups[2].Value; + } + else + return ""; + } + + throw new InvalidExpressionException(expr); + } + + private static string GetVariableOrThrow(string variable, Dictionary variables) + { + return variables.TryGetValue(variable, out var value) + ? value + : throw new VariableNotFoundException(variable); + } +} \ No newline at end of file diff --git a/src/marlin.core/util/VariableExpansionException.cs b/src/marlin.core/util/VariableExpansionException.cs new file mode 100644 index 0000000..3053880 --- /dev/null +++ b/src/marlin.core/util/VariableExpansionException.cs @@ -0,0 +1,56 @@ +namespace marlin.core.util; + +/// +/// Base exception for all variable expansion errors +/// +public class VariableExpansionException(string message) : Exception(message); + +/// +/// Thrown when a variable is referenced but not found in the variables dictionary +/// +public class VariableNotFoundException : VariableExpansionException +{ + public string VariableName { get; } + + public VariableNotFoundException(string variableName) + : base($"Variable '{variableName}' not found") + { + this.VariableName = variableName; + } +} + +/// +/// Thrown when an expression has invalid syntax or cannot be parsed +/// +public class InvalidExpressionException : VariableExpansionException +{ + public string Expression { get; } + + public InvalidExpressionException(string expression) + : base($"Invalid expression '{expression}'") + { + this.Expression = expression; + } + + public InvalidExpressionException(string expression, string reason) + : base($"Invalid expression '{expression}': {reason}") + { + this.Expression = expression; + } +} + +/// +/// Thrown when braces are not properly matched or escaped +/// +public class UnmatchedBraceException : VariableExpansionException +{ + public int Position { get; } + public string Remainder { get; } + + public UnmatchedBraceException(string message, int position, string remainder) + : base(message) + { + this.Position = position; + this.Remainder = remainder; + } +} -- cgit v1.3.1