Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
use FreeDSx\Ldap\Server\Backend\Storage\Config\InMemoryStorageConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Config\JsonStorageConfig;
use FreeDSx\Ldap\Server\Backend\Storage\EntryStorageInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Export\DirectoryDumper;
use FreeDSx\Ldap\Server\Backend\Write\WriteRequestReplayer;
use FreeDSx\Ldap\Server\Backend\Storage\FilterEvaluator;
use FreeDSx\Ldap\Server\Backend\Storage\FilterEvaluatorInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Journal\Audit\AuditingChangeJournal;
Expand Down Expand Up @@ -99,11 +101,13 @@ public function factories(): array
PasswordAuthenticatableInterface::class => $this->makePasswordAuthenticator(...),
FilterEvaluatorInterface::class => $this->makeFilterEvaluator(...),
EntryStorageInterface::class => $this->makeStorage(...),
DirectoryDumper::class => $this->makeDirectoryDumper(...),
OperationalAttributeGenerator::class => $this->makeOperationalAttributeGenerator(...),
SearchStreamBuilder::class => $this->makeSearchStreamBuilder(...),
LdapImporter::class => $this->makeLdapImporter(...),
PdoBackendBuilder::class => $this->makePdoBackendBuilder(...),
WritableStorageBackend::class => $this->makeBackend(...),
WriteRequestReplayer::class => $this->makeWriteRequestReplayer(...),
ServerProtocolFactory::class => $this->makeServerProtocolFactory(...),
ServerProtocolFactoryInterface::class => static fn(Container $c): ServerProtocolFactoryInterface => $c->get(ServerProtocolFactory::class),
ServerProtocolHandlerFactory::class => $this->makeServerProtocolHandlerFactory(...),
Expand Down Expand Up @@ -166,6 +170,22 @@ private function makeFilterEvaluator(Container $container): FilterEvaluator
return new FilterEvaluator($container->get(ServerOptions::class)->getSchema());
}

private function makeWriteRequestReplayer(Container $container): WriteRequestReplayer
{
return new WriteRequestReplayer($container->get(WritableStorageBackend::class));
}

private function makeDirectoryDumper(Container $container): DirectoryDumper
{
$storage = $container->get(EntryStorageInterface::class);

return new DirectoryDumper(
$storage,
$storage->namingContexts(),
$container->get(FilterEvaluatorInterface::class),
);
}

/**
* Build the runner-appropriate storage backend from the configured StorageConfigInterface.
*/
Expand Down Expand Up @@ -290,6 +310,7 @@ private function makeBackend(Container $container): WritableStorageBackend
$options->getSchema(),
$options->makeSearchLimits(),
),
filterEvaluator: $container->get(FilterEvaluatorInterface::class),
operationalAttrs: $container->get(OperationalAttributeGenerator::class),
changeRecorder: $this->changeRecorderFor($container, $storage),
);
Expand Down
19 changes: 11 additions & 8 deletions src/FreeDSx/Ldap/Entry/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,20 @@ public function last(): ?Option
*/
public function toString(bool $sortedlc = false): string
{
$opts = $this->options;
if ($sortedlc) {
sort($opts);
}
// Sorting the rendered options rather than the objects, so ordering follows the text and not their internals.
$options = array_map(
static fn(Option $option): string => $option->toString($sortedlc),
$this->options,
);

$options = '';
foreach ($opts as $option) {
$options .= ($options === '') ? $option->toString($sortedlc) : ';' . $option->toString($sortedlc);
if ($sortedlc) {
sort($options);
}

return $options;
return implode(
';',
$options,
);
}

/**
Expand Down
25 changes: 4 additions & 21 deletions src/FreeDSx/Ldap/LdapServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@
use FreeDSx\Ldap\Ldif\Loader\LdifLoaderInterface;
use FreeDSx\Ldap\Ldif\Output\LdifOutputInterface;
use FreeDSx\Ldap\Operation\Request\AddRequest;
use FreeDSx\Ldap\Server\Backend\Storage\EntryStorageInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Export\DirectoryDumper;
use FreeDSx\Ldap\Server\Backend\Storage\Export\DumpOptions;
use FreeDSx\Ldap\Server\Backend\Storage\FilterEvaluatorInterface;
use FreeDSx\Ldap\Server\Backend\Storage\LdapImporter;
use FreeDSx\Ldap\Server\Backend\Storage\WritableStorageBackend;
use FreeDSx\Ldap\Server\Backend\Write\WriteRequestReplayer;
use FreeDSx\Ldap\Server\ServerRunner\ServerRunnerInterface;
use FreeDSx\Socket\Exception\ConnectionException;
Expand Down Expand Up @@ -105,9 +102,7 @@ public function seed(
*/
public function applyChanges(LdifLoaderInterface $loader): self
{
$backend = $this->backend();

(new WriteRequestReplayer($backend))
$this->container->get(WriteRequestReplayer::class)
->apply((new LdifParser())->parse($loader));

return $this;
Expand All @@ -123,25 +118,13 @@ public function dump(
LdifOutputInterface $output,
DumpOptions $options = new DumpOptions(),
): self {
$storage = $this->container->get(EntryStorageInterface::class);

$output->write((new DirectoryDumper(
$storage,
$storage->namingContexts(),
$this->container->get(FilterEvaluatorInterface::class),
))->dump($options));
$output->write(
$this->container->get(DirectoryDumper::class)->dump($options),
);

return $this;
}

/**
* The assembled storage backend from the container.
*/
private function backend(): WritableStorageBackend
{
return $this->container->get(WritableStorageBackend::class);
}

/**
* @return Generator<Entry>
* @throws RuntimeException when the LDIF contains a non-add change record
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,29 +83,45 @@ public static function forRequest(

public function project(Entry $entry): Entry
{
$attributes = $entry->getAttributes();
$filteredAttributes = [];

if (!$this->returnNone) {
foreach ($attributes as $attribute) {
if (!$this->shouldInclude($attribute)) {
continue;
}

$filteredAttributes[] = $this->typesOnly
? new Attribute($attribute->getName())
: $attribute;
}
if ($this->returnNone) {
return Entry::raw(
$entry->getDn(),
[],
);
}

$attributes = $entry->getAttributes();
$selected = array_filter(
$attributes,
$this->shouldInclude(...),
);

// Nothing was withheld, so the entry already is its own projection.
if (!$this->typesOnly && count($filteredAttributes) === count($attributes)) {
if (!$this->typesOnly && count($selected) === count($attributes)) {
return $entry;
}

return Entry::raw(
$entry->getDn(),
$filteredAttributes,
array_values(
$this->typesOnly
? $this->withoutValues($selected)
: $selected,
),
);
}

/**
* Options are part of the description a client asked for, so they survive a types-only request.
*
* @param array<Attribute> $attributes
* @return array<Attribute>
*/
private function withoutValues(array $attributes): array
{
return array_map(
static fn(Attribute $attribute): Attribute => new Attribute($attribute->getDescription()),
$attributes,
);
}

Expand All @@ -121,11 +137,37 @@ private function decideInclude(Attribute $attribute): bool
return true;
}

if ($this->isNamedType($attribute)) {
return true;
}

return $this->isOperational($attribute)
? $this->wantsOperational
: $this->wantsUser;
}

/**
* Whether a requested name asks for this attribute: its own type, or one it descends from.
*
* A type may be named by any of its names or its OID, and naming it asks for the values held under its options
* and its subtypes too (RFC 4511 4.5.1.8, RFC 4512 2.5.2).
*/
private function isNamedType(Attribute $attribute): bool
{
$type = Attribute::normalizeName($attribute->getDescription());

foreach ($this->names as $name) {
if ($name === $type) {
return true;
}
if ($this->schema->isTypeOrSubtypeOf($type, $name)) {
return true;
}
}

return false;
}

private function isOperational(Attribute $attribute): bool
{
$key = strtolower($attribute->getName());
Expand Down
52 changes: 50 additions & 2 deletions src/FreeDSx/Ldap/Schema/Matching/Comparator/IntegerComparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,25 @@ public function equals(
string $a,
string $b,
): bool {
return (int) $a === (int) $b;
$left = self::canonical($a);
$right = self::canonical($b);

return $left !== null
&& $left === $right;
}

public function compare(
string $a,
string $b,
): int {
return (int) $a <=> (int) $b;
$left = self::canonical($a);
$right = self::canonical($b);

if ($left === null || $right === null) {
return strcmp($a, $b);
}

return self::compareCanonical($left, $right);
}

public function substringMatches(
Expand All @@ -41,4 +52,41 @@ public function substringMatches(
): bool {
return false;
}

/**
* An integer reduced to one spelling, or null when the value is not one.
*
* Values can exceed the platform integer, so they stay as digits rather than being cast.
*/
private static function canonical(string $value): ?string
{
if (preg_match('/^[+-]?\d+$/', $value) !== 1) {
return null;
}

$isNegative = $value[0] === '-';
$digits = ltrim(ltrim($value, '+-'), '0');

if ($digits === '') {
return '0';
}

return ($isNegative ? '-' : '') . $digits;
}

private static function compareCanonical(
string $a,
string $b,
): int {
$aNegative = $a[0] === '-';
$bNegative = $b[0] === '-';

if ($aNegative !== $bNegative) {
return $aNegative ? -1 : 1;
}

$magnitude = strlen($a) <=> strlen($b) ?: strcmp($a, $b);

return $aNegative ? -$magnitude : $magnitude;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@
namespace FreeDSx\Ldap\Schema\Matching\Comparator;

use FreeDSx\Ldap\Schema\Matching\MatchingRuleComparatorInterface;
use FreeDSx\Ldap\Schema\Matching\StringPrep;
use FreeDSx\Ldap\Schema\Matching\SubstringAssertion;

/**
* Numeric string comparator treating spaces as insignificant (RFC 4517 section 4.2.22).
*/
final class NumericStringComparator implements MatchingRuleComparatorInterface
{
public function __construct(
private readonly StringPrep $prep = new StringPrep(),
) {}

public function equals(
string $a,
string $b,
Expand Down Expand Up @@ -54,12 +59,15 @@ public function substringMatches(
);
}

/**
* RFC 4518 2.6.2: every space is insignificant, so the profile's space handling is simply undone here.
*/
private function normalize(string $value): string
{
return str_replace(
' ',
'',
$value,
$this->prep->prepareForEquality($value),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,32 @@
namespace FreeDSx\Ldap\Schema\Matching\Comparator;

use FreeDSx\Ldap\Schema\Matching\MatchingRuleComparatorInterface;
use FreeDSx\Ldap\Schema\Matching\StringPrep;
use FreeDSx\Ldap\Schema\Matching\SubstringAssertion;

/**
* Telephone number comparator (telephoneNumberMatch): strips spaces and hyphens before comparing case-insensitively.
*/
final class TelephoneNumberComparator implements MatchingRuleComparatorInterface
{
/**
* The hyphens and the space RFC 4518 2.6.3 calls insignificant.
*/
private const INSIGNIFICANT = [
' ',
"\u{002D}",
"\u{058A}",
"\u{2010}",
"\u{2011}",
"\u{2212}",
"\u{FE63}",
"\u{FF0D}",
];

public function __construct(
private readonly StringPrep $prep = new StringPrep(),
) {}

public function equals(
string $a,
string $b,
Expand Down Expand Up @@ -55,12 +74,15 @@ public function substringMatches(
);
}

/**
* The preparation profile only rearranges spaces, which are removed here along with the hyphens.
*/
private function normalize(string $value): string
{
return strtolower(str_replace(
[' ', '-'],
return str_replace(
self::INSIGNIFICANT,
'',
$value,
));
$this->prep->prepareForEquality($value),
);
}
}
Loading
Loading