summaryrefslogtreecommitdiff
path: root/src/marlin.core/util
diff options
context:
space:
mode:
authorMelodi <62381002+Melodi17@users.noreply.github.com>2026-09-13 13:22:20 +1000
committerMelodi <62381002+Melodi17@users.noreply.github.com>2026-09-13 13:22:20 +1000
commitdf0737e55afeb5c01eb39909605970d1d3a73ffc (patch)
tree09823eaa310c60eefd2420dc305acae45c81900f /src/marlin.core/util
parentee69ce25b2fba871920260028bb10c2d6b62b26f (diff)
refactor: cleanup; feat: connection to agent
Diffstat (limited to 'src/marlin.core/util')
-rw-r--r--src/marlin.core/util/SecureTokenGenerator.cs16
-rw-r--r--src/marlin.core/util/VariableExpander.cs140
-rw-r--r--src/marlin.core/util/VariableExpansionException.cs56
3 files changed, 212 insertions, 0 deletions
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<string, string> 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<string, string> 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<string, string> 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;
+
+/// <summary>
+/// Base exception for all variable expansion errors
+/// </summary>
+public class VariableExpansionException(string message) : Exception(message);
+
+/// <summary>
+/// Thrown when a variable is referenced but not found in the variables dictionary
+/// </summary>
+public class VariableNotFoundException : VariableExpansionException
+{
+ public string VariableName { get; }
+
+ public VariableNotFoundException(string variableName)
+ : base($"Variable '{variableName}' not found")
+ {
+ this.VariableName = variableName;
+ }
+}
+
+/// <summary>
+/// Thrown when an expression has invalid syntax or cannot be parsed
+/// </summary>
+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;
+ }
+}
+
+/// <summary>
+/// Thrown when braces are not properly matched or escaped
+/// </summary>
+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;
+ }
+}