Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions src/LdifDotNet.Generator/SchemaEntryGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -557,16 +557,7 @@ private static bool IsPrintableChar(char c) =>
private (bool Found, string? Syntax) ResolveSyntax(string attributeName)
{
var definition = _schema.FindAttributeType(attributeName);
bool found = definition is not null;
for (int depth = 0; definition is not null && depth < 20; depth++)
{
if (definition.Syntax is not null)
return (true, definition.Syntax);
definition = definition.SuperiorName is { } superior
? _schema.FindAttributeType(superior)
: null;
}
return (found, null);
return (definition is not null, definition is null ? null : _schema.ResolveSyntaxOid(definition));
}

private LdifValue? SyntaxValue(string syntax, string parentDn)
Expand Down
3 changes: 2 additions & 1 deletion src/LdifDotNet.Schema/LdapAttributeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ internal LdapAttributeType()
/// the form a subschema subentry publishes as attributeTypes values, e.g.
/// "( 2.5.4.4 NAME ( 'sn' 'surname' ) SUP name )". Strict: an unknown keyword,
/// a non-numeric OID, or trailing text throws <see cref="LdapSchemaParseException"/>;
/// for input a server published, use the lenient <see cref="LdapSchema.ParseSubschema"/>.
/// for input a server published, use the lenient
/// <see cref="LdapSchema.ParseSubschema(IEnumerable{string}, IEnumerable{string}, IEnumerable{string})"/>.
/// </summary>
public static LdapAttributeType Parse(string definition)
{
Expand Down
3 changes: 2 additions & 1 deletion src/LdifDotNet.Schema/LdapObjectClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ internal LdapObjectClass()
/// "( 2.5.6.6 NAME 'person' SUP top STRUCTURAL MUST ( sn $ cn ) )". Strict: an
/// unknown keyword, a non-numeric OID, or trailing text throws
/// <see cref="LdapSchemaParseException"/>; for input a server published, use
/// the lenient <see cref="LdapSchema.ParseSubschema"/>.
/// the lenient
/// <see cref="LdapSchema.ParseSubschema(IEnumerable{string}, IEnumerable{string}, IEnumerable{string})"/>.
/// </summary>
public static LdapObjectClass Parse(string definition)
{
Expand Down
97 changes: 88 additions & 9 deletions src/LdifDotNet.Schema/LdapSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ public sealed class LdapSchema
{
private readonly List<LdapAttributeType> _attributeTypes;
private readonly List<LdapObjectClass> _objectClasses;
private readonly List<LdapSyntax> _syntaxes;
private readonly List<LdapUnparsedDefinition> _unparsedDefinitions;
private readonly Dictionary<string, LdapAttributeType> _attributeIndex = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, LdapObjectClass> _classIndex = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, LdapSyntax> _syntaxIndex = new(StringComparer.OrdinalIgnoreCase);

private LdapSchema(
List<LdapAttributeType> attributeTypes,
List<LdapObjectClass> objectClasses,
List<LdapSyntax> syntaxes,
List<LdapUnparsedDefinition>? unparsedDefinitions = null)
{
_attributeTypes = attributeTypes;
_objectClasses = objectClasses;
_syntaxes = syntaxes;
_unparsedDefinitions = unparsedDefinitions ?? [];

foreach (var attributeType in attributeTypes)
Expand All @@ -36,6 +40,12 @@ private LdapSchema(
foreach (string name in objectClass.Names)
_classIndex.TryAdd(name, objectClass);
}
foreach (var syntax in syntaxes)
{
_syntaxIndex.TryAdd(syntax.Oid, syntax);
foreach (string name in syntax.Names)
_syntaxIndex.TryAdd(name, syntax);
}
}

/// <summary>Loads and aggregates schema files in order (later files may reference earlier OID macros).</summary>
Expand All @@ -45,6 +55,7 @@ public static LdapSchema Load(params string[] paths)
var parser = new SchemaParser();
var attributeTypes = new List<LdapAttributeType>();
var objectClasses = new List<LdapObjectClass>();
var syntaxes = new List<LdapSyntax>();

foreach (string path in paths)
{
Expand All @@ -59,14 +70,14 @@ public static LdapSchema Load(params string[] paths)
}
try
{
parser.ParseInto(text, attributeTypes, objectClasses);
parser.ParseInto(text, attributeTypes, objectClasses, syntaxes);
}
catch (LdapSchemaParseException e)
{
throw new LdapSchemaParseException($"{Path.GetFileName(path)}: {e.Message}", e.LineNumber);
}
}
return new LdapSchema(attributeTypes, objectClasses);
return new LdapSchema(attributeTypes, objectClasses, syntaxes);
}

/// <summary>Parses schema definitions from text in slapd.conf schema file format.</summary>
Expand All @@ -75,8 +86,21 @@ public static LdapSchema Parse(string text)
ArgumentNullException.ThrowIfNull(text);
var attributeTypes = new List<LdapAttributeType>();
var objectClasses = new List<LdapObjectClass>();
new SchemaParser().ParseInto(text, attributeTypes, objectClasses);
return new LdapSchema(attributeTypes, objectClasses);
var syntaxes = new List<LdapSyntax>();
new SchemaParser().ParseInto(text, attributeTypes, objectClasses, syntaxes);
return new LdapSchema(attributeTypes, objectClasses, syntaxes);
}

/// <summary>
/// Parses attributeTypes and objectClasses subschema values; equivalent to
/// <see cref="ParseSubschema(IEnumerable{string}, IEnumerable{string}, IEnumerable{string})"/>
/// with no ldapSyntaxes values.
/// </summary>
public static LdapSchema ParseSubschema(
IEnumerable<string> attributeTypeDefinitions,
IEnumerable<string> objectClassDefinitions)
{
return ParseSubschema(attributeTypeDefinitions, objectClassDefinitions, []);
}

/// <summary>
Expand All @@ -90,14 +114,17 @@ public static LdapSchema Parse(string text)
/// </summary>
public static LdapSchema ParseSubschema(
IEnumerable<string> attributeTypeDefinitions,
IEnumerable<string> objectClassDefinitions)
IEnumerable<string> objectClassDefinitions,
IEnumerable<string> ldapSyntaxDefinitions)
{
ArgumentNullException.ThrowIfNull(attributeTypeDefinitions);
ArgumentNullException.ThrowIfNull(objectClassDefinitions);
ArgumentNullException.ThrowIfNull(ldapSyntaxDefinitions);

var parser = new SchemaParser();
var attributeTypes = new List<LdapAttributeType>();
var objectClasses = new List<LdapObjectClass>();
var syntaxes = new List<LdapSyntax>();
var unparsed = new List<LdapUnparsedDefinition>();

foreach (string definition in attributeTypeDefinitions)
Expand Down Expand Up @@ -126,7 +153,20 @@ public static LdapSchema ParseSubschema(
unparsed.Add(new LdapUnparsedDefinition(LdapSchemaDefinitionKind.ObjectClass, definition, e.Message));
}
}
return new LdapSchema(attributeTypes, objectClasses, unparsed);
foreach (string definition in ldapSyntaxDefinitions)
{
if (definition is null)
throw new ArgumentException("Definition values must not be null.", nameof(ldapSyntaxDefinitions));
try
{
syntaxes.Add(parser.ParseSyntaxDefinition(definition, lenient: true));
}
catch (LdapSchemaParseException e)
{
unparsed.Add(new LdapUnparsedDefinition(LdapSchemaDefinitionKind.Syntax, definition, e.Message));
}
}
return new LdapSchema(attributeTypes, objectClasses, syntaxes, unparsed);
}

/// <summary>All attribute types in declaration order.</summary>
Expand All @@ -135,10 +175,15 @@ public static LdapSchema ParseSubschema(
/// <summary>All object classes in declaration order.</summary>
public IReadOnlyList<LdapObjectClass> ObjectClasses => _objectClasses;

/// <summary>All syntax definitions in declaration order.</summary>
public IReadOnlyList<LdapSyntax> Syntaxes => _syntaxes;

/// <summary>
/// Definitions <see cref="ParseSubschema"/> could not parse, raw text
/// preserved. Always empty for the strict <see cref="Load"/> and
/// <see cref="Parse"/> paths, which throw on the first error instead.
/// Definitions
/// <see cref="ParseSubschema(IEnumerable{string}, IEnumerable{string}, IEnumerable{string})"/>
/// could not parse, raw text preserved. Always empty for the strict
/// <see cref="Load"/> and <see cref="Parse"/> paths, which throw on the
/// first error instead.
/// </summary>
public IReadOnlyList<LdapUnparsedDefinition> UnparsedDefinitions => _unparsedDefinitions;

Expand All @@ -156,6 +201,40 @@ public static LdapSchema ParseSubschema(
return _classIndex.GetValueOrDefault(nameOrOid);
}

/// <summary>
/// Finds a syntax by OID (or a slapd-extension name), or null. A length
/// bound is stripped before the lookup, so a raw SYNTAX reference like
/// "1.3.6.1.4.1.1466.115.121.1.15{32768}" finds the syntax it names —
/// OpenLDAP publishes bounded references, and the bound is not part of the
/// syntax's identity.
/// </summary>
public LdapSyntax? FindSyntax(string nameOrOid)
{
ArgumentNullException.ThrowIfNull(nameOrOid);
return _syntaxIndex.GetValueOrDefault(SchemaParser.StripLengthBound(nameOrOid, out _));
}

/// <summary>
/// The attribute type's syntax OID, inherited through its SUP chain when
/// the definition omits SYNTAX (RFC 4512 §2.5.1) — OpenLDAP publishes cn as
/// "SUP name" with no SYNTAX at all. Cycle-guarded; a superior missing from
/// this schema ends the walk. Null when no definition on the chain declares
/// a syntax.
/// </summary>
public string? ResolveSyntaxOid(LdapAttributeType attributeType)
{
ArgumentNullException.ThrowIfNull(attributeType);

var visited = new HashSet<LdapAttributeType>();
for (var current = attributeType; current is not null && visited.Add(current);)
{
if (current.Syntax is not null)
return current.Syntax;
current = current.SuperiorName is { } superior ? FindAttributeType(superior) : null;
}
return null;
}

/// <summary>
/// Attribute names the class requires (MUST), including those inherited through
/// its superior chain. Superiors missing from this schema are skipped.
Expand Down
3 changes: 3 additions & 0 deletions src/LdifDotNet.Schema/LdapSchemaDefinitionKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ public enum LdapSchemaDefinitionKind

/// <summary>An object class description (RFC 4512 §4.1.1), from objectClasses values.</summary>
ObjectClass,

/// <summary>An LDAP syntax description (RFC 4512 §4.1.5), from ldapSyntaxes values.</summary>
Syntax,
}
65 changes: 65 additions & 0 deletions src/LdifDotNet.Schema/LdapSyntax.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
namespace LdifDotNet.Schema;

/// <summary>An LDAP syntax definition (RFC 4512 §4.1.5).</summary>
public sealed class LdapSyntax
{
internal LdapSyntax()
{
}

/// <summary>
/// Parses one bare parenthesized syntax description, the form a subschema
/// subentry publishes as ldapSyntaxes values, e.g.
/// "( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )".
/// Strict: an unknown keyword, a non-numeric OID, or trailing text throws
/// <see cref="LdapSchemaParseException"/>; for input a server published, use
/// the lenient <see cref="LdapSchema.ParseSubschema(System.Collections.Generic.IEnumerable{string}, System.Collections.Generic.IEnumerable{string}, System.Collections.Generic.IEnumerable{string})"/>.
/// </summary>
public static LdapSyntax Parse(string definition)
{
ArgumentNullException.ThrowIfNull(definition);
return new SchemaParser().ParseSyntaxDefinition(definition, lenient: false);
}

/// <summary>The numeric OID that identifies this syntax.</summary>
public string Oid { get; internal set; } = "";

/// <summary>
/// All short names, in declaration order. Usually empty: RFC 4512's grammar
/// gives syntaxes no NAME, but slapd accepts one in ldapsyntax directives
/// and its own shipped pmi.schema uses it.
/// </summary>
public IReadOnlyList<string> Names { get; internal set; } = [];

/// <summary>The first short name, or the OID when the definition has no name.</summary>
public string Name => Names.Count > 0 ? Names[0] : Oid;

/// <summary>The DESC text, if any.</summary>
public string? Description { get; internal set; }

/// <summary>X-* extensions and their values (names are case-insensitive).</summary>
public IReadOnlyDictionary<string, IReadOnlyList<string>> Extensions { get; internal set; } =
new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Whether the definition asserts OpenLDAP's X-NOT-HUMAN-READABLE extension —
/// how a server declares octet-carrying syntaxes no RFC lists. Only an
/// explicit 'TRUE' asserts the flag; a published 'FALSE' is not an assertion.
/// </summary>
public bool NotHumanReadable => HasTrueExtension("X-NOT-HUMAN-READABLE");

/// <summary>
/// Whether the definition asserts OpenLDAP's X-BINARY-TRANSFER-REQUIRED
/// extension (values must transfer via the ;binary option, RFC 4522). Only
/// an explicit 'TRUE' asserts the flag.
/// </summary>
public bool BinaryTransferRequired => HasTrueExtension("X-BINARY-TRANSFER-REQUIRED");

private bool HasTrueExtension(string name) =>
Extensions.TryGetValue(name, out var values)
&& values.Count > 0
&& string.Equals(values[0], "TRUE", StringComparison.OrdinalIgnoreCase);

/// <summary>Returns <see cref="Name"/>.</summary>
public override string ToString() => Name;
}
10 changes: 6 additions & 4 deletions src/LdifDotNet.Schema/LdapUnparsedDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
namespace LdifDotNet.Schema;

/// <summary>
/// A definition value that <see cref="LdapSchema.ParseSubschema"/> could not
/// parse, preserved raw so nothing is silently dropped. A live server's schema
/// cannot be fixed by the consumer, so one malformed or vendor-specific
/// definition degrades into this bucket instead of failing the whole schema.
/// A definition value that
/// <see cref="LdapSchema.ParseSubschema(IEnumerable{string}, IEnumerable{string}, IEnumerable{string})"/>
/// could not parse, preserved raw so nothing is silently dropped. A live
/// server's schema cannot be fixed by the consumer, so one malformed or
/// vendor-specific definition degrades into this bucket instead of failing the
/// whole schema.
/// </summary>
public sealed class LdapUnparsedDefinition
{
Expand Down
12 changes: 9 additions & 3 deletions src/LdifDotNet.Schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@ consumer cannot fix a server's schema, so a definition that fails to parse is
preserved raw in `UnparsedDefinitions` instead of failing the whole schema:

```csharp
// attributeTypes / objectClasses values fetched from the subschema subentry
var schema = LdapSchema.ParseSubschema(attributeTypeValues, objectClassValues);
// attributeTypes / objectClasses / ldapSyntaxes values from the subschema subentry
var schema = LdapSchema.ParseSubschema(attributeTypeValues, objectClassValues, ldapSyntaxValues);
foreach (var bad in schema.UnparsedDefinitions)
Console.WriteLine($"unparsed {bad.Kind}: {bad.Error}");

// A live server publishes cn as "SUP name" with no SYNTAX; resolution walks the chain:
var cn = schema.FindAttributeType("cn");
string? syntaxOid = schema.ResolveSyntaxOid(cn); // 1.3.6.1.4.1.1466.115.121.1.15
var syntax = schema.FindSyntax(syntaxOid); // bounds like {32768} strip automatically
bool octets = syntax.NotHumanReadable; // OpenLDAP's X-NOT-HUMAN-READABLE 'TRUE'

// Strict single-definition parsing is also available:
var cn = LdapAttributeType.Parse("( 2.5.4.3 NAME ( 'cn' 'commonName' ) SUP name )");
var sn = LdapAttributeType.Parse("( 2.5.4.4 NAME ( 'sn' 'surname' ) SUP name )");
```

Proven against OpenLDAP's complete shipped schema set plus eduPerson,
Expand Down
Loading