Acl parser + unit tests

This commit is contained in:
eelke 2024-11-24 12:46:21 +01:00
parent b846a60f25
commit 9a5feb9d54
6 changed files with 181 additions and 0 deletions

View file

@ -0,0 +1,113 @@
using System.Text;
namespace pgLabII.PgUtils;
public class Acl(string grantee, string rights, string grantor)
{
public string Grantee { get; } = grantee;
public string Rights { get; } = rights;
public string Grantor { get; } = grantor;
}
/// <summary>
/// Parser for acl's. Acl's have a fairly simple structure grantee=right/grantor see also https://www.postgresql.org/docs/current/ddl-priv.html
/// When the grantee is empty it is "PUBLIC" which you can see as a wildcard that matches everybody that can authenticate.
/// Because the grantee and grantor name can contain special characters there is an escape mechinism in place.
/// When the name contains a conflicting character it is quoted. If the name contains a quote it is doubles.
/// </summary>
public class AclParser
{
private int _position = 0;
private string _input = null!;
public Acl Parse(string acl)
{
_input = acl;
_position = 0;
ConsumeWhitespace();
string grantee = ConsumeRoleName();
string rights = ConsumeRights();
if (Current() != '/')
throw new ArgumentException("Unexpected symbol in acl string");
_position++;
string grantor = ConsumeRoleName();
return new Acl(grantee, rights, grantor);
}
/// <summary>
/// There is a limited set of possible characters for the rights
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private string ConsumeRights()
{
if (Current() != '=')
throw new ArgumentException("Unexpected symbol in acl string");
_position++;
int start = _position;
while (NotAtEnd() && Current() != '/')
_position++;
return _input.Substring(start, _position - start);
}
// Whitespace should have been consumed
// There should be either a " in which case the name is quoted
// a = in which case there is no name ie PUBLIC
// all other cases it is an unquoted name
private string ConsumeRoleName() =>
_input[_position] switch
{
'=' => "",
'"' => ParseQuotedName(),
_ => ParseUnquotedName()
};
private string ParseQuotedName()
{
_position++; // consume opening quote
int start = _position;
StringBuilder sb = new();
while (true)
{
while (NotAtEnd() && Current() != '"')
_position++;
char peek = _position + 1 < _input.Length ? _input[_position + 1] : '\0';
if (peek == '"')
{
_position++;
sb.Append(_input.AsSpan(start, _position - start));
_position++;
start = _position;
}
else
{
sb.Append(_input.AsSpan(start, _position - start));
_position++;
break;
}
}
return sb.ToString();
}
private string ParseUnquotedName()
{
int start = _position;
while (NotAtEnd() && Current() != '=')
_position++;
return _input.Substring(start, _position - start);
}
private void ConsumeWhitespace()
{
while (NotAtEnd() && char.IsWhiteSpace(Current()))
++_position;
}
private bool NotAtEnd() => _position < _input.Length;
private char Current() => _input[_position];
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>