moonldap

RFC-driven LDAPv3 client library for MoonBit with a generic ASN.1 BER codec

ldap
asn1
ber
directory
client
moon add hbYlj/moonldap@0.3.0
Download zip
Author
Version
0.3.0
License
Apache-2.0
Last updated
12 hours ago
Downloads
4

Dependencies

README

#hbYlj/moonldap

MoonLDAP is a transport-agnostic, RFC-driven LDAPv3 client library for MoonBit. It ships a generic ASN.1 BER codec (X.690), an RFC 4515 filter parser, an RFC 4514 DN parser, and the LDAPv3 operations needed to bind and search a directory server.

  • The protocol core is backend-agnostic: it never touches sockets or the file system, so moon check --target native/js/wasm-gc all pass.
  • A native socket adapter is isolated in the hbYlj/moonldap/socket package.
  • A scripted FakeTransport makes tests fully deterministic and offline.

#Layout

PathPurpose
*.mbt (module root)Protocol core: BER, messages, filter, DN, session, client
socket/Native async/socket transport adapter
cmd/mainmoonldap CLI (search / explain)
examples/Runnable examples (bind_search, sasl_plain, ...)
testdata/Scripted scenario data

#Minimal example

///|
async fn main {
let cfg = @moonldap.LdapConfig::new("localhost", 389)
let transport = @socket.SocketTransport::new()
let client = @moonldap.LdapClient::new(cfg, transport)
let _ = client.connect_and_bind_simple("cn=admin,dc=example,dc=com", "secret")
let req = @moonldap.SearchRequest::new(
"dc=example,dc=com",
@moonldap.SearchScope::Subtree,
"(cn=John*)",
).unwrap()
let outcome = client.search_to_array(req).unwrap()
for entry in outcome.entries {
println(entry.object_name)
}
println(client.trace.to_string())
}

#CLI

moon run cmd/main -- search --host localhost --port 389 \ --base "ou=People,dc=example,dc=com" --scope sub \ --filter "(cn=John*)" --attrs cn,mail --auth-dn "..." --auth-pass "..."

moon run cmd/main -- explain testdata/scenario_bind.json

#Examples

moon run examples/filter_roundtrip # filter string -> BER -> roundtrip moon run examples/ber_dump # byte stream -> BER tree moon run examples/fake_session # offline FakeTransport session moon run examples/bind_search # live bind + search (needs a server) moon run examples/sasl_plain # live SASL PLAIN bind (needs a server)

#Scope

The 0.3.0 release (final validation) keeps every 0.1.0 capability and adds:

  • Full operation set: add, modify, delete, modifyDN, compare (client.add/modify/delete/modify_dn/compare).
  • Paged results control (RFC 2696) with automatic cookie-following via client.search_paged, plus the server-side sort control (RFC 2891).
  • SASL DIGEST-MD5 (RFC 2831, session.bind_digest_md5) with a pure-MoonBit MD5, and an SASL EXTERNAL bind helper.
  • StartTLS protocol negotiation (RFC 2830) with a mandatory-ordering mode: LdapConfig::new(require_tls=true) refuses any plaintext bind before StartTLS succeeded.
  • LDAP URL parsing (RFC 4516) with percent-decoding and serialization.
  • Retry/reconnect: RetryPolicy transparently reconnects and resends on transport-level failures; protocol results are never retried.
  • Ten runnable examples and 219 deterministic offline tests.

See docs/SECURITY_MODEL.md, docs/COMPATIBILITY.md and docs/LIMITATIONS.md for the security model, wire-compatibility notes (including the RFC 4511 tag corrections made in this release) and the maintenance roadmap.

#License

Apache-2.0

#
LdapTransport

pub(open) trait LdapTransport {
async fn connect(Self, LdapConfig) -> Result[Unit, LdapError]
async fn write(Self, Bytes) -> Result[Unit, LdapError]
async fn read(Self) -> Result[Bytes, LdapError]
fn close(Self) -> Unit
}

The transport abstraction. The protocol core never touches sockets directly; it only depends on this interface. Implementations include the scripted FakeTransport and the native socket adapter.

#
BerError

pub(all) suberror BerError {
Truncated
InvalidLength
InvalidTag
IndefinitePrimitive
MissingEoc
UnexpectedEoc
TooDeep
TooManyElements
IntegerTooLarge
LengthExceedsLimit(Int)
NotSingleValue
InvalidOid
} derive(Eq,
Debug
)

Errors produced by the BER encoder/decoder.

#
LdapError

pub(all) suberror LdapError {
Ber(BerError)
Decode(String)
Encode(String)
Transport(String)
NotBound
PrematureClose
UnexpectedOp(Int)
InvalidMessageId(Int)
Unsupported(String)
InvalidFilter(String)
InvalidDn(String)
ScriptMismatch(String)
TlsRequired
} derive(Eq,
Debug
)

Errors produced by the LDAP message layer and the session layer. BER errors are nested so callers can distinguish transport, protocol and decode failures.

#
LdapError::to_string

fn LdapError::to_string(self : LdapError) -> String

#
AddRequest

pub struct AddRequest {
entry : String
attributes : Array[Attribute]
} derive(Eq,
Debug
)

AddRequest ::= [APPLICATION 8] SEQUENCE { entry, attributes } (RFC 4511 4.8).

#
AddRequest::new

fn AddRequest::new(entry : String, attributes : Array[Attribute]) -> AddRequest

#
AddResponse

pub struct AddResponse {
result : LdapResult
} derive(Eq,
Debug
)

The response to an add operation (RFC 4511 4.9).

#
AddResponse::new

fn AddResponse::new(result : LdapResult) -> AddResponse

#
Attribute

pub struct Attribute {
attr_type : String
values : Array[Bytes]
} derive(Eq,
Debug
)

An attribute with a required single value (RFC 4511 Attribute).

#
Attribute::new

fn Attribute::new(attr_type : String, values : Array[Bytes]) -> Attribute

#
AttributeValueAssertion

pub struct AttributeValueAssertion {
attribute_desc : String
assertion_value : Bytes
} derive(Eq,
Debug
)

AttributeValueAssertion used by CompareRequest (RFC 4511 4.11).

#
AttributeValueAssertion::new

fn AttributeValueAssertion::new(attribute_desc : String, assertion_value : Bytes) -> AttributeValueAssertion

#
AuthenticationChoice

pub(all) enum AuthenticationChoice {
Simple(String)
Sasl(SaslCredentials)
} derive(Eq,
Debug
)

Authentication choice for the bind operation (RFC 4511 4.2).

#
BerDecoder

pub struct BerDecoder {
data : Bytes
limits : BerLimits
// private fields
} derive(
Debug
)

A cursor over a byte buffer used to decode one or more BER values.

#
BerDecoder::at_end

fn BerDecoder::at_end(self : BerDecoder) -> Bool

#
BerDecoder::new

fn BerDecoder::new(data : Bytes, limits : BerLimits) -> BerDecoder

#
BerDecoder::position

fn BerDecoder::position(self : BerDecoder) -> Int

#
BerDecoder::read_length

fn BerDecoder::read_length(self : BerDecoder) -> Result[BerLength, BerError]

#
BerDecoder::read_tag

fn BerDecoder::read_tag(self : BerDecoder) -> Result[BerTag, BerError]

#
BerDecoder::read_value

fn BerDecoder::read_value(self : BerDecoder, depth : Int) -> Result[BerValue, BerError]

Decode a single BER value (and all its nested values) starting at the current cursor position.

#
BerDecoder::remaining

fn BerDecoder::remaining(self : BerDecoder) -> Int

#
BerLength

pub(all) enum BerLength {
Definite(Int)
Indefinite
} derive(Eq,
Debug
)

A decoded BER length octet(s) (X.690 8.1.3).

#
BerLimits

pub struct BerLimits {
max_length : Int
max_depth : Int
max_elements : Int
max_integer_bytes : Int
} derive(Eq,
Debug
)

Limits enforced during BER decoding to protect against malformed or malicious input (anti-DoS).

#
BerTag

pub struct BerTag {
class : TagClass
constructed : Bool
number : Int
} derive(Eq,
Debug
)

An ASN.1 identifier octet(s) decoded into its components.

#
BerTag::first_octet

fn BerTag::first_octet(self : BerTag) -> Int

First identifier octet. When number >= 31 the low five bits are 0b11111 and the full number follows in long form.

#
BerTag::is_constructed

fn BerTag::is_constructed(self : BerTag) -> Bool

#
BerTag::new

fn BerTag::new(class : TagClass, constructed : Bool, number : Int) -> BerTag

#
BerTag::to_string

fn BerTag::to_string(self : BerTag) -> String

#
BerValue

pub struct BerValue {
tag : BerTag
content : Bytes
children : Array[BerValue]?
} derive(Eq,
Debug
)

A generic decoded BER value (X.690). Constructed values expose their children; primitive values expose their raw content octets.

#
BerValue::children_or_empty

fn BerValue::children_or_empty(self : BerValue) -> Array[BerValue]

#
BerValue::constructed

fn BerValue::constructed(tag : BerTag, children : Array[BerValue]) -> BerValue

#
BerValue::is_constructed

fn BerValue::is_constructed(self : BerValue) -> Bool

#
BerValue::primitive

fn BerValue::primitive(tag : BerTag, content : Bytes) -> BerValue

#
BerValue::to_string

fn BerValue::to_string(self : BerValue) -> String

A human-readable dump of the value tree; used by the ber_dump example and debugging tooling.

#
BindRequest

pub struct BindRequest {
version : Int
name : String
authentication : AuthenticationChoice
} derive(Eq,
Debug
)

#
BindRequest::sasl

fn BindRequest::sasl(version : Int, name : String, creds : SaslCredentials) -> BindRequest

#
BindRequest::simple

fn BindRequest::simple(version : Int, name : String, password : String) -> BindRequest

#
BindResponse

pub struct BindResponse {
result : LdapResult
server_sasl_creds : Bytes?
} derive(Eq,
Debug
)

#
BindResponse::new

fn BindResponse::new(result : LdapResult) -> BindResponse

#
CompareRequest

pub struct CompareRequest {
entry : String
ava : AttributeValueAssertion
} derive(Eq,
Debug
)

CompareRequest (RFC 4511 4.11): assert an attribute value on an entry.

#
CompareRequest::new

fn CompareRequest::new(entry : String, ava : AttributeValueAssertion) -> CompareRequest

#
CompareResponse

pub struct CompareResponse {
result : LdapResult
} derive(Eq,
Debug
)

The response to a compare operation; compareTrue / compareFalse carry the assertion outcome (RFC 4511 4.12).

#
CompareResponse::new

#
Control

pub struct Control {
control_type : String
criticality : Bool
control_value : Bytes
} derive(Eq,
Debug
)

A request control (RFC 4511 4.1.11).

#
Control::new

fn Control::new(control_type : String, criticality : Bool, control_value : Bytes) -> Control

#
DelRequest

pub struct DelRequest {
entry : String
} derive(Eq,
Debug
)

DelRequest ::= [APPLICATION 10] LDAPDN (primitive, RFC 4511 4.10).

#
DelRequest::new

fn DelRequest::new(entry : String) -> DelRequest

#
DelResponse

pub struct DelResponse {
result : LdapResult
} derive(Eq,
Debug
)

The response to a delete operation (RFC 4511 4.11).

#
DelResponse::new

fn DelResponse::new(result : LdapResult) -> DelResponse

#
DerefAliases

pub(all) enum DerefAliases {
Never
InSearching
FindingBase
Always
} derive(Eq,
Debug
)

Alias dereferencing policy (RFC 4511 4.5.1.3).

#
DerefAliases::from_int

fn DerefAliases::from_int(v : Int) -> DerefAliases?

#
DerefAliases::to_int

fn DerefAliases::to_int(self : DerefAliases) -> Int

#
DigestChallenge

pub struct DigestChallenge {
realms : Array[String]
nonce : String
qop_options : Array[String]
charset : String?
} derive(Eq,
Debug
)

Parsed SASL DIGEST-MD5 challenge directives (RFC 2831 2.1.1).
pub struct Dn {
rdns : Array[Array[(String, String)]]
} derive(Eq,
Debug
)

A parsed RFC 4514 distinguished name. rdns is the ordered list of relative distinguished names; each RDN is an ordered list of attribute type/value pairs (a multi-valued RDN has more than one pair).

#
Dn::empty

fn Dn::empty() -> Dn

#
Dn::is_empty

fn Dn::is_empty(self : Dn) -> Bool

#
Dn::length

fn Dn::length(self : Dn) -> Int

The number of RDNs.

#
Dn::parse

fn Dn::parse(s : String) -> Result[Dn, LdapError]

#
Dn::to_string

fn Dn::to_string(self : Dn) -> String

#
ExtendedRequest

pub struct ExtendedRequest {
request_name : String
request_value : Bytes?
} derive(Eq,
Debug
)

ExtendedRequest ::= [APPLICATION 23] SEQUENCE { requestName [0] LDAPOID, requestValue [1] OCTET STRING OPTIONAL } (RFC 4511 4.12).

#
ExtendedRequest::new

fn ExtendedRequest::new(request_name : String, request_value : Bytes?) -> ExtendedRequest

#
ExtendedResponse

pub struct ExtendedResponse {
result : LdapResult
response_name : String?
response_value : Bytes?
} derive(Eq,
Debug
)

ExtendedResponse ::= [APPLICATION 24] SEQUENCE { COMPONENTS OF LDAPResult, responseName [10] LDAPOID OPTIONAL, responseValue [11] OPTIONAL } (RFC 4511 4.12).

#
ExtendedResponse::new

#
ExtensibleMatch

pub struct ExtensibleMatch {
matching_rule : String?
attr_type : String?
match_value : Bytes
dn_attributes : Bool
} derive(Eq,
Debug
)

An extensible match filter (RFC 4515 3 / RFC 4511 4.5.1.7).

#
ExtensibleMatch::new

fn ExtensibleMatch::new(matching_rule : String?, attr_type : String?, match_value : Bytes, dn_attributes : Bool) -> ExtensibleMatch

#
FakeStep

pub(all) enum FakeStep {
Expect(Bytes)
Recv(Bytes)
Respond(Bytes)
Fail(LdapError)
Close
} derive(
Debug
)

One step of a scripted transport. Constructed by callers (or tests) to script a FakeTransport.

#
FakeTransport

pub struct FakeTransport {
steps : Array[FakeStep]
index : Int
log : Array[Bytes]
connected : Bool
// private fields
}

A fully scripted transport for deterministic, offline tests. It verifies the request bytes written by the client and feeds back scripted response bytes.

#
FakeTransport::has_unconsumed_steps

fn FakeTransport::has_unconsumed_steps(self : FakeTransport) -> Bool

#
FakeTransport::is_connected

fn FakeTransport::is_connected(self : FakeTransport) -> Bool

#
FakeTransport::log

fn FakeTransport::log(self : FakeTransport) -> Array[Bytes]

The request bytes received so far.

#
FakeTransport::new

#
FakeTransport::with_steps

fn FakeTransport::with_steps(steps : Array[FakeStep]) -> FakeTransport

#
LdapClient

pub struct LdapClient[T] {
session : Session[T]
trace : LdapTrace
}

A high-level LDAP client that wraps a Session and records a diagnostic trace. It never logs passwords or SASL payloads.

#
LdapClient::abandon

async fn[T : LdapTransport] LdapClient::abandon(self : LdapClient[T], message_id : Int) -> Result[Unit, LdapError]

Abandon an outstanding request.

#
LdapClient::add

async fn[T : LdapTransport] LdapClient::add(self : LdapClient[T], request : AddRequest) -> Result[AddResponse, LdapError]

Add an entry and record the trace.

#
LdapClient::bind_plain

async fn[T : LdapTransport] LdapClient::bind_plain(self : LdapClient[T], authzid : String, authcid : String, password : String) -> Result[BindResponse, LdapError]

Perform a SASL PLAIN bind and record the trace.

#
LdapClient::bind_simple

async fn[T : LdapTransport] LdapClient::bind_simple(self : LdapClient[T], name : String, password : String) -> Result[BindResponse, LdapError]

Perform a simple bind and record the trace.

#
LdapClient::compare

async fn[T : LdapTransport] LdapClient::compare(self : LdapClient[T], request : CompareRequest) -> Result[CompareResponse, LdapError]

Compare an attribute value; compareTrue / compareFalse are recorded as successful steps in the trace.

#
LdapClient::connect

async fn[T : LdapTransport] LdapClient::connect(self : LdapClient[T]) -> Result[Unit, LdapError]

#
LdapClient::connect_and_bind_plain

async fn[T : LdapTransport] LdapClient::connect_and_bind_plain(self : LdapClient[T], authzid : String, authcid : String, password : String) -> Result[BindResponse, LdapError]

Connect, then perform a SASL PLAIN bind.

#
LdapClient::connect_and_bind_simple

async fn[T : LdapTransport] LdapClient::connect_and_bind_simple(self : LdapClient[T], name : String, password : String) -> Result[BindResponse, LdapError]

Connect, then perform a simple bind.

#
LdapClient::delete

async fn[T : LdapTransport] LdapClient::delete(self : LdapClient[T], entry : String) -> Result[DelResponse, LdapError]

Delete an entry and record the trace.

#
LdapClient::is_bound

fn[T] LdapClient::is_bound(self : LdapClient[T]) -> Bool

#
LdapClient::modify

async fn[T : LdapTransport] LdapClient::modify(self : LdapClient[T], request : ModifyRequest) -> Result[ModifyResponse, LdapError]

Modify an entry and record the trace.

#
LdapClient::modify_dn

async fn[T : LdapTransport] LdapClient::modify_dn(self : LdapClient[T], request : ModifyDnRequest) -> Result[ModifyDnResponse, LdapError]

Rename or move an entry and record the trace.

#
LdapClient::new

fn[T] LdapClient::new(config : LdapConfig, transport : T) -> LdapClient[T]

#
LdapClient::search

async fn[T : LdapTransport] LdapClient::search(self : LdapClient[T], request : SearchRequest, on_entry? : (SearchResultEntry) -> Unit, on_reference? : (SearchResultReference) -> Unit) -> Result[LdapResult, LdapError]

Run a streaming search and record the trace.

#
LdapClient::search_paged

async fn[T : LdapTransport] LdapClient::search_paged(self : LdapClient[T], request : SearchRequest, page_size : Int) -> Result[SearchOutcome, LdapError]

Run a search with automatic paged-results iteration (RFC 2696): each page is requested with page_size entries and the server-issued cookie is followed until it comes back empty. All pages are merged into a single outcome and one trace step per page is recorded.

#
LdapClient::search_to_array

async fn[T : LdapTransport] LdapClient::search_to_array(self : LdapClient[T], request : SearchRequest) -> Result[SearchOutcome, LdapError]

Collect a search into an outcome and record the trace.

#
LdapClient::set_retry

fn[T] LdapClient::set_retry(self : LdapClient[T], policy : RetryPolicy) -> Unit

Configure retry behaviour on the client's underlying session.

#
LdapClient::start_tls

async fn[T : LdapTransport] LdapClient::start_tls(self : LdapClient[T]) -> Result[ExtendedResponse, LdapError]

Issue StartTLS on the client's session and record the outcome in the diagnostic trace.

#
LdapClient::unbind

async fn[T : LdapTransport] LdapClient::unbind(self : LdapClient[T]) -> Result[Unit, LdapError]

Send an unbind and close the connection.

#
LdapConfig

pub struct LdapConfig {
host : String
port : Int
version : Int
max_message_size : Int
timeout_ms : Int?
require_tls : Bool
} derive(Eq,
Debug
)

The connection parameters for an LDAP session.

#
LdapConfig::new

fn LdapConfig::new(host : String, port : Int, version? : Int, max_message_size? : Int, timeout_ms? : Int, require_tls? : Bool) -> LdapConfig

#
LdapFilter

pub(all) enum LdapFilter {
And(Array[LdapFilter])
Or(Array[LdapFilter])
Not(LdapFilter)
Equality(String, Bytes)
Substrings(String, SubstringFilter)
GreaterOrEqual(String, Bytes)
LessOrEqual(String, Bytes)
Present(String)
Approx(String, Bytes)
Extensible(ExtensibleMatch)
} derive(Eq,
Debug
)

An LDAP search filter (RFC 4515).

#
LdapFilter::equality

fn LdapFilter::equality(attr : String, value : Bytes) -> LdapFilter

#
LdapFilter::present

fn LdapFilter::present(attr : String) -> LdapFilter

#
LdapFilter::to_bytes

fn LdapFilter::to_bytes(self : LdapFilter) -> Bytes

#
LdapFilter::to_string

fn LdapFilter::to_string(self : LdapFilter) -> String

Render a filter back to an RFC 4515 string (lossless round trip).

#
LdapMessage

pub struct LdapMessage {
message_id : Int
op : ProtocolOp
controls : Array[Control]?
} derive(Eq,
Debug
)

The LDAPMessage envelope (RFC 4511 4.1.1).

#
LdapMessage::new

fn LdapMessage::new(message_id : Int, op : ProtocolOp) -> LdapMessage

#
LdapMessage::with_controls

fn LdapMessage::with_controls(message_id : Int, op : ProtocolOp, controls : Array[Control]) -> LdapMessage

#
LdapResult

pub struct LdapResult {
result_code : ResultCode
matched_dn : String
diagnostic_message : String
referral : Array[String]?
} derive(Eq,
Debug
)

#
LdapResult::failure

fn LdapResult::failure(code : ResultCode, diagnostic : String) -> LdapResult

#
LdapResult::is_success

fn LdapResult::is_success(self : LdapResult) -> Bool

#
LdapResult::success

fn LdapResult::success() -> LdapResult

#
LdapStep

pub struct LdapStep {
op : String
result_code : Int
diagnostic : String
} derive(Eq,
Debug
)

A single recorded operation step.

#
LdapStep::new

fn LdapStep::new(op : String, result_code : Int, diagnostic : String) -> LdapStep

#
LdapTrace

pub struct LdapTrace {
steps : Array[LdapStep]
result_code : Int
matched_dn : String?
diagnostic : String?
entries : Int
referrals : Array[String]
} derive(
Debug
)

A running diagnostic report. It is recorded by LdapClient as operations execute; passwords and SASL payloads never appear in it.

#
LdapTrace::diagnostic

fn LdapTrace::diagnostic(self : LdapTrace) -> String?

#
LdapTrace::entries

fn LdapTrace::entries(self : LdapTrace) -> Int

#
LdapTrace::matched_dn

fn LdapTrace::matched_dn(self : LdapTrace) -> String?

#
LdapTrace::new

fn LdapTrace::new() -> LdapTrace

#
LdapTrace::referrals

fn LdapTrace::referrals(self : LdapTrace) -> Array[String]

#
LdapTrace::result_code

fn LdapTrace::result_code(self : LdapTrace) -> Int

#
LdapTrace::steps

fn LdapTrace::steps(self : LdapTrace) -> Array[LdapStep]

#
LdapTrace::to_string

fn LdapTrace::to_string(self : LdapTrace) -> String

Render the trace as a compact multi-line string for CLI output.

#
LdapUrl

pub struct LdapUrl {
secure : Bool
host : String
port : Int
base_dn : String
attributes : Array[String]
scope : SearchScope
filter : String
} derive(Eq,
Debug
)

An LDAP URL (RFC 4516): scheme://host:port/base?attrs?scope?filter?exts.

#
LdapUrl::is_secure

fn LdapUrl::is_secure(self : LdapUrl) -> Bool

#
LdapUrl::to_string

fn LdapUrl::to_string(self : LdapUrl) -> String

Serialize back to a canonical LDAP URL string.

#
ModifyDnRequest

pub struct ModifyDnRequest {
entry : String
newrdn : String
delete_old_rdn : Bool
new_superior : String?
} derive(Eq,
Debug
)

ModifyDNRequest (RFC 4511 4.9): rename/move an entry.

#
ModifyDnRequest::new

fn ModifyDnRequest::new(entry : String, newrdn : String, delete_old_rdn : Bool, new_superior : String?) -> ModifyDnRequest

#
ModifyDnResponse

pub struct ModifyDnResponse {
result : LdapResult
} derive(Eq,
Debug
)

The response to a modifyDN operation (RFC 4511 4.10).

#
ModifyDnResponse::new

#
ModifyOperation

pub(all) enum ModifyOperation {
Add
Delete
Replace
} derive(Eq,
Debug
)

A modify operation kind (RFC 4511 4.6): add(0), delete(1), replace(2).

#
ModifyOperation::from_int

fn ModifyOperation::from_int(v : Int) -> ModifyOperation?

#
ModifyOperation::to_int

fn ModifyOperation::to_int(self : ModifyOperation) -> Int

#
ModifyRequest

pub struct ModifyRequest {
object : String
changes : Array[ModifyRequestChange]
} derive(Eq,
Debug
)

ModifyRequest ::= [APPLICATION 6] SEQUENCE { object, changes } (RFC 4511 4.6).

#
ModifyRequest::new

fn ModifyRequest::new(object : String, changes : Array[ModifyRequestChange]) -> ModifyRequest

#
ModifyRequestChange

pub struct ModifyRequestChange {
operation : ModifyOperation
modification : PartialAttribute
} derive(Eq,
Debug
)

One change entry of a ModifyRequest (RFC 4511 4.6).

#
ModifyRequestChange::new

#
ModifyResponse

pub struct ModifyResponse {
result : LdapResult
} derive(Eq,
Debug
)

The response to a modify operation (RFC 4511 4.7).

#
ModifyResponse::new

#
PagedResultsResponse

pub struct PagedResultsResponse {
total_size : Int
cookie : Bytes
} derive(Eq,
Debug
)

#
PagedResultsResponse::new

fn PagedResultsResponse::new(total_size : Int, cookie : Bytes) -> PagedResultsResponse

#
PartialAttribute

pub struct PartialAttribute {
attr_type : String
values : Array[Bytes]
} derive(Eq,
Debug
)

A single attribute value list (RFC 4511 PartialAttribute).

#
PartialAttribute::first_string

fn PartialAttribute::first_string(self : PartialAttribute) -> String?

Return the first value decoded as UTF-8 (lossy).

#
PartialAttribute::new

fn PartialAttribute::new(attr_type : String, values : Array[Bytes]) -> PartialAttribute

#
PartialAttribute::single

fn PartialAttribute::single(attr_type : String, value : Bytes) -> PartialAttribute

#
ProtocolOp

pub(all) enum ProtocolOp {
BindRequest(BindRequest)
BindResponse(BindResponse)
UnbindRequest
SearchRequest(SearchRequest)
SearchResultEntry(SearchResultEntry)
SearchResultDone(LdapResult)
SearchResultReference(SearchResultReference)
ModifyRequest(ModifyRequest)
ModifyResponse(ModifyResponse)
AddRequest(AddRequest)
AddResponse(AddResponse)
DelRequest(DelRequest)
DelResponse(DelResponse)
ModifyDnRequest(ModifyDnRequest)
ModifyDnResponse(ModifyDnResponse)
CompareRequest(CompareRequest)
CompareResponse(CompareResponse)
ExtendedRequest(ExtendedRequest)
ExtendedResponse(ExtendedResponse)
AbandonRequest(Int)
} derive(Eq,
Debug
)

The protocolOp CHOICE of LDAPMessage (RFC 4511 4.1.1).

#
ResultCode

pub(all) enum ResultCode {
Success
OperationsError
ProtocolError
TimeLimitExceeded
SizeLimitExceeded
CompareFalse
CompareTrue
AuthMethodNotSupported
StrongerAuthRequired
Referral
AdminLimitExceeded
UnavailableCriticalExtension
ConfidentialityRequired
SaslBindInProgress
NoSuchAttribute
UndefinedAttributeType
InappropriateMatching
ConstraintViolation
AttributeOrValueExists
InvalidAttributeSyntax
NoSuchObject
AliasProblem
InvalidDNSyntax
AliasDereferencingProblem
InappropriateAuthentication
InvalidCredentials
InsufficientAccessRights
Busy
Unavailable
UnwillingToPerform
LoopDetect
NamingViolation
ObjectClassViolation
NotAllowedOnNonLeaf
NotAllowedOnRDN
EntryAlreadyExists
ObjectClassModsProhibited
AffectsMultipleDSAs
Other
ServerDown
LocalError
EncodingError
DecodingError
Timeout
AuthUnknown
FilterError
UserCanceled
ParameterError
NoMemory
ConnectError
NotSupported
ControlNotFound
NoResultsReturned
MoreResultsToReturn
ClientLoop
ReferralLimitExceeded
Unknown(Int)
} derive(Eq,
Debug
)

LDAP result codes. Codes 0-80 come from RFC 4511 A.10; codes 81+ are the conventional client-side extensions (matching ldap.h).

#
ResultCode::from_int

fn ResultCode::from_int(code : Int) -> ResultCode

#
ResultCode::is_referral

fn ResultCode::is_referral(self : ResultCode) -> Bool

#
ResultCode::is_success

fn ResultCode::is_success(self : ResultCode) -> Bool

#
ResultCode::to_int

fn ResultCode::to_int(self : ResultCode) -> Int

#
ResultCode::to_string

fn ResultCode::to_string(self : ResultCode) -> String

#
RetryPolicy

pub struct RetryPolicy {
max_retries : Int
backoff_ms : Int
} derive(Eq,
Debug
)

Retry behaviour for session round trips. Only transport-level failures (broken connection, premature close) trigger a retry; protocol results are never retried. backoff_ms is the wait between attempts (0 keeps tests and offline usage fully deterministic).

#
RetryPolicy::new

fn RetryPolicy::new(max_retries? : Int, backoff_ms? : Int) -> RetryPolicy

#
SaslCredentials

pub struct SaslCredentials {
mechanism : String
credentials : Bytes
} derive(Eq,
Debug
)

#
SaslCredentials::new

fn SaslCredentials::new(mechanism : String, credentials : Bytes) -> SaslCredentials

#
SearchDone

pub struct SearchDone {
result : LdapResult
controls : Array[Control]?
} derive(Eq,
Debug
)

A completed search exchange: the final result plus any response controls (e.g. the paged results cookie from RFC 2696).

#
SearchOutcome

pub struct SearchOutcome {
entries : Array[SearchResultEntry]
references : Array[SearchResultReference]
result : LdapResult
} derive(Eq,
Debug
)

The outcome of a search: all entries and references collected plus the final SearchResultDone.

#
SearchOutcome::new

#
SearchRequest

pub struct SearchRequest {
base_object : String
scope : SearchScope
deref_aliases : DerefAliases
size_limit : Int
time_limit : Int
types_only : Bool
attributes : Array[String]
filter : LdapFilter
} derive(Eq,
Debug
)

#
SearchRequest::new

fn SearchRequest::new(base_object : String, scope : SearchScope, filter : String, deref_aliases? : DerefAliases, size_limit? : Int, time_limit? : Int, types_only? : Bool, attributes? : Array[String]) -> Result[SearchRequest, LdapError]

#
SearchResultEntry

pub struct SearchResultEntry {
object_name : String
attributes : Array[PartialAttribute]
} derive(Eq,
Debug
)

#
SearchResultEntry::new

fn SearchResultEntry::new(object_name : String, attributes : Array[PartialAttribute]) -> SearchResultEntry

#
SearchResultReference

pub struct SearchResultReference {
uris : Array[String]
} derive(Eq,
Debug
)

SearchResultReference is the list of referral URIs (RFC 4511 4.5.3).

#
SearchResultReference::new

#
SearchScope

pub(all) enum SearchScope {
Base
OneLevel
Subtree
} derive(Eq,
Debug
)

Search scope (RFC 4511 4.5.1.2).

#
SearchScope::from_int

fn SearchScope::from_int(v : Int) -> SearchScope?

#
SearchScope::to_int

fn SearchScope::to_int(self : SearchScope) -> Int

#
Session

pub struct Session[T] {
config : LdapConfig
transport : T
// private fields
}

A simple sequential LDAP session. It allocates message IDs, sends requests and matches the corresponding responses.

#
Session::abandon

async fn[T : LdapTransport] Session::abandon(self : Session[T], message_id : Int) -> Result[Unit, LdapError]

Abandon an outstanding request (RFC 4511 4.11). No response is expected.

#
Session::add

async fn[T : LdapTransport] Session::add(self : Session[T], request : AddRequest) -> Result[AddResponse, LdapError]

Add an entry (RFC 4511 4.8).

#
Session::bind

async fn[T : LdapTransport] Session::bind(self : Session[T], request : BindRequest) -> Result[BindResponse, LdapError]

Perform a bind with a custom bind request.

#
Session::bind_digest_md5

async fn[T : LdapTransport] Session::bind_digest_md5(self : Session[T], username : String, password : String, digest_uri? : String, cnonce? : String) -> Result[BindResponse, LdapError]

Perform the two-step DIGEST-MD5 bind exchange (RFC 2831): an initial empty SASL bind, parsing the server challenge, then answering with the computed response. cnonce can be supplied for deterministic tests; when omitted it is derived from the nonce.

#
Session::bind_plain

async fn[T : LdapTransport] Session::bind_plain(self : Session[T], authzid : String, authcid : String, password : String) -> Result[BindResponse, LdapError]

Perform a SASL PLAIN bind (RFC 4616). Marks the session as bound on success.

#
Session::bind_simple

async fn[T : LdapTransport] Session::bind_simple(self : Session[T], name : String, password : String) -> Result[BindResponse, LdapError]

Perform a simple bind (RFC 4513 5.2.1). Marks the session as bound on success.

#
Session::compare

async fn[T : LdapTransport] Session::compare(self : Session[T], request : CompareRequest) -> Result[CompareResponse, LdapError]

Compare an attribute value on an entry (RFC 4511 4.11). The assertion outcome is carried in the result code (compareTrue / compareFalse).

#
Session::connect

async fn[T : LdapTransport] Session::connect(self : Session[T]) -> Result[Unit, LdapError]

Establish the underlying connection.

#
Session::delete

async fn[T : LdapTransport] Session::delete(self : Session[T], entry : String) -> Result[DelResponse, LdapError]

Delete an entry (RFC 4511 4.10).

#
Session::is_bound

fn[T] Session::is_bound(self : Session[T]) -> Bool

#
Session::is_tls_active

fn[T] Session::is_tls_active(self : Session[T]) -> Bool

#
Session::modify

async fn[T : LdapTransport] Session::modify(self : Session[T], request : ModifyRequest) -> Result[ModifyResponse, LdapError]

Modify an entry (RFC 4511 4.6).

#
Session::modify_dn

async fn[T : LdapTransport] Session::modify_dn(self : Session[T], request : ModifyDnRequest) -> Result[ModifyDnResponse, LdapError]

Rename or move an entry (RFC 4511 4.9).

#
Session::new

fn[T] Session::new(config : LdapConfig, transport : T) -> Session[T]

#
Session::search

async fn[T : LdapTransport] Session::search(self : Session[T], request : SearchRequest, on_entry? : (SearchResultEntry) -> Unit, on_reference? : (SearchResultReference) -> Unit) -> Result[LdapResult, LdapError]

Execute a search and stream the results to callbacks. on_entry and on_reference are invoked as the corresponding messages arrive; the returned LdapResult is the final SearchResultDone.

#
Session::search_done

async fn[T : LdapTransport] Session::search_done(self : Session[T], request : SearchRequest, controls? : Array[Control], on_entry? : (SearchResultEntry) -> Unit, on_reference? : (SearchResultReference) -> Unit) -> Result[SearchDone, LdapError]

Execute a search with optional request controls, streaming results to callbacks. Returns the final result together with response controls.

#
Session::search_to_array

async fn[T : LdapTransport] Session::search_to_array(self : Session[T], request : SearchRequest) -> Result[SearchOutcome, LdapError]

Execute a search and collect all entries, references and the final result.

#
Session::set_retry

fn[T] Session::set_retry(self : Session[T], policy : RetryPolicy) -> Unit

Configure retry behaviour for all subsequent operations on this session.

#
Session::start_tls

async fn[T : LdapTransport] Session::start_tls(self : Session[T]) -> Result[ExtendedResponse, LdapError]

Issue the StartTLS extended operation (RFC 2830). On success the session is marked as having a TLS-protected channel so that require_tls configurations allow subsequent binds.

Note: this method performs the protocol negotiation; installing a real TLS transport on top of an existing socket is the responsibility of the native transport adapter (hbYlj/moonldap/socket).

#
Session::unbind

async fn[T : LdapTransport] Session::unbind(self : Session[T]) -> Result[Unit, LdapError]

Send an unbind request and close the transport. The server does not reply to unbind (RFC 4511 4.3).

#
SortKey

pub struct SortKey {
attribute_type : String
ordering_rule : String?
reverse : Bool
} derive(Eq,
Debug
)

One sort key of the server-side sort request control (RFC 2891).

#
SortKey::new

fn SortKey::new(attribute_type : String, ordering_rule? : String, reverse? : Bool) -> SortKey

#
SortResponse

pub struct SortResponse {
result : ResultCode
attribute_type : String?
} derive(Eq,
Debug
)

#
SubstringFilter

pub struct SubstringFilter {
initial : Bytes?
any : Array[Bytes]
final_ : Bytes?
} derive(Eq,
Debug
)

A substring filter's three segment kinds (RFC 4515 3).

#
SubstringFilter::new

fn SubstringFilter::new(initial : Bytes?, any : Array[Bytes], final_ : Bytes?) -> SubstringFilter

#
TagClass

pub(all) enum TagClass {
Universal
Application
ContextSpecific
Private
} derive(Eq,
Debug
)

ASN.1 tag class (X.690 8.1.2.2).

#
TagClass::from_bits

fn TagClass::from_bits(bits : Int) -> TagClass

#
TagClass::to_bits

fn TagClass::to_bits(self : TagClass) -> Int

#
PAGED_RESULTS_OID

let PAGED_RESULTS_OID : String

The paged results control OID (RFC 2696).

#
SORT_REQUEST_OID

let SORT_REQUEST_OID : String

The server-side sorting request control OID (RFC 2891 1.1).

#
SORT_RESPONSE_OID

let SORT_RESPONSE_OID : String

The server-side sorting response control OID (RFC 2891 1.2).

#
STARTTLS_OID

let STARTTLS_OID : String

The StartTLS extended operation OID (RFC 4511 4.14, RFC 2830).

#
application_tag

fn application_tag(number : Int, constructed : Bool) -> BerTag

Short helper for constructing application tags.

#
compute_digest_md5_response

fn compute_digest_md5_response(username~ : String, password~ : String, realm~ : String, nonce~ : String, cnonce~ : String, nc~ : String, qop~ : String, digest_uri~ : String) -> String

Compute the DIGEST-MD5 response directive string (RFC 2831 2.1.2.1) with qop=auth. The returned string is the credentials value sent back to the server in the second bind.

#
context_tag

fn context_tag(number : Int, constructed : Bool) -> BerTag

Short helper for constructing context-specific tags.

#
decode_ber

fn decode_ber(data : Bytes, limits : BerLimits?) -> Result[BerValue, BerError]

Decode exactly one top-level BER value. Any trailing octets are reported as NotSingleValue.

#
decode_ber_many

fn decode_ber_many(data : Bytes, limits : BerLimits?) -> Result[Array[BerValue], BerError]

Decode all BER values in the buffer until it is fully consumed. Used to decode LDAP message streams.

#
decode_boolean

fn decode_boolean(value : BerValue) -> Result[Bool, BerError]

Decode a BER BOOLEAN value.

#
decode_filter

fn decode_filter(bytes : Bytes, limits : BerLimits?) -> Result[LdapFilter, LdapError]

Decode a filter from its BER wire form.

#
decode_integer

fn decode_integer(value : BerValue, limits : BerLimits) -> Result[Int, BerError]

Decode a BER INTEGER value, checking the tag is INTEGER and the width respects limits.

#
decode_message

fn decode_message(bytes : Bytes, limits : BerLimits?) -> Result[LdapMessage, LdapError]

Decode a full LDAPMessage from its BER wire form.

#
decode_oid

fn decode_oid(value : BerValue) -> Result[String, BerError]

Decode a BER OID value.

#
describe_op

fn describe_op(op : ProtocolOp) -> String

A safe, secret-free one-line summary of a protocol op, used by CLI tooling and diagnostic output. Passwords and SASL payloads are never included.

#
encode_bit_string

fn encode_bit_string(data : Bytes, unused_bits : Int) -> Bytes

Encode a BIT STRING (X.690 8.6). The first content octet is the number of unused trailing bits.

#
encode_boolean

fn encode_boolean(b : Bool) -> Bytes

Encode a BER BOOLEAN (X.690 8.2). false encodes as 0x01 0x01 0x00.

#
encode_enumerated

fn encode_enumerated(v : Int) -> Bytes

Encode a BER ENUMERATED (X.690 8.4), reusing the INTEGER encoding rules.

#
encode_filter

fn encode_filter(f : LdapFilter) -> Bytes

Encode a filter into its BER wire form (RFC 4511 4.5.1.7).

#
encode_ia5_string

fn encode_ia5_string(s : String) -> Bytes

Encode an IA5String (X.690 8.22).

#
encode_identifier

fn encode_identifier(tag : BerTag) -> Bytes

Encode the identifier octets for a tag, including the long form when the tag number is 31 or greater (X.690 8.1.2.4).

#
encode_integer

fn encode_integer(v : Int) -> Bytes

Encode a BER INTEGER with the minimal two's-complement representation (X.690 8.3). Negative values get a leading 0xFF padding byte when the high bit would otherwise be clear.

#
encode_length

fn encode_length(len : Int) -> Bytes

Encode a definite length. Short form is used for len < 128, long form otherwise (X.690 8.1.3.4 / 8.1.3.5). Encoding is always definite.

#
encode_message

fn encode_message(msg : LdapMessage) -> Result[Bytes, LdapError]

Encode a full LDAPMessage to its BER wire form.

#
encode_null

fn encode_null() -> Bytes

Encode a BER NULL (X.690 8.8): 0x05 0x00.

#
encode_octet_string

fn encode_octet_string(content : Bytes) -> Bytes

Encode a BER OCTET STRING (X.690 8.7).

#
encode_oid

fn encode_oid(oid : String) -> Result[Bytes, BerError]

Encode a BER OID from its dotted-decimal string form (X.690 8.19).

#
encode_sequence

fn encode_sequence(items : Array[Bytes]) -> Bytes

Encode a constructed SEQUENCE (X.690 8.9).

#
encode_set

fn encode_set(items : Array[Bytes]) -> Bytes

Encode a constructed SET (X.690 8.11).

#
encode_tlv

fn encode_tlv(tag : BerTag, content : Bytes) -> Bytes

Encode a full TLV (tag + length + content).

#
encode_utf8_string

fn encode_utf8_string(s : String) -> Bytes

Encode a UTF8String (X.690 8.21).

#
escape_dn_value

fn escape_dn_value(value : String) -> String

Escape a DN attribute value for serialization. Escapes the special characters , + " \ < > ; =, NUL and leading #, plus leading and trailing spaces, using the \XX hex form. Runs of non-special bytes are decoded as UTF-8 so multi-byte characters survive the round trip.

#
escape_filter_value

fn escape_filter_value(value : Bytes) -> String

Escape a raw assertion value for inclusion in a filter string. Escapes *, (, ), \ and NUL using the \XX form. Runs of non-special bytes are decoded as UTF-8 so multi-byte characters survive the round trip.

#
from_hex

fn from_hex(s : String) -> Result[Bytes, String]

Decode an even-length hexadecimal string into bytes.

#
integer_content

fn integer_content(v : Int) -> Bytes

#
md5_hex

fn md5_hex(data : Bytes) -> String

Compute the MD5 digest of data as a lowercase hex string.

#
paged_results_control

fn paged_results_control(size : Int, cookie : Bytes) -> Control

Build the paged results request control with the given page size and cookie (empty cookie starts the enumeration).

#
paged_results_request_value

fn paged_results_request_value(size : Int, cookie : Bytes) -> Bytes

RealSearchControlValue ::= SEQUENCE { size INTEGER, cookie OCTET STRING } for a paged results request control value (RFC 2696).

#
paged_results_response_value

fn paged_results_response_value(total_size : Int, cookie : Bytes) -> Bytes

The paged results response control value: an estimated total size plus the server-issued continuation cookie.

#
parse_boolean

fn parse_boolean(content : Bytes) -> Result[Bool, BerError]

Parse the content octets of a BOOLEAN value (X.690 8.2.1: false is all zeros; any non-zero octet is true).

#
parse_digest_challenge

fn parse_digest_challenge(data : Bytes) -> Result[DigestChallenge, LdapError]

Parse the BASE64-decoded challenge into directives. Fails when no nonce is present (RFC 2831 requires it).

#
parse_dn

fn parse_dn(s : String) -> Result[Dn, LdapError]

Parse an RFC 4514 DN string.

#
parse_filter

fn parse_filter(s : String) -> Result[LdapFilter, LdapError]

Parse an RFC 4515 filter string into a structured LdapFilter.

#
parse_integer

fn parse_integer(content : Bytes, max_bytes : Int) -> Result[Int, BerError]

Parse the content octets of an INTEGER value into a signed Int, rejecting integers wider than max_bytes.

#
parse_ldap_url

fn parse_ldap_url(url : String) -> Result[LdapUrl, LdapError]

Parse an LDAP URL string per RFC 4516.

#
parse_oid

fn parse_oid(content : Bytes) -> Result[String, BerError]

Parse the content octets of an OID into its dotted-decimal form (X.690 8.19).

#
parse_paged_results_response

fn parse_paged_results_response(controls : Array[Control]?) -> PagedResultsResponse?

Extract the paged results response control from a response's controls, returning None when absent or malformed.

#
parse_sort_response

fn parse_sort_response(controls : Array[Control]?) -> SortResponse?

Extract the server-side sort response control, returning None when absent or malformed.

#
plain_sasl_credentials

fn plain_sasl_credentials(authzid : String, authcid : String, password : String) -> SaslCredentials

SASL PLAIN credentials: authzid \0 authcid \0 password (RFC 4616).

#
premature_close_error

fn premature_close_error() -> LdapError

#
protocol_op_name

fn protocol_op_name(op : ProtocolOp) -> String

#
protocol_op_tag

fn protocol_op_tag(op : ProtocolOp) -> Int

Application tag numbers for each protocol op (RFC 4511 4.1.1.2).

#
redact_password

fn redact_password(_password : String) -> String

Redact a bind password regardless of length.

#
redact_sasl_credentials

fn redact_sasl_credentials(mechanism : String, payload : Bytes) -> String

Mask a SASL mechanism credentials description for safe display.

#
redact_sasl_payload

fn redact_sasl_payload(payload : Bytes) -> Bytes

Redact a SASL payload by returning a constant placeholder. The original payload bytes never reach logs or traces.

#
redact_secret

fn redact_secret(value : String) -> String

Replace a secret with a fixed placeholder so it never appears in logs.

#
result_of_ber

fn[T] result_of_ber(f : () -> T raise BerError) -> Result[T, BerError]

Run a raising function that raises BerError and convert the outcome to a Result.

#
result_of_ldap

fn[T] result_of_ldap(f : () -> T raise LdapError) -> Result[T, LdapError]

Run a raising function that raises LdapError, converting the outcome to a Result.

#
result_of_unit_ber

fn result_of_unit_ber(f : () -> Unit raise BerError) -> Result[Unit, BerError]

Run a raising Unit function that raises BerError, converting the outcome to a Result.

#
reverse_array

fn[T] reverse_array(arr : Array[T]) -> Array[T]

Return a new array with the elements reversed.

#
sasl_external_bind_request

fn sasl_external_bind_request(authzid : String) -> BindRequest

Build an EXTERNAL SASL bind request (RFC 4422 appendix A / RFC 4616 style): the authzid travels as the SASL credentials; authentication is derived from the transport layer.

#
sasl_plain_bind_request

fn sasl_plain_bind_request(name : String, authzid : String, authcid : String, password : String, version? : Int) -> BindRequest

Build a SASL PLAIN bind request (RFC 4616).

#
simple_bind_request

fn simple_bind_request(name : String, password : String, version? : Int) -> BindRequest

Build a simple (cleartext password) bind request (RFC 4513 5.2.1).

#
sort_request_control

fn sort_request_control(keys : Array[SortKey]) -> Control

Build the server-side sort request control for a key list.

#
sort_response_value

fn sort_response_value(sort_result : Int, attribute_type : String?) -> Bytes

Encode a sort response control value (used by tests and mock servers).

#
strict_limits

fn strict_limits() -> BerLimits

A tight limit set used by tests and defensive callers.

#
to_hex_lower

fn to_hex_lower(data : BytesView) -> String

Lowercase hex encoding of a byte view.

#
to_hex_upper

fn to_hex_upper(data : BytesView) -> String

Uppercase hex encoding of a byte view.

#
transport_error

fn transport_error(msg : String) -> LdapError

#
unescape_dn_value

fn unescape_dn_value(s : String) -> Result[String, LdapError]

Unescape an RFC 4514 attribute value.

#
unescape_filter_value

fn unescape_filter_value(s : String) -> Result[Bytes, LdapError]

Unescape an RFC 4515 assertion value: \XX becomes the byte XX.

#
universal_tag

fn universal_tag(number : Int, constructed : Bool) -> BerTag

Short helper for constructing universal tags.