From a84d86b41a0e924874ba361433f6bf1df464b060 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Tue, 6 May 2014 09:34:56 +0200
Subject: [PATCH 01/32] Initial release: Protocol / stream implementation

---
 .../php/peer/ldap/util/BerStream.class.php    | 189 ++++++++++++++++++
 .../php/peer/ldap/util/LdapProtocol.class.php |  53 +++++
 2 files changed, 242 insertions(+)
 create mode 100755 src/main/php/peer/ldap/util/BerStream.class.php
 create mode 100755 src/main/php/peer/ldap/util/LdapProtocol.class.php

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
new file mode 100755
index 00000000..b4a82f88
--- /dev/null
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -0,0 +1,189 @@
+<?php namespace peer\ldap\util;
+
+class BerStream extends \lang\Object {
+  const EOC = 0;
+  const BOOLEAN = 1;
+  const INTEGER = 2;
+  const BITSTRING = 3;
+  const OCTETSTRING = 4;
+  const NULL = 5;
+  const OID = 6;
+  const OBJECTDESCRIPTOR = 7;
+  const EXTERNAL = 8;
+  const REAL = 9;
+  const ENUMERATION = 10;
+  const PDV = 11;
+  const UTF8STRING = 12;
+  const RELATIVEOID = 13;
+  const SEQUENCE = 16;
+  const SET = 17;
+  const NUMERICSTRING = 18;
+  const PRINTABLESTRING = 19;
+  const T61STRING = 20;
+  const VIDEOTEXSTRING = 21;
+  const IA5STRING = 22;
+  const UTCTIME = 23;
+  const GENERALIZEDTIME = 24;
+  const GRAPHICSTRING = 25;
+  const VISIBLESTRING = 26;
+  const GENERALSTRING = 28;
+  const UNIVERSALSTRING = 29;
+  const CHARACTERSTRING = 30;
+  const BMPSTRING = 31;
+  const CONSTRUCTOR = 32;
+  const CONTEXT = 128;
+
+  const SEQ_CTOR = 48;   // (SEQUENCE | CONSTRUCTOR)
+
+  protected $seq= [''];
+
+  public function __construct(\io\streams\InputStream $in, \io\streams\OutputStream $out) {
+    $this->in= $in;
+    $this->out= $out;
+  }
+
+  public function startSequence($tag= self::SEQ_CTOR) {
+    array_unshift($this->seq, '');
+    $this->writeByte($tag);
+  }
+
+  protected function encodeLength($l) {
+    if ($l <= 0x7f) {
+      return pack('C', $l);
+    } else if ($l <= 0xff) {
+      return "\x81".pack('C', $l);
+    } else if ($l <= 0xffff) {
+      return "\x82".pack('CC', $l >> 8, $l);
+    } else if ($l <= 0xffffff) {
+      return "\x83".pack('CCC', $l << 16, $l >> 8, $l);
+    } else {
+      throw new \lang\IllegalStateException('Length too long: '.$l);
+    }
+  }
+
+  public function write($raw) {
+    $this->seq[0].= $raw;
+  }
+
+  public function writeByte($b) {
+    $this->seq[0].= pack('C', $b);
+  }
+
+  public function writeLength($l) {
+    $this->seq[0].= $this->encodeLength($l);
+  }
+
+  public function writeInt($i, $tag= self::INTEGER) {
+    if ($i < -0xffffff || $i >= 0xffffff) {
+      $len= 4;
+    } else if ($i < -0xffff || $i >= 0xffff) {
+      $len= 3;
+    } else if ($i < -0xff || $i >= 0xff) {
+      $len= 2;
+    } else {
+      $len= 1;
+    }
+    $this->seq[0].= pack('CC', $tag, $len).substr(pack('N', $i), -$len);
+  }
+
+  public function writeNull() {
+    $this->seq[0].= pack('C', self::NULL)."\x00";
+  }
+
+  public function writeBoolean($b, $tag= self::BOOLEAN) {
+    $this->seq[0].= pack('C', $tag)."\x01".($b ? "\xff" : "\x00");
+  }
+
+  public function writeString($s, $tag= self::OCTETSTRING) {
+    $length= $this->encodeLength(strlen($s));
+    $this->seq[0].= pack('C', $tag).$length.$s;
+  }
+
+  public function writeEnumeration($e, $tag= self::ENUMERATION) {
+    $this->writeInt($e, $tag);
+  }
+
+  public function endSequence() {
+    $length= $this->encodeLength(strlen($this->seq[0]) - 1);
+    $seq= array_shift($this->seq);
+    $this->seq[0].= $seq{0}.$length.substr($seq, 1);
+  }
+
+  private function chars($bytes, $start, $offset) {
+    $s= '';
+    for ($j= $start; $j < min($offset, strlen($bytes)); $j++) {
+      $c= $bytes{$j};
+      $s.= ($c < "\x20" || $c > "\x7F" ? '.' : $c);
+    }
+    return $s;
+  }
+
+  private function dump($bytes, $message= null) {
+    $n= strlen($bytes);
+    $next= ' ';
+    if (null === $message) {
+      $s= '';
+      $p= 74;
+    } else {
+      $s= $message.' ';
+      $p= 74 - strlen($message) - 1;
+    }
+    $s.= str_pad('== ('.$n." bytes) ==\n", $p, '=', STR_PAD_LEFT);
+    $o= 0;
+    while ($o < $n) {
+      $s.= sprintf('%04x: ', $o);
+      for ($i= 0; $i < 16; $i++) {  
+        if ($i + $o >= $n) {
+          $s.= 7 === $i ? '    ' :  '   ';
+        } else {
+          $s.= sprintf('%02x %s', ord($bytes{$i + $o}), 7 === $i ? ' ' : '');
+        }
+      }
+      $o+= $i;
+      $s.= '|'.str_pad($this->chars($bytes, $o - 16, $o), 16, ' ', STR_PAD_RIGHT)."|\n";
+    }
+    return $s;
+  }
+
+  public function flush() {
+    \util\cmd\Console::writeLine($this->dump($this->seq[0], '>>>'));
+    $this->out->write($this->seq[0]);
+  }
+
+  public function readSequence($tag= self::SEQ_CTOR) {
+    $head= unpack('Ctag/Cl0/a3rest', $this->in->read(5));
+    if ($head['tag'] !== $tag) {
+      throw new \lang\IllegalStateException(sprintf('Expected %0x, have %0x', $tag, $head['tag']));
+    }
+
+    if ($head['l0'] <= 0x7f) {
+      $length= $head['l0'];
+      $s= $head['rest'];
+    } else if (0x81 === $head['l0']) {
+      $l= unpack('C', $head['rest']);
+      $length= $l[1];
+      $s= substr($head['rest'], -2);
+    } else if (0x82 === $head['l0']) {
+      $l= unpack('C2', $head['rest']);
+      $length= $l[1] * 0x100 + $l[2];
+      $s= substr($head['rest'], -1);
+    } else if (0x83 === $head['l0']) {
+      $l= unpack('C3', $head['rest']);
+      $length= $l[1] * 0x10000 + $l[2] * 0x100 + $l[3];
+      $s= '';
+    } else {
+      throw new \lang\IllegalStateException('Length too long: '.$head['l0']);
+    }
+
+    while (strlen($s) < $length) {
+      $s.= $this->in->read($length - strlen($s));
+    }
+    return $s;
+  }
+
+  public function read() {
+    $seq= $this->readSequence();
+    \util\cmd\Console::writeLine($this->dump($seq, '<<<'));
+    return new \lang\types\Bytes($seq);
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
new file mode 100755
index 00000000..af2c4821
--- /dev/null
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -0,0 +1,53 @@
+<?php namespace peer\ldap\util;
+
+class LdapProtocol extends \lang\Object {
+  const REQ_BIND = 0x60;
+  const REQ_UNBIND = 0x42;
+  const REQ_SEARCH = 0x63;
+  const REQ_MODIFY = 0x66;
+  const REQ_ADD = 0x68;
+  const REQ_DELETE = 0x4a;
+  const REQ_MODRDN = 0x6c;
+  const REQ_COMPARE = 0x6e;
+  const REQ_ABANDON = 0x50;
+  const REQ_EXTENSION = 0x77;
+
+  const SCOPE_BASE_OBJECT = 0;
+  const SCOPE_ONE_LEVEL   = 1;
+  const SCOPE_SUBTREE     = 2;
+
+  const NEVER_DEREF_ALIASES = 0;
+  const DEREF_IN_SEARCHING = 1;
+  const DEREF_BASE_OBJECT = 2;
+  const DEREF_ALWAYS = 3;
+
+  protected $messageId= 0;
+
+  public function __construct(\peer\Socket $sock) {
+    $this->stream= new BerStream(
+      $sock->getInputStream(),
+      $sock->getOutputStream()
+    );
+  }
+
+  protected function nextMessageId() {
+    if (++$this->messageId >= 0x7fffffff) {
+      $this->messageId= 1;
+    }
+    return $this->messageId;
+  }
+
+  public function send($request) {
+    $this->stream->startSequence();
+    $this->stream->writeInt($this->nextMessageId());
+
+    $this->stream->startSequence($request['op']);
+    call_user_func($request['write'], $this->stream);
+    $this->stream->endSequence();
+
+    $this->stream->endSequence();
+    $this->stream->flush();
+
+    return $this->stream->read();
+  }
+}
\ No newline at end of file

From 9c3e84d91f8b7c00f8e1279b943a4f462cb97022 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Tue, 6 May 2014 17:26:27 +0200
Subject: [PATCH 02/32] Utilize new BufferedInputStream::pushBack() See
 xp-framework/core#16

---
 .../php/peer/ldap/util/BerStream.class.php    | 229 +++++++++++++-----
 1 file changed, 167 insertions(+), 62 deletions(-)

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
index b4a82f88..ea38e847 100755
--- a/src/main/php/peer/ldap/util/BerStream.class.php
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -1,5 +1,9 @@
 <?php namespace peer\ldap\util;
 
+use io\streams\BufferedInputStream;
+use io\streams\InputStream;
+use io\streams\OutputStream;
+
 class BerStream extends \lang\Object {
   const EOC = 0;
   const BOOLEAN = 1;
@@ -35,18 +39,98 @@ class BerStream extends \lang\Object {
 
   const SEQ_CTOR = 48;   // (SEQUENCE | CONSTRUCTOR)
 
-  protected $seq= [''];
+  protected $write= [''];
+
+  /**
+   * Constructor
+   *
+   * @param  io.streams.InputStream $in
+   * @param  io.streams.OutputStream $out
+   */
+  public function __construct(InputStream $in, OutputStream $out) {
 
-  public function __construct(\io\streams\InputStream $in, \io\streams\OutputStream $out) {
-    $this->in= $in;
+    // Debug
+    $in= newinstance('io.streams.InputStream', [$in], [
+      'backing'     => null,
+      '__construct' => function($backing) { $this->backing= $backing; },
+      'read'        => function($length= 8192) {
+        $chunk= $this->backing->read($length);
+        if (null !== $chunk) {
+          \util\cmd\Console::writeLine(BerStream::dump($chunk, '<<<'));
+        }
+        return $chunk;
+      },
+      'available'   => function() { return $this->backing->available(); },
+      'close'       => function() { $this->backing->close(); }
+    ]);
+    $out= newinstance('io.streams.OutputStream', [$out], [
+      'backing'     => null,
+      '__construct' => function($backing) { $this->backing= $backing; },
+      'write'       => function($chunk) {
+        \util\cmd\Console::writeLine(BerStream::dump($chunk, '>>>'));
+        return $this->backing->write($chunk);
+      },
+      'flush'       => function() { return $this->backing->flush(); },
+      'close'       => function() { $this->backing->close(); }
+    ]);
+
+    $this->in= $in instanceof BufferedInputStream ? $in : new BufferedInputStream($in);
     $this->out= $out;
   }
 
+  private static function chars($bytes, $start, $offset) {
+    $s= '';
+    for ($j= $start; $j < min($offset, strlen($bytes)); $j++) {
+      $c= $bytes{$j};
+      $s.= ($c < "\x20" || $c > "\x7F" ? '.' : $c);
+    }
+    return $s;
+  }
+
+  public static function dump($bytes, $message= null) {
+    $n= strlen($bytes);
+    $next= ' ';
+    if (null === $message) {
+      $s= '';
+      $p= 74;
+    } else {
+      $s= $message.' ';
+      $p= 74 - strlen($message) - 1;
+    }
+    $s.= str_pad('== ('.$n." bytes) ==\n", $p, '=', STR_PAD_LEFT);
+    $o= 0;
+    while ($o < $n) {
+      $s.= sprintf('%04x: ', $o);
+      for ($i= 0; $i < 16; $i++) {  
+        if ($i + $o >= $n) {
+          $s.= 7 === $i ? '    ' :  '   ';
+        } else {
+          $s.= sprintf('%02x %s', ord($bytes{$i + $o}), 7 === $i ? ' ' : '');
+        }
+      }
+      $o+= $i;
+      $s.= '|'.str_pad(self::chars($bytes, $o - 16, $o), 16, ' ', STR_PAD_RIGHT)."|\n";
+    }
+    return $s;
+  }
+
+  /**
+   * Starts writing a sequence
+   *
+   * @param  int $tag
+   */
   public function startSequence($tag= self::SEQ_CTOR) {
-    array_unshift($this->seq, '');
+    array_unshift($this->write, '');
     $this->writeByte($tag);
   }
 
+  /**
+   * Encode length
+   *
+   * @param  int $l
+   * @return string encoded bytes
+   * @throws lang.IllegalStateException if length is > 0xffffff
+   */
   protected function encodeLength($l) {
     if ($l <= 0x7f) {
       return pack('C', $l);
@@ -61,18 +145,39 @@ protected function encodeLength($l) {
     }
   }
 
+  /**
+   * Write raw bytes to current sequence
+   *
+   * @param  string $raw
+   */
   public function write($raw) {
-    $this->seq[0].= $raw;
+    $this->write[0].= $raw;
   }
 
+  /**
+   * Write single byte to current sequence
+   *
+   * @param  int $b
+   */
   public function writeByte($b) {
-    $this->seq[0].= pack('C', $b);
+    $this->write[0].= pack('C', $b);
   }
 
+  /**
+   * Write length to current sequence
+   *
+   * @param  int $l
+   */
   public function writeLength($l) {
-    $this->seq[0].= $this->encodeLength($l);
+    $this->write[0].= $this->encodeLength($l);
   }
 
+  /**
+   * Write integer to current sequence
+   *
+   * @param  int $i
+   * @param  int $tag
+   */
   public function writeInt($i, $tag= self::INTEGER) {
     if ($i < -0xffffff || $i >= 0xffffff) {
       $len= 4;
@@ -83,73 +188,73 @@ public function writeInt($i, $tag= self::INTEGER) {
     } else {
       $len= 1;
     }
-    $this->seq[0].= pack('CC', $tag, $len).substr(pack('N', $i), -$len);
+    $this->write[0].= pack('CC', $tag, $len).substr(pack('N', $i), -$len);
   }
 
+  /**
+   * Write NULL value to current sequence
+   *
+   * @param  string $raw
+   */
   public function writeNull() {
-    $this->seq[0].= pack('C', self::NULL)."\x00";
+    $this->write[0].= pack('C', self::NULL)."\x00";
   }
 
+  /**
+   * Write boolean to current sequence
+   *
+   * @param  bool $b
+   * @param  int $tag
+   */
   public function writeBoolean($b, $tag= self::BOOLEAN) {
-    $this->seq[0].= pack('C', $tag)."\x01".($b ? "\xff" : "\x00");
+    $this->write[0].= pack('C', $tag)."\x01".($b ? "\xff" : "\x00");
   }
 
+  /**
+   * Write string to current sequence
+   *
+   * @param  string $s
+   * @param  int $tag
+   */
   public function writeString($s, $tag= self::OCTETSTRING) {
     $length= $this->encodeLength(strlen($s));
-    $this->seq[0].= pack('C', $tag).$length.$s;
+    $this->write[0].= pack('C', $tag).$length.$s;
   }
 
+  /**
+   * Write enumeration to current sequence
+   *
+   * @param  int $e
+   * @param  int $tag
+   */
   public function writeEnumeration($e, $tag= self::ENUMERATION) {
     $this->writeInt($e, $tag);
   }
 
+  /**
+   * Ends current sequences
+   */
   public function endSequence() {
-    $length= $this->encodeLength(strlen($this->seq[0]) - 1);
-    $seq= array_shift($this->seq);
-    $this->seq[0].= $seq{0}.$length.substr($seq, 1);
-  }
-
-  private function chars($bytes, $start, $offset) {
-    $s= '';
-    for ($j= $start; $j < min($offset, strlen($bytes)); $j++) {
-      $c= $bytes{$j};
-      $s.= ($c < "\x20" || $c > "\x7F" ? '.' : $c);
-    }
-    return $s;
-  }
-
-  private function dump($bytes, $message= null) {
-    $n= strlen($bytes);
-    $next= ' ';
-    if (null === $message) {
-      $s= '';
-      $p= 74;
-    } else {
-      $s= $message.' ';
-      $p= 74 - strlen($message) - 1;
-    }
-    $s.= str_pad('== ('.$n." bytes) ==\n", $p, '=', STR_PAD_LEFT);
-    $o= 0;
-    while ($o < $n) {
-      $s.= sprintf('%04x: ', $o);
-      for ($i= 0; $i < 16; $i++) {  
-        if ($i + $o >= $n) {
-          $s.= 7 === $i ? '    ' :  '   ';
-        } else {
-          $s.= sprintf('%02x %s', ord($bytes{$i + $o}), 7 === $i ? ' ' : '');
-        }
-      }
-      $o+= $i;
-      $s.= '|'.str_pad($this->chars($bytes, $o - 16, $o), 16, ' ', STR_PAD_RIGHT)."|\n";
-    }
-    return $s;
+    $length= $this->encodeLength(strlen($this->write[0]) - 1);
+    $seq= array_shift($this->write);
+    $this->write[0].= $seq{0}.$length.substr($seq, 1);
   }
 
+  /**
+   * Flushes all sequences to output
+   *
+   * @return int number of bytes written
+   */
   public function flush() {
-    \util\cmd\Console::writeLine($this->dump($this->seq[0], '>>>'));
-    $this->out->write($this->seq[0]);
+    return $this->out->write($this->write[0]);
   }
 
+  /**
+   * Read sequence
+   *
+   * @param  string $tag expected tag
+   * @return var
+   */
   public function readSequence($tag= self::SEQ_CTOR) {
     $head= unpack('Ctag/Cl0/a3rest', $this->in->read(5));
     if ($head['tag'] !== $tag) {
@@ -158,32 +263,32 @@ public function readSequence($tag= self::SEQ_CTOR) {
 
     if ($head['l0'] <= 0x7f) {
       $length= $head['l0'];
-      $s= $head['rest'];
+      $this->in->pushBack(substr($head['rest']));
     } else if (0x81 === $head['l0']) {
       $l= unpack('C', $head['rest']);
       $length= $l[1];
-      $s= substr($head['rest'], -2);
+      $this->in->pushBack(substr($head['rest'], 1));
     } else if (0x82 === $head['l0']) {
       $l= unpack('C2', $head['rest']);
       $length= $l[1] * 0x100 + $l[2];
-      $s= substr($head['rest'], -1);
+      $this->in->pushBack(substr($head['rest'], 2));
     } else if (0x83 === $head['l0']) {
       $l= unpack('C3', $head['rest']);
       $length= $l[1] * 0x10000 + $l[2] * 0x100 + $l[3];
-      $s= '';
     } else {
       throw new \lang\IllegalStateException('Length too long: '.$head['l0']);
     }
 
-    while (strlen($s) < $length) {
-      $s.= $this->in->read($length - strlen($s));
-    }
-    return $s;
+    return ['tag' => $tag, 'length' => $length];
   }
 
+  /**
+   * Read response
+   *
+   * @return var
+   */
   public function read() {
     $seq= $this->readSequence();
-    \util\cmd\Console::writeLine($this->dump($seq, '<<<'));
-    return new \lang\types\Bytes($seq);
+    return $seq;
   }
 }
\ No newline at end of file

From 8e18258eb4de94ef3947c1579b504ab77cb9a33f Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Tue, 6 May 2014 18:49:35 +0200
Subject: [PATCH 03/32] Add badges: XP, BSD licence, PHP 5.4+

---
 README.md | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index dfcd30c2..b9b3e5b1 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,10 @@
-LDAP protocol support for the XP Framework
+LDAP support for the XP Framework
 ========================================================================
 
+[![XP Framework Mdodule](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core)
+[![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md)
+[![Required PHP 5.4+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-5_4plus.png)](http://php.net/)
+
 The peer.ldap package implements LDAP (Lighweight Directory Access 
 Protocol) access.
 

From 07cebb06fa883c32b8b2e371b3abf58505e63e3a Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Tue, 6 May 2014 21:38:15 +0200
Subject: [PATCH 04/32] First working reader

---
 .../php/peer/ldap/util/BerStream.class.php    | 133 +++++++++++++++---
 .../php/peer/ldap/util/LdapProtocol.class.php |  90 +++++++++++-
 2 files changed, 194 insertions(+), 29 deletions(-)

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
index ea38e847..8fbe2732 100755
--- a/src/main/php/peer/ldap/util/BerStream.class.php
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -40,6 +40,7 @@ class BerStream extends \lang\Object {
   const SEQ_CTOR = 48;   // (SEQUENCE | CONSTRUCTOR)
 
   protected $write= [''];
+  protected $read= [0];
 
   /**
    * Constructor
@@ -246,49 +247,135 @@ public function endSequence() {
    * @return int number of bytes written
    */
   public function flush() {
-    return $this->out->write($this->write[0]);
+    $w= $this->out->write($this->write[0]);
+    $this->write= [''];
+    return $w;
   }
 
-  /**
-   * Read sequence
-   *
-   * @param  string $tag expected tag
-   * @return var
-   */
-  public function readSequence($tag= self::SEQ_CTOR) {
-    $head= unpack('Ctag/Cl0/a3rest', $this->in->read(5));
-    if ($head['tag'] !== $tag) {
-      throw new \lang\IllegalStateException(sprintf('Expected %0x, have %0x', $tag, $head['tag']));
+  public function read($l) {
+    $t= debug_backtrace();
+    $chunk= $this->in->read($l);
+    $this->read[0]-= strlen($chunk);
+    // fprintf(STDOUT, "%s   READ %d bytes from %s, remain %d\n", str_repeat('   ', sizeof($this->read)), $l, $t[1]['function'], $this->read[0]);
+    return $chunk;
+  }
+
+  public function readTag($expected) {
+    $head= unpack('Ctag', $this->in->read(1));
+    $test= (array)$expected;
+    if (!in_array($head['tag'], $test)) {
+      throw new \lang\IllegalStateException(sprintf(
+        'Expected any of [%s], have 0x%02x',
+        implode(', ', array_map(function($t) { return sprintf('0x%02x', $t); }, $test)),
+        $head['tag']
+      ));
     }
 
+    return $head['tag'];
+  }
+
+  protected function decodeLength() {
+    $head= unpack('Cl0', $this->in->read(1));
     if ($head['l0'] <= 0x7f) {
       $length= $head['l0'];
-      $this->in->pushBack(substr($head['rest']));
+      $this->read[0]-= 2;
     } else if (0x81 === $head['l0']) {
-      $l= unpack('C', $head['rest']);
+      $l= unpack('C', $this->in->read(1));
       $length= $l[1];
-      $this->in->pushBack(substr($head['rest'], 1));
+      $this->read[0]-= 3;
     } else if (0x82 === $head['l0']) {
-      $l= unpack('C2', $head['rest']);
+      $l= unpack('C2', $this->in->read(2));
       $length= $l[1] * 0x100 + $l[2];
-      $this->in->pushBack(substr($head['rest'], 2));
+      $this->read[0]-= 4;
     } else if (0x83 === $head['l0']) {
-      $l= unpack('C3', $head['rest']);
+      $l= unpack('C3', $this->in->read(3));
       $length= $l[1] * 0x10000 + $l[2] * 0x100 + $l[3];
+      $this->read[0]-= 5;
     } else {
       throw new \lang\IllegalStateException('Length too long: '.$head['l0']);
     }
+    return $length;
+  }
+
+  /**
+   * Reads an integer
+   *
+   * @return int
+   */
+  public function readInt() {
+    $this->readTag(self::INTEGER);
+    return unpack('N', str_pad($this->read($this->decodeLength()), 4, "\0", STR_PAD_LEFT))[1];
+  }
 
-    return ['tag' => $tag, 'length' => $length];
+  /**
+   * Reads an enumeration
+   *
+   * @return int
+   */
+  public function readEnumeration() {
+    $this->readTag(self::ENUMERATION);
+    return unpack('N', str_pad($this->read($this->decodeLength()), 4, "\0", STR_PAD_LEFT))[1];
+  }
+
+  /**
+   * Reads a string
+   *
+   * @return string
+   */
+  public function readString() {
+    $this->readTag(self::OCTETSTRING);
+    return $this->read($this->decodeLength());
+  }
+
+  /**
+   * Reads a boolean
+   *
+   * @return bool
+   */
+  public function readBoolean() {
+    $this->readTag(self::BOOLEAN);
+    return $this->read($this->decodeLength()) ? true : false;
+  }
+
+  /**
+   * Read sequence
+   *
+   * @param  var $tag expected either a tag or an array of tags
+   * @return int The found tag
+   */
+  public function readSequence($tag= self::SEQ_CTOR) {
+    $tag= $this->readTag($tag);
+    $len= $this->decodeLength();
+    $this->read[0]-= $len;
+    array_unshift($this->read, $len);
+    // fprintf(STDOUT, "%s`- BEGIN SEQ %d bytes\n", str_repeat('   ', sizeof($this->read)), $this->read[0]);
+    return $tag;
+  }
+
+  public function remaining() {
+    return $this->read[0];
+  }
+
+  public function available() {
+    return $this->in->available();
+  }
+
+  /**
+   * Finish reading a sequence
+   */
+  public function finishSequence() {
+    // fprintf(STDOUT, "%s   END SEQ remain: %d bytes\n", str_repeat('   ', sizeof($this->read)), $this->read[0]);
+    $shift= array_shift($this->read);
+    $this->read[0]-= $shift;
   }
 
   /**
-   * Read response
+   * Closes I/O
    *
-   * @return var
+   * @return void
    */
-  public function read() {
-    $seq= $this->readSequence();
-    return $seq;
+  public function close() {
+    $this->in->close();
+    $this->out->close();
   }
 }
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index af2c4821..bf1ac436 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -12,6 +12,17 @@ class LdapProtocol extends \lang\Object {
   const REQ_ABANDON = 0x50;
   const REQ_EXTENSION = 0x77;
 
+  const REP_BIND = 0x61;
+  const REP_SEARCH_ENTRY = 0x64;
+  const REP_SEARCH_REF = 0x73;
+  const REP_SEARCH = 0x65;
+  const REP_MODIFY = 0x67;
+  const REP_ADD = 0x69;
+  const REP_DELETE = 0x6b;
+  const REP_MODRDN = 0x6d;
+  const REP_COMPARE = 0x6f;
+  const REP_EXTENSION = 0x78;
+
   const SCOPE_BASE_OBJECT = 0;
   const SCOPE_ONE_LEVEL   = 1;
   const SCOPE_SUBTREE     = 2;
@@ -37,17 +48,84 @@ protected function nextMessageId() {
     return $this->messageId;
   }
 
-  public function send($request) {
+  public function send($message) {
     $this->stream->startSequence();
     $this->stream->writeInt($this->nextMessageId());
-
-    $this->stream->startSequence($request['op']);
-    call_user_func($request['write'], $this->stream);
+    call_user_func($message['write'], $this->stream);
     $this->stream->endSequence();
 
-    $this->stream->endSequence();
     $this->stream->flush();
 
-    return $this->stream->read();
+    return call_user_func($message['read'], $this->stream);
+  }
+
+  public function search() {
+    static $handlers= null;
+
+    if (!$handlers) $handlers= [
+      self::REP_SEARCH_ENTRY => function($stream) {
+        $name= $stream->readString();
+        $stream->readSequence();
+        $attributes= [];
+        do {
+          $stream->readSequence();
+          $attr= $stream->readString();
+
+          $stream->readSequence(0x31);
+          $attributes[$attr]= [];
+          do {
+            $attributes[$attr][]= $stream->readString();
+          } while ($stream->remaining());
+          $stream->finishSequence();
+
+          $stream->finishSequence();
+        } while ($stream->remaining());
+        $stream->finishSequence();
+        return ['name' => $name, 'attr' => $attributes];
+      },
+      self::REP_SEARCH => function($stream) {
+        $stream->read($stream->remaining());    // XXX FIXME
+        return '<EOR>';
+      }
+    ];
+
+    return $this->send([
+      'write' => function($stream) {
+        $stream->startSequence(self::REQ_SEARCH);
+        $stream->writeString('o=example');
+        $stream->writeEnumeration(0);
+        $stream->writeEnumeration(0);
+        $stream->writeInt(0);
+        $stream->writeInt(0);
+        $stream->writeBoolean(false);
+
+        $stream->startSequence(0x87);
+        $stream->write('objectClass');
+        $stream->endSequence();
+
+        $stream->startSequence();
+        $stream->endSequence();
+        $stream->endSequence();
+      },
+      'read'  => function($stream) use($handlers) {
+        $result= [];
+        do {
+          $stream->readSequence();
+          $messageId= $stream->readInt();
+
+          $tag= $stream->readSequence([self::REP_SEARCH_ENTRY, self::REP_SEARCH]);
+          $result[]= call_user_func($handlers[$tag], $stream);
+          $stream->finishSequence();
+
+          $stream->finishSequence();
+        } while (self::REP_SEARCH_ENTRY === $tag);
+
+        return $result;
+      }
+    ]);
+  }
+
+  public function __destruct() {
+    $this->stream->close();
   }
 }
\ No newline at end of file

From cf34029d99cf39daebe3bfa4c52f09d00c2719cc Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Tue, 6 May 2014 21:48:30 +0200
Subject: [PATCH 05/32] Move handling of multiple results into send()

---
 .../php/peer/ldap/util/LdapProtocol.class.php | 94 +++++++++----------
 1 file changed, 46 insertions(+), 48 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index bf1ac436..38434efb 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -32,6 +32,10 @@ class LdapProtocol extends \lang\Object {
   const DEREF_BASE_OBJECT = 2;
   const DEREF_ALWAYS = 3;
 
+  protected static $continue= [
+    self::REP_SEARCH_ENTRY => true
+  ];
+
   protected $messageId= 0;
 
   public function __construct(\peer\Socket $sock) {
@@ -49,49 +53,32 @@ protected function nextMessageId() {
   }
 
   public function send($message) {
-    $this->stream->startSequence();
-    $this->stream->writeInt($this->nextMessageId());
-    call_user_func($message['write'], $this->stream);
-    $this->stream->endSequence();
-
+    with ($this->stream->startSequence()); {
+      $this->stream->writeInt($this->nextMessageId());
+      $this->stream->startSequence($message['req']);
+      call_user_func($message['write'], $this->stream);
+      $this->stream->endSequence();
+      $this->stream->endSequence();
+    }
     $this->stream->flush();
 
-    return call_user_func($message['read'], $this->stream);
+    $result= [];
+    do {
+      with ($this->stream->readSequence()); {
+        $messageId= $this->stream->readInt();
+        $tag= $this->stream->readSequence($message['rep']);
+        $result[]= call_user_func($message['read'][$tag], $this->stream);
+        $this->stream->finishSequence();
+        $this->stream->finishSequence();
+      }
+    } while (isset(self::$continue[$tag]));
+    return $result;
   }
 
   public function search() {
-    static $handlers= null;
-
-    if (!$handlers) $handlers= [
-      self::REP_SEARCH_ENTRY => function($stream) {
-        $name= $stream->readString();
-        $stream->readSequence();
-        $attributes= [];
-        do {
-          $stream->readSequence();
-          $attr= $stream->readString();
-
-          $stream->readSequence(0x31);
-          $attributes[$attr]= [];
-          do {
-            $attributes[$attr][]= $stream->readString();
-          } while ($stream->remaining());
-          $stream->finishSequence();
-
-          $stream->finishSequence();
-        } while ($stream->remaining());
-        $stream->finishSequence();
-        return ['name' => $name, 'attr' => $attributes];
-      },
-      self::REP_SEARCH => function($stream) {
-        $stream->read($stream->remaining());    // XXX FIXME
-        return '<EOR>';
-      }
-    ];
-
     return $this->send([
+      'req'   => self::REQ_SEARCH,
       'write' => function($stream) {
-        $stream->startSequence(self::REQ_SEARCH);
         $stream->writeString('o=example');
         $stream->writeEnumeration(0);
         $stream->writeEnumeration(0);
@@ -105,23 +92,34 @@ public function search() {
 
         $stream->startSequence();
         $stream->endSequence();
-        $stream->endSequence();
       },
-      'read'  => function($stream) use($handlers) {
-        $result= [];
-        do {
+      'rep'   => [self::REP_SEARCH_ENTRY, self::REP_SEARCH],
+      'read'  => [
+        self::REP_SEARCH_ENTRY => function($stream) {
+          $name= $stream->readString();
           $stream->readSequence();
-          $messageId= $stream->readInt();
+          $attributes= [];
+          do {
+            $stream->readSequence();
+            $attr= $stream->readString();
 
-          $tag= $stream->readSequence([self::REP_SEARCH_ENTRY, self::REP_SEARCH]);
-          $result[]= call_user_func($handlers[$tag], $stream);
-          $stream->finishSequence();
+            $stream->readSequence(0x31);
+            $attributes[$attr]= [];
+            do {
+              $attributes[$attr][]= $stream->readString();
+            } while ($stream->remaining());
+            $stream->finishSequence();
 
+            $stream->finishSequence();
+          } while ($stream->remaining());
           $stream->finishSequence();
-        } while (self::REP_SEARCH_ENTRY === $tag);
-
-        return $result;
-      }
+          return ['name' => $name, 'attr' => $attributes];
+        },
+        self::REP_SEARCH => function($stream) {
+          $stream->read($stream->remaining());    // XXX FIXME
+          return '<EOR>';
+        }
+      ]
     ]);
   }
 

From e584c3a33384e9ad3588c99d28a6f8fd549d1813 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Tue, 6 May 2014 21:52:03 +0200
Subject: [PATCH 06/32] Make in and out members protected

---
 src/main/php/peer/ldap/util/BerStream.class.php | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
index 8fbe2732..e91f7862 100755
--- a/src/main/php/peer/ldap/util/BerStream.class.php
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -42,6 +42,8 @@ class BerStream extends \lang\Object {
   protected $write= [''];
   protected $read= [0];
 
+  protected $in, $out;
+
   /**
    * Constructor
    *

From 84ccf07bde0c41ec7c3d8b531c3afbe5716a8282 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Tue, 6 May 2014 22:09:09 +0200
Subject: [PATCH 07/32] Refactor: REP -> RES (better abbreviation for response)

---
 .../php/peer/ldap/util/LdapProtocol.class.php | 30 +++++++++----------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 38434efb..ae4fc2ed 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -12,16 +12,16 @@ class LdapProtocol extends \lang\Object {
   const REQ_ABANDON = 0x50;
   const REQ_EXTENSION = 0x77;
 
-  const REP_BIND = 0x61;
-  const REP_SEARCH_ENTRY = 0x64;
-  const REP_SEARCH_REF = 0x73;
-  const REP_SEARCH = 0x65;
-  const REP_MODIFY = 0x67;
-  const REP_ADD = 0x69;
-  const REP_DELETE = 0x6b;
-  const REP_MODRDN = 0x6d;
-  const REP_COMPARE = 0x6f;
-  const REP_EXTENSION = 0x78;
+  const RES_BIND = 0x61;
+  const RES_SEARCH_ENTRY = 0x64;
+  const RES_SEARCH_REF = 0x73;
+  const RES_SEARCH = 0x65;
+  const RES_MODIFY = 0x67;
+  const RES_ADD = 0x69;
+  const RES_DELETE = 0x6b;
+  const RES_MODRDN = 0x6d;
+  const RES_COMPARE = 0x6f;
+  const RES_EXTENSION = 0x78;
 
   const SCOPE_BASE_OBJECT = 0;
   const SCOPE_ONE_LEVEL   = 1;
@@ -33,7 +33,7 @@ class LdapProtocol extends \lang\Object {
   const DEREF_ALWAYS = 3;
 
   protected static $continue= [
-    self::REP_SEARCH_ENTRY => true
+    self::RES_SEARCH_ENTRY => true
   ];
 
   protected $messageId= 0;
@@ -66,7 +66,7 @@ public function send($message) {
     do {
       with ($this->stream->readSequence()); {
         $messageId= $this->stream->readInt();
-        $tag= $this->stream->readSequence($message['rep']);
+        $tag= $this->stream->readSequence($message['res']);
         $result[]= call_user_func($message['read'][$tag], $this->stream);
         $this->stream->finishSequence();
         $this->stream->finishSequence();
@@ -93,9 +93,9 @@ public function search() {
         $stream->startSequence();
         $stream->endSequence();
       },
-      'rep'   => [self::REP_SEARCH_ENTRY, self::REP_SEARCH],
+      'res'   => [self::RES_SEARCH_ENTRY, self::RES_SEARCH],
       'read'  => [
-        self::REP_SEARCH_ENTRY => function($stream) {
+        self::RES_SEARCH_ENTRY => function($stream) {
           $name= $stream->readString();
           $stream->readSequence();
           $attributes= [];
@@ -115,7 +115,7 @@ public function search() {
           $stream->finishSequence();
           return ['name' => $name, 'attr' => $attributes];
         },
-        self::REP_SEARCH => function($stream) {
+        self::RES_SEARCH => function($stream) {
           $stream->read($stream->remaining());    // XXX FIXME
           return '<EOR>';
         }

From 2cb998ac2e87997cc88c1e56088baf8aee1a6f40 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Tue, 6 May 2014 22:10:24 +0200
Subject: [PATCH 08/32] Make base passable to search() as parameter

---
 src/main/php/peer/ldap/util/LdapProtocol.class.php | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index ae4fc2ed..9304ca16 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -75,11 +75,11 @@ public function send($message) {
     return $result;
   }
 
-  public function search() {
+  public function search($base) {
     return $this->send([
       'req'   => self::REQ_SEARCH,
-      'write' => function($stream) {
-        $stream->writeString('o=example');
+      'write' => function($stream) use($base) {
+        $stream->writeString($base);
         $stream->writeEnumeration(0);
         $stream->writeEnumeration(0);
         $stream->writeInt(0);

From cd08bb230112b6f67a967855d17a041d14cbec73 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Wed, 7 May 2014 11:59:55 +0200
Subject: [PATCH 09/32] Fill in enums

---
 src/main/php/peer/ldap/util/LdapProtocol.class.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 9304ca16..769c5a5b 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -80,8 +80,8 @@ public function search($base) {
       'req'   => self::REQ_SEARCH,
       'write' => function($stream) use($base) {
         $stream->writeString($base);
-        $stream->writeEnumeration(0);
-        $stream->writeEnumeration(0);
+        $stream->writeEnumeration(self::SCOPE_ONE_LEVEL);
+        $stream->writeEnumeration(self::NEVER_DEREF_ALIASES);
         $stream->writeInt(0);
         $stream->writeInt(0);
         $stream->writeBoolean(false);

From dcf9de04e0258e316ce1bd1258b90c3b6be55d4d Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Thu, 8 May 2014 12:46:28 +0200
Subject: [PATCH 10/32] QA: Add api documentation to all methods

---
 .../php/peer/ldap/util/BerStream.class.php    |  2 ++
 .../php/peer/ldap/util/LdapProtocol.class.php | 22 +++++++++++++++++++
 2 files changed, 24 insertions(+)

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
index e91f7862..a9b23a55 100755
--- a/src/main/php/peer/ldap/util/BerStream.class.php
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -53,6 +53,7 @@ class BerStream extends \lang\Object {
   public function __construct(InputStream $in, OutputStream $out) {
 
     // Debug
+    /*
     $in= newinstance('io.streams.InputStream', [$in], [
       'backing'     => null,
       '__construct' => function($backing) { $this->backing= $backing; },
@@ -76,6 +77,7 @@ public function __construct(InputStream $in, OutputStream $out) {
       'flush'       => function() { return $this->backing->flush(); },
       'close'       => function() { $this->backing->close(); }
     ]);
+    */
 
     $this->in= $in instanceof BufferedInputStream ? $in : new BufferedInputStream($in);
     $this->out= $out;
diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 769c5a5b..5e6ff1bb 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -38,6 +38,11 @@ class LdapProtocol extends \lang\Object {
 
   protected $messageId= 0;
 
+  /**
+   * Creates a new protocol instance communicating on the given socket
+   *
+   * @param  peer.Socket $sock
+   */
   public function __construct(\peer\Socket $sock) {
     $this->stream= new BerStream(
       $sock->getInputStream(),
@@ -45,6 +50,11 @@ public function __construct(\peer\Socket $sock) {
     );
   }
 
+  /**
+   * Calculates and returns next message id, starting with 1.
+   *
+   * @return  int
+   */
   protected function nextMessageId() {
     if (++$this->messageId >= 0x7fffffff) {
       $this->messageId= 1;
@@ -52,6 +62,12 @@ protected function nextMessageId() {
     return $this->messageId;
   }
 
+  /**
+   * Send message, return result
+   *
+   * @param  var $message
+   * @return var
+   */
   public function send($message) {
     with ($this->stream->startSequence()); {
       $this->stream->writeInt($this->nextMessageId());
@@ -75,6 +91,12 @@ public function send($message) {
     return $result;
   }
 
+  /**
+   * Search
+   *
+   * @param  string $base
+   * @return var
+   */
   public function search($base) {
     return $this->send([
       'req'   => self::REQ_SEARCH,

From bc599462bad911f079c784202d08cccf360d9471 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Thu, 8 May 2014 13:04:44 +0200
Subject: [PATCH 11/32] Use larger default buffer for reading

---
 src/main/php/peer/ldap/util/BerStream.class.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
index a9b23a55..3c0b41dd 100755
--- a/src/main/php/peer/ldap/util/BerStream.class.php
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -79,7 +79,7 @@ public function __construct(InputStream $in, OutputStream $out) {
     ]);
     */
 
-    $this->in= $in instanceof BufferedInputStream ? $in : new BufferedInputStream($in);
+    $this->in= $in instanceof BufferedInputStream ? $in : new BufferedInputStream($in, 8192);
     $this->out= $out;
   }
 

From a6366971a132c1a88cd83727afeec02fa51a0d4d Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Thu, 8 May 2014 13:05:03 +0200
Subject: [PATCH 12/32] Implement bind operation

---
 .../php/peer/ldap/util/LdapProtocol.class.php | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 5e6ff1bb..379a351c 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -1,5 +1,7 @@
 <?php namespace peer\ldap\util;
 
+use peer\ldap\LDAPException;
+
 class LdapProtocol extends \lang\Object {
   const REQ_BIND = 0x60;
   const REQ_UNBIND = 0x42;
@@ -32,6 +34,8 @@ class LdapProtocol extends \lang\Object {
   const DEREF_BASE_OBJECT = 2;
   const DEREF_ALWAYS = 3;
 
+  const STATUS_OK = 0;
+
   protected static $continue= [
     self::RES_SEARCH_ENTRY => true
   ];
@@ -91,6 +95,38 @@ public function send($message) {
     return $result;
   }
 
+
+  /**
+   * Bind
+   *
+   * @param  string $user
+   * @param  string $password
+   */
+  public function bind($user, $password) {
+    $result= $this->send([
+      'req'   => self::REQ_BIND,
+      'write' => function($stream) use($user, $password) {
+        $stream->writeInt($version= 3);
+        $stream->writeString($user);
+        $stream->writeString($password, BerStream::CONTEXT);
+      },
+      'res'   => self::RES_BIND,
+      'read'  => [self::RES_BIND => function($stream) {
+        $status= $stream->readEnumeration();
+        $matchedDN= $stream->readString();
+        $error= $stream->readString();
+
+        // TODO: Referalls
+        $stream->read($stream->remaining());
+        return  ['status' => $status, 'matchedDN' => $matchedDN, 'error' => $error];
+      }]
+    ])[0];
+    \util\cmd\Console::writeLine($result);
+    if (self::STATUS_OK === $result['status']) return true;
+
+    throw new LDAPException($result['error'] ?: 'Bind error', $result['status']);
+  }
+
   /**
    * Search
    *

From fab460be3a08fea950c2a99d17552363494cf519 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Thu, 8 May 2014 16:50:04 +0200
Subject: [PATCH 13/32] Document bind() returns void, throws an LDAPException

---
 src/main/php/peer/ldap/util/LdapProtocol.class.php | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 379a351c..3440c785 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -101,6 +101,8 @@ public function send($message) {
    *
    * @param  string $user
    * @param  string $password
+   * @return void
+   * @throws peer.ldap.LDAPException
    */
   public function bind($user, $password) {
     $result= $this->send([
@@ -122,9 +124,9 @@ public function bind($user, $password) {
       }]
     ])[0];
     \util\cmd\Console::writeLine($result);
-    if (self::STATUS_OK === $result['status']) return true;
-
-    throw new LDAPException($result['error'] ?: 'Bind error', $result['status']);
+    if (self::STATUS_OK !== $result['status']) {
+      throw new LDAPException($result['error'] ?: 'Bind error', $result['status']);
+    }
   }
 
   /**

From 751ee120ef518adaaea268bba7aeeb23b11512ac Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Thu, 8 May 2014 16:50:23 +0200
Subject: [PATCH 14/32] QA: WS

---
 src/main/php/peer/ldap/util/LdapProtocol.class.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 3440c785..169e2973 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -95,7 +95,6 @@ public function send($message) {
     return $result;
   }
 
-
   /**
    * Bind
    *

From 1d2bb2eeceeaee4be2bb583b05aabfd69011780b Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Thu, 8 May 2014 18:16:22 +0200
Subject: [PATCH 15/32] Throw exceptions if search failed

---
 .../php/peer/ldap/util/LdapProtocol.class.php | 30 ++++++++++++-------
 1 file changed, 20 insertions(+), 10 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 169e2973..37155ffc 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -87,7 +87,14 @@ public function send($message) {
       with ($this->stream->readSequence()); {
         $messageId= $this->stream->readInt();
         $tag= $this->stream->readSequence($message['res']);
-        $result[]= call_user_func($message['read'][$tag], $this->stream);
+        try {
+          $result[]= call_user_func($message['read'][$tag], $this->stream);
+        } catch (\lang\XPException $e) {
+          $this->stream->finishSequence();
+          $this->stream->finishSequence();
+          $this->stream->read($this->stream->remaining());
+          throw $e;
+        }
         $this->stream->finishSequence();
         $this->stream->finishSequence();
       }
@@ -104,7 +111,7 @@ public function send($message) {
    * @throws peer.ldap.LDAPException
    */
   public function bind($user, $password) {
-    $result= $this->send([
+    $this->send([
       'req'   => self::REQ_BIND,
       'write' => function($stream) use($user, $password) {
         $stream->writeInt($version= 3);
@@ -119,13 +126,11 @@ public function bind($user, $password) {
 
         // TODO: Referalls
         $stream->read($stream->remaining());
-        return  ['status' => $status, 'matchedDN' => $matchedDN, 'error' => $error];
+        if (self::STATUS_OK !== $status) {
+          throw new LDAPException($error ?: 'Bind error', $status);
+        }
       }]
-    ])[0];
-    \util\cmd\Console::writeLine($result);
-    if (self::STATUS_OK !== $result['status']) {
-      throw new LDAPException($result['error'] ?: 'Bind error', $result['status']);
-    }
+    ]);
   }
 
   /**
@@ -175,8 +180,13 @@ public function search($base) {
           return ['name' => $name, 'attr' => $attributes];
         },
         self::RES_SEARCH => function($stream) {
-          $stream->read($stream->remaining());    // XXX FIXME
-          return '<EOR>';
+          $status= $stream->readEnumeration();
+          $matchedDN= $stream->readString();
+          $error= $stream->readString();
+
+          if (self::STATUS_OK !== $status) {
+            throw new LDAPException($error ?: 'Search failed', $status);
+          }
         }
       ]
     ]);

From 4207884619ff731a4fc286bdbd9373389dee0c74 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Thu, 8 May 2014 18:18:06 +0200
Subject: [PATCH 16/32] Add explicit close() method

---
 src/main/php/peer/ldap/util/LdapProtocol.class.php | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 37155ffc..600abd7e 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -192,7 +192,17 @@ public function search($base) {
     ]);
   }
 
-  public function __destruct() {
+  /**
+   * Closes the connection
+   */
+  public function close() {
     $this->stream->close();
   }
+
+  /**
+   * Destructor. Ensures stream is closed.
+   */
+  public function __destruct() {
+    $this->close();
+  }
 }
\ No newline at end of file

From 4370bf065baf0a434d516d3423fdafcaa7d33ceb Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Thu, 8 May 2014 18:27:01 +0200
Subject: [PATCH 17/32] Refactor: Extract response handling into helper method

---
 .../php/peer/ldap/util/LdapProtocol.class.php | 22 +++++++++++++------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 600abd7e..dda5a059 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -66,6 +66,18 @@ protected function nextMessageId() {
     return $this->messageId;
   }
 
+  /**
+   * Handle response
+   *
+   * @param  int $status
+   * @param  sting $error
+   */
+  protected function handleResponse($status, $error= null) {
+    if (self::STATUS_OK !== $status) {
+      throw new LDAPException($error, $status);
+    }
+  }
+
   /**
    * Send message, return result
    *
@@ -125,10 +137,8 @@ public function bind($user, $password) {
         $error= $stream->readString();
 
         // TODO: Referalls
-        $stream->read($stream->remaining());
-        if (self::STATUS_OK !== $status) {
-          throw new LDAPException($error ?: 'Bind error', $status);
-        }
+
+        $this->handleResponse($status, $error ?: 'Bind error');
       }]
     ]);
   }
@@ -184,9 +194,7 @@ public function search($base) {
           $matchedDN= $stream->readString();
           $error= $stream->readString();
 
-          if (self::STATUS_OK !== $status) {
-            throw new LDAPException($error ?: 'Search failed', $status);
-          }
+          $this->handleResponse($status, $error ?: 'Search failed');
         }
       ]
     ]);

From c5391a68222b9e55cdb487ba61f78e1971742ee9 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Mon, 30 Jul 2018 12:18:15 +0200
Subject: [PATCH 18/32] Remove lang.Object base class

---
 src/main/php/peer/ldap/util/BerStream.class.php    | 2 +-
 src/main/php/peer/ldap/util/LdapProtocol.class.php | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
index 3c0b41dd..ddb1f6f0 100755
--- a/src/main/php/peer/ldap/util/BerStream.class.php
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -4,7 +4,7 @@
 use io\streams\InputStream;
 use io\streams\OutputStream;
 
-class BerStream extends \lang\Object {
+class BerStream {
   const EOC = 0;
   const BOOLEAN = 1;
   const INTEGER = 2;
diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index dda5a059..21295073 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -2,7 +2,7 @@
 
 use peer\ldap\LDAPException;
 
-class LdapProtocol extends \lang\Object {
+class LdapProtocol {
   const REQ_BIND = 0x60;
   const REQ_UNBIND = 0x42;
   const REQ_SEARCH = 0x63;

From 518e09083def5d2dad2a7ec25c24244c48301133 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Fri, 17 Aug 2018 00:58:53 +0200
Subject: [PATCH 19/32] Extract search(), connect(), bind() and close() to
 library class

---
 .../php/peer/ldap/LDAPConnection.class.php    | 179 ++++++------------
 .../php/peer/ldap/util/LdapLibrary.class.php  | 153 +++++++++++++++
 2 files changed, 216 insertions(+), 116 deletions(-)
 create mode 100755 src/main/php/peer/ldap/util/LdapLibrary.class.php

diff --git a/src/main/php/peer/ldap/LDAPConnection.class.php b/src/main/php/peer/ldap/LDAPConnection.class.php
index 0257216e..dec8b44c 100755
--- a/src/main/php/peer/ldap/LDAPConnection.class.php
+++ b/src/main/php/peer/ldap/LDAPConnection.class.php
@@ -1,9 +1,12 @@
 <?php namespace peer\ldap;
 
+use lang\IllegalArgumentException;
+use lang\Value;
+use lang\XPClass;
 use peer\ConnectException;
 use peer\URL;
-use lang\XPClass;
-use lang\IllegalArgumentException;
+use peer\ldap\util\LdapLibrary;
+use peer\ldap\util\LdapProtocol;
 use util\Secret;
 
 /**
@@ -23,31 +26,12 @@
  * @ext   ldap
  * @test  xp://peer.ldap.unittest.LDAPConnectionTest
  */
-class LDAPConnection {
-  private static $options;
-
+class LDAPConnection implements Value {
   private $url;
   private $handle= null;
 
   static function __static() {
     XPClass::forName('peer.ldap.LDAPException');  // Error codes
-    self::$options= [
-      'deref' => function($handle, $value) {
-        return ldap_set_option($handle, LDAP_OPT_DEREF, constant('LDAP_DEREF_'.strtoupper($value)));
-      },
-      'sizelimit' => function($handle, $value) {
-        return ldap_set_option($handle, LDAP_OPT_SIZELIMIT, (int)$value);
-      },
-      'timelimit' => function($handle, $value) {
-        return ldap_set_option($handle, LDAP_OPT_TIMELIMIT, (int)$value);
-      },
-      'network_timeout' => function($handle, $value) {
-        return ldap_set_option($handle, LDAP_OPT_NETWORK_TIMEOUT, (int)$value);
-      },
-      'protocol_version' => function($handle, $value) {
-        return ldap_set_option($handle, LDAP_OPT_PROTOCOL_VERSION, (int)$value);
-      },
-    ];
   }
 
   /**
@@ -58,12 +42,18 @@ static function __static() {
    * @throws lang.IllegalArgumentException when DSN is malformed
    */
   public function __construct($dsn) {
+    static $ports= ['ldap' => 389, 'ldaps' => 636];
+
+    // TODO: Driver!
+    $impl= getenv('PROTO') ? LdapProtocol::class : LdapLibrary::class;
+
     $this->url= $dsn instanceof URL ? $dsn : new URL($dsn);
-    foreach ($this->url->getParams() as $option => $value) {
-      if (!isset(self::$options[$option])) {
-        throw new IllegalArgumentException('Unknown option "'.$option.'"');
-      }
-    }
+    $this->proto= new $impl(
+      $this->url->getScheme(),
+      $this->url->getHost(),
+      $this->url->getPort($ports[$this->url->getScheme()]),
+      $this->url->getParams()
+    );
   }
 
   /** @return peer.URL */
@@ -79,45 +69,12 @@ public function dsn() { return $this->url; }
    * @throws peer.ConnectException
    */
   public function connect($dn= null, Secret $password= null) {
-    static $ports= ['ldap' => 389, 'ldaps' => 636];
-
-    if ($this->isConnected()) return true;
+    if ($this->proto->connected()) return true;
 
-    $uri= sprintf(
-      '%s://%s:%d',
-      $this->url->getScheme(),
-      $this->url->getHost(),
-      $this->url->getPort($ports[$this->url->getScheme()])
+    $this->proto->connect(
+      $dn ?: $this->url->getUser(null),
+      $password ?: new Secret($this->url->getPassword(null))
     );
-    if (false === ($this->handle= ldap_connect($uri))) {
-      throw new ConnectException('Cannot connect to '.$uri);
-    }
-
-    foreach (array_merge(['protocol_version' => 3], $this->url->getParams()) as $option => $value) {
-      $set= self::$options[$option];
-      if (!$set($this->handle, $value)) {
-        ldap_unbind($this->handle);
-        $this->handle= null;
-        throw new LDAPException('Cannot set option "'.$option.'"', ldap_errno($this->handle));
-      }
-    }
-
-    if (null === $dn) {
-      $result= ldap_bind($this->handle, $this->url->getUser(null), $this->url->getPassword(null));
-    } else {
-      $result= ldap_bind($this->handle, $dn, $password->reveal());
-    }
-    if (false === $result) {
-      $error= ldap_errno($this->handle);
-      ldap_unbind($this->handle);
-      $this->handle= null;
-      if (LDAP_SERVER_DOWN === $error || -1 === $error) {
-        throw new ConnectException('Cannot connect to '.$uri);
-      } else {
-        throw new LDAPException('Cannot bind for "'.($dn ?: $this->url->getUser(null)).'"', $error);
-      }
-    }
-
     return $this;
   }
 
@@ -127,7 +84,7 @@ public function connect($dn= null, Secret $password= null) {
    * @return bool
    */
   public function isConnected() {
-    return is_resource($this->handle);
+    return $this->proto->connected();
   }
   
   /**
@@ -136,10 +93,7 @@ public function isConnected() {
    * @see     php://ldap_close
    */
   public function close() {
-    if ($this->handle) {
-      ldap_unbind($this->handle);
-      $this->handle= null;
-    }
+    $this->proto->close();
   }
 
   /**
@@ -149,11 +103,11 @@ public function close() {
    * @return peer.ldap.LDAPException
    */
   private function error($message) {
-    $error= ldap_errno($this->handle);
+    $error= ldap_errno($this->proto->handle);
     switch ($error) {
       case -1: case LDAP_SERVER_DOWN:
-        ldap_unbind($this->handle);
-        $this->handle= null;
+        ldap_unbind($this->proto->handle);
+        $this->proto->handle= null;
         return new LDAPDisconnected($message, $error);
 
       case LDAP_NO_SUCH_OBJECT:
@@ -179,20 +133,17 @@ private function error($message) {
    * @see     php://ldap_search
    */
   public function search($base, $filter, $attributes= [], $attrsOnly= 0, $sizeLimit= 0, $timeLimit= 0, $deref= LDAP_DEREF_NEVER) {
-    if (false === ($res= ldap_search(
-      $this->handle,
+    return $this->proto->search(
+      LDAPQuery::SCOPE_SUB,
       $base,
       $filter,
       $attributes,
       $attrsOnly,
       $sizeLimit,
       $timeLimit,
+      null,
       $deref
-    ))) {
-      throw $this->error('Search failed');
-    }
-    
-    return new LDAPSearchResult(new LDAPEntries($this->handle, $res));
+    );
   }
   
   /**
@@ -202,37 +153,17 @@ public function search($base, $filter, $attributes= [], $attrsOnly= 0, $sizeLimi
    * @return  peer.ldap.LDAPSearchResult search result object
    */
   public function searchBy(LDAPQuery $filter) {
-    static $methods= [
-      LDAPQuery::SCOPE_BASE     => 'ldap_read',
-      LDAPQuery::SCOPE_ONELEVEL => 'ldap_list',
-      LDAPQuery::SCOPE_SUB      => 'ldap_search'
-    ];
-    
-    if (!isset($methods[$filter->getScope()])) {
-      throw new IllegalArgumentException('Scope '.$filter->getScope().' not supported');
-    }
-
-    $f= $methods[$filter->getScope()];
-    if (false === ($res= $f(
-      $this->handle,
+    return $this->proto->search(
+      $filter->getScope(),
       $filter->getBase(),
       $filter->getFilter(),
       $filter->getAttrs(),
       $filter->getAttrsOnly(),
       $filter->getSizeLimit(),
       $filter->getTimelimit(),
+      $filter->getSort(),
       $filter->getDeref()
-    ))) {
-      throw $this->error('Search failed');
-    }
-
-    if ($sort= $filter->getSort()) {
-      $entries= new SortedLDAPEntries($this->handle, $res, $sort);
-    } else {
-      $entries= new LDAPEntries($this->handle, $res);
-    }
-
-    return new LDAPSearchResult($entries);
+    );
   }
   
   /**
@@ -244,13 +175,13 @@ public function searchBy(LDAPQuery $filter) {
    * @throws  peer.ldap.LDAPException
    */
   public function read(LDAPEntry $entry) {
-    $res= ldap_read($this->handle, $entry->getDN(), 'objectClass=*', [], false, 0);
-    if (LDAP_SUCCESS != ldap_errno($this->handle)) {
+    $res= ldap_read($this->proto->handle, $entry->getDN(), 'objectClass=*', [], false, 0);
+    if (LDAP_SUCCESS != ldap_errno($this->proto->handle)) {
       throw $this->error('Read "'.$entry->getDN().'" failed');
     }
 
-    $entry= ldap_first_entry($this->handle, $res);
-    return LDAPEntry::create(ldap_get_dn($this->handle, $entry), ldap_get_attributes($this->handle, $entry));
+    $entry= ldap_first_entry($this->proto->handle, $res);
+    return LDAPEntry::create(ldap_get_dn($this->proto->handle, $entry), ldap_get_attributes($this->proto->handle, $entry));
   }
   
   /**
@@ -260,15 +191,15 @@ public function read(LDAPEntry $entry) {
    * @return  bool TRUE if the entry exists
    */
   public function exists(LDAPEntry $entry) {
-    $res= ldap_read($this->handle, $entry->getDN(), 'objectClass=*', [], false, 0);
+    $res= ldap_read($this->proto->handle, $entry->getDN(), 'objectClass=*', [], false, 0);
     
     // Check for certain error code (#32)
-    if (LDAP_NO_SUCH_OBJECT === ldap_errno($this->handle)) {
+    if (LDAP_NO_SUCH_OBJECT === ldap_errno($this->proto->handle)) {
       return false;
     }
     
     // Check for other errors
-    if (LDAP_SUCCESS != ldap_errno($this->handle)) {
+    if (LDAP_SUCCESS != ldap_errno($this->proto->handle)) {
       throw $this->error('Read "'.$entry->getDN().'" failed');
     }
     
@@ -289,7 +220,7 @@ public function add(LDAPEntry $entry) {
     
     // This actually returns NULL on failure, not FALSE, as documented
     if (null == ($res= ldap_add(
-      $this->handle, 
+      $this->proto->handle, 
       $entry->getDN(), 
       $entry->getAttributes()
     ))) {
@@ -312,7 +243,7 @@ public function add(LDAPEntry $entry) {
    */
   public function modify(LDAPEntry $entry) {
     if (false == ($res= ldap_modify(
-      $this->handle,
+      $this->proto->handle,
       $entry->getDN(),
       $entry->getAttributes()
     ))) {
@@ -332,7 +263,7 @@ public function modify(LDAPEntry $entry) {
    */
   public function delete(LDAPEntry $entry) {
     if (false == ($res= ldap_delete(
-      $this->handle,
+      $this->proto->handle,
       $entry->getDN()
     ))) {
       throw $this->error('Delete for "'.$entry->getDN().'" failed');
@@ -351,7 +282,7 @@ public function delete(LDAPEntry $entry) {
    */
   public function addAttribute(LDAPEntry $entry, $name, $value) {
     if (false == ($res= ldap_mod_add(
-      $this->handle,
+      $this->proto->handle,
       $entry->getDN(),
       [$name => $value]
     ))) {
@@ -370,7 +301,7 @@ public function addAttribute(LDAPEntry $entry, $name, $value) {
    */
   public function deleteAttribute(LDAPEntry $entry, $name) {
     if (false == ($res= ldap_mod_del(
-      $this->handle,
+      $this->proto->handle,
       $entry->getDN(),
       $name
     ))) {
@@ -390,7 +321,7 @@ public function deleteAttribute(LDAPEntry $entry, $name) {
    */
   public function replaceAttribute(LDAPEntry $entry, $name, $value) {
     if (false == ($res= ldap_mod_replace(
-      $this->handle,
+      $this->proto->handle,
       $entry->getDN(),
       [$name => $value]
     ))) {
@@ -399,4 +330,20 @@ public function replaceAttribute(LDAPEntry $entry, $name, $value) {
     
     return $res;
   }
+
+  /** @return string */
+  public function toString() { return nameof($this).'('.$this->proto->connection().')'; }
+
+  /** @return string */
+  public function hashCode() { return 'C'.$this->proto->id(); }
+
+  /**
+   * Compare
+   *
+   * @param  var $value
+   * @return int
+   */
+  public function compareTo($value) {
+    return $value === $this ? 0 : 1;
+  }
 }
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/util/LdapLibrary.class.php b/src/main/php/peer/ldap/util/LdapLibrary.class.php
new file mode 100755
index 00000000..cca6e5b9
--- /dev/null
+++ b/src/main/php/peer/ldap/util/LdapLibrary.class.php
@@ -0,0 +1,153 @@
+<?php namespace peer\ldap\util;
+
+use lang\IllegalArgumentException;
+use peer\ConnectException;
+use peer\ldap\LDAPDisconnected;
+use peer\ldap\LDAPEntries;
+use peer\ldap\LDAPException;
+use peer\ldap\LDAPNoSuchObject;
+use peer\ldap\LDAPQuery;
+use peer\ldap\LDAPSearchResult;
+use peer\ldap\SortedLDAPEntries;
+
+class LdapLibrary {
+  private static $options;
+  private $url;
+  public $handle= null;
+
+  static function __static() {
+    self::$options= [
+      'deref' => function($handle, $value) {
+        return ldap_set_option($handle, LDAP_OPT_DEREF, constant('LDAP_DEREF_'.strtoupper($value)));
+      },
+      'sizelimit' => function($handle, $value) {
+        return ldap_set_option($handle, LDAP_OPT_SIZELIMIT, (int)$value);
+      },
+      'timelimit' => function($handle, $value) {
+        return ldap_set_option($handle, LDAP_OPT_TIMELIMIT, (int)$value);
+      },
+      'network_timeout' => function($handle, $value) {
+        return ldap_set_option($handle, LDAP_OPT_NETWORK_TIMEOUT, (int)$value);
+      },
+      'protocol_version' => function($handle, $value) {
+        return ldap_set_option($handle, LDAP_OPT_PROTOCOL_VERSION, (int)$value);
+      },
+    ];
+  }
+
+  public function __construct($scheme, $host, $port, $params) {
+    $this->uri= sprintf('%s://%s:%d', $scheme, $host, $port);
+    foreach ($params as $option => $value) {
+      if (!isset(self::$options[$option])) {
+        throw new IllegalArgumentException('Unknown option "'.$option.'"');
+      }
+    }
+    $this->params= array_merge(['protocol_version' => 3], $params);
+  }
+
+  /**
+   * Error handler
+   *
+   * @param  string $message
+   * @return peer.ldap.LDAPException
+   */
+  private function error($message) {
+    $error= ldap_errno($this->proto->handle);
+    switch ($error) {
+      case -1: case LDAP_SERVER_DOWN:
+        ldap_unbind($this->proto->handle);
+        $this->proto->handle= null;
+        return new LDAPDisconnected($message, $error);
+
+      case LDAP_NO_SUCH_OBJECT:
+        return new LDAPNoSuchObject($message, $error);
+    
+      default:  
+        return new LDAPException($message, $error);
+    }
+  }
+
+  /** @return string */
+  public function connection() { return $this->handle.' -> '.$this->uri; }
+
+  /** @return int */
+  public function id() { return (int)$this->handle; }
+
+  /** @return bool */
+  public function connected() { return null !== $this->handle; }
+
+  /**
+   * Connect and bind
+   *
+   * @param  string $user
+   * @param  util.Secret $password
+   * @return void
+   * @throws peer.ConnectException
+   * @throws peer.ldap.LDAPException
+   */
+  public function connect($user, $password) {
+    if (false === ($this->handle= ldap_connect($this->uri))) {
+      throw new ConnectException('Cannot connect to '.$this->uri);
+    }
+
+    foreach ($this->params as $option => $value) {
+      $set= self::$options[$option];
+      if (!$set($this->handle, $value)) {
+        ldap_unbind($this->handle);
+        $this->handle= null;
+        throw new ConnectException('Cannot set option "'.$option.'"', ldap_errno($this->handle));
+      }
+    }
+
+    $result= ldap_bind($this->handle, $user, $password ? $password->reveal() : null);
+    if (false === $result) {
+      $error= ldap_errno($this->handle);
+      ldap_unbind($this->handle);
+      $this->handle= null;
+      if (LDAP_SERVER_DOWN === $error || -1 === $error) {
+        throw new ConnectException('Cannot connect to '.$uri);
+      } else {
+        throw new LDAPException('Cannot bind for "'.($dn ?: $this->url->getUser(null)).'"', $error);
+      }
+    } 
+  }
+
+  public function search($scope, $base, $filter, $attributes= [], $attrsOnly= 0, $sizeLimit= 0, $timeLimit= 0, $sort= [], $deref= LDAP_DEREF_NEVER) {
+    static $methods= [
+      LDAPQuery::SCOPE_BASE     => 'ldap_read',
+      LDAPQuery::SCOPE_ONELEVEL => 'ldap_list',
+      LDAPQuery::SCOPE_SUB      => 'ldap_search'
+    ];
+
+    if (!isset($methods[$scope])) {
+      throw new IllegalArgumentException('Scope '.$filter->getScope().' not supported');
+    }
+
+    if (false === ($res= $methods[$scope]($this->handle, $base, $filter, $attributes, $attrsOnly, $sizeLimit, $timeLimit, $deref))) {
+      throw $this->error('Search failed');
+    }
+
+    if ($sort) {
+      $entries= new SortedLDAPEntries($this->handle, $res, $sort);
+    } else {
+      $entries= new LDAPEntries($this->handle, $res);
+    }
+
+    return new LDAPSearchResult($entries);
+  }
+
+  /**
+   * Closes the connection
+   *
+   * @return void
+   */
+  public function close() {
+    if ($this->handle) {
+      ldap_unbind($this->handle);
+      $this->handle= null;
+    }
+  }
+
+  /** Ensures stream is closed. */
+  public function __destruct() { $this->close(); }
+}
\ No newline at end of file

From a2c60caabbcdfd6b856d28c6cbfddcb756622d06 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Fri, 17 Aug 2018 00:59:41 +0200
Subject: [PATCH 20/32] Implement substring filter, hardcoded at the moment

---
 .../php/peer/ldap/util/BerStream.class.php    | 60 +++++++++++--------
 src/main/php/peer/ldap/util/Entries.class.php | 49 +++++++++++++++
 .../php/peer/ldap/util/LdapProtocol.class.php | 60 +++++++++++++------
 3 files changed, 125 insertions(+), 44 deletions(-)
 create mode 100755 src/main/php/peer/ldap/util/Entries.class.php

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
index ddb1f6f0..f5659050 100755
--- a/src/main/php/peer/ldap/util/BerStream.class.php
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -53,31 +53,29 @@ class BerStream {
   public function __construct(InputStream $in, OutputStream $out) {
 
     // Debug
-    /*
-    $in= newinstance('io.streams.InputStream', [$in], [
-      'backing'     => null,
-      '__construct' => function($backing) { $this->backing= $backing; },
-      'read'        => function($length= 8192) {
-        $chunk= $this->backing->read($length);
-        if (null !== $chunk) {
-          \util\cmd\Console::writeLine(BerStream::dump($chunk, '<<<'));
-        }
-        return $chunk;
-      },
-      'available'   => function() { return $this->backing->available(); },
-      'close'       => function() { $this->backing->close(); }
-    ]);
-    $out= newinstance('io.streams.OutputStream', [$out], [
-      'backing'     => null,
-      '__construct' => function($backing) { $this->backing= $backing; },
-      'write'       => function($chunk) {
-        \util\cmd\Console::writeLine(BerStream::dump($chunk, '>>>'));
-        return $this->backing->write($chunk);
-      },
-      'flush'       => function() { return $this->backing->flush(); },
-      'close'       => function() { $this->backing->close(); }
-    ]);
-    */
+    // $in= newinstance('io.streams.InputStream', [$in], [
+    //   'backing'     => null,
+    //   '__construct' => function($backing) { $this->backing= $backing; },
+    //   'read'        => function($length= 8192) {
+    //     $chunk= $this->backing->read($length);
+    //     if (null !== $chunk) {
+    //       \util\cmd\Console::writeLine(BerStream::dump($chunk, '<<<'));
+    //     }
+    //     return $chunk;
+    //   },
+    //   'available'   => function() { return $this->backing->available(); },
+    //   'close'       => function() { $this->backing->close(); }
+    // ]);
+    // $out= newinstance('io.streams.OutputStream', [$out], [
+    //   'backing'     => null,
+    //   '__construct' => function($backing) { $this->backing= $backing; },
+    //   'write'       => function($chunk) {
+    //     \util\cmd\Console::writeLine(BerStream::dump($chunk, '>>>'));
+    //     return $this->backing->write($chunk);
+    //   },
+    //   'flush'       => function() { return $this->backing->flush(); },
+    //   'close'       => function() { $this->backing->close(); }
+    // ]);
 
     $this->in= $in instanceof BufferedInputStream ? $in : new BufferedInputStream($in, 8192);
     $this->out= $out;
@@ -236,6 +234,18 @@ public function writeEnumeration($e, $tag= self::ENUMERATION) {
     $this->writeInt($e, $tag);
   }
 
+  /**
+   * Write enumeration to current sequence
+   *
+   * @param  string $buffer
+   * @param  int $tag
+   */
+  public function writeBuffer($buffer, $tag) {
+    $this->writeByte($tag);
+    $this->writeLength(strlen($buffer));
+    $this->write($buffer);
+  }
+
   /**
    * Ends current sequences
    */
diff --git a/src/main/php/peer/ldap/util/Entries.class.php b/src/main/php/peer/ldap/util/Entries.class.php
new file mode 100755
index 00000000..87472ddc
--- /dev/null
+++ b/src/main/php/peer/ldap/util/Entries.class.php
@@ -0,0 +1,49 @@
+<?php namespace peer\ldap\util;
+
+use peer\ldap\LDAPEntry;
+
+class Entries {
+  private $list, $offset= 0;
+  
+  public function __construct($list) {
+    $this->list= $list;
+  }
+
+  /** @return int */
+  public function size() { return sizeof($this->list); }
+
+  /**
+   * Gets first entry
+   *
+   * @return  peer.ldap.LDAPEntry or NULL if nothing was found
+   * @throws  peer.ldap.LDAPException in case of a read error
+   */
+  public function first() {
+    if (empty($this->list)) return null; // Nothing found
+
+    $this->offset= 0;
+    return LDAPEntry::create($this->list[0]['dn'], $this->list[0]['attr']);
+  }
+
+  /**
+   * Gets next entry
+   *
+   * @return  peer.ldap.LDAPEntry or NULL if nothing was found
+   * @throws  peer.ldap.LDAPException in case of a read error
+   */
+  public function next() {
+    if (++$this->offset >= sizeof($this->list) - 1) return null;
+
+    $entry= LDAPEntry::create($this->list[$this->offset]['dn'], $this->list[$this->offset]['attr']);
+    return $entry;
+  }
+
+  /**
+   * Close resultset and free result memory
+   *
+   * @return  bool success
+   */
+  public function close() {
+    $this->offset= 0;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 21295073..d61842f5 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -1,6 +1,9 @@
 <?php namespace peer\ldap\util;
 
+use peer\SSLSocket;
+use peer\Socket;
 use peer\ldap\LDAPException;
+use peer\ldap\LDAPSearchResult;
 
 class LdapProtocol {
   const REQ_BIND = 0x60;
@@ -42,18 +45,23 @@ class LdapProtocol {
 
   protected $messageId= 0;
 
-  /**
-   * Creates a new protocol instance communicating on the given socket
-   *
-   * @param  peer.Socket $sock
-   */
-  public function __construct(\peer\Socket $sock) {
-    $this->stream= new BerStream(
-      $sock->getInputStream(),
-      $sock->getOutputStream()
-    );
+  public function __construct($scheme, $host, $port, $params) {
+    if ('ldaps' === $scheme) {
+      $this->sock= new SSLSocket($host, $port);  
+    } else {
+      $this->sock= new Socket($host, $port);
+    }
   }
 
+  /** @return string */
+  public function connection() { return $this->sock->toString(); }
+
+  /** @return int */
+  public function id() { return (int)$this->sock->getHandle(); }
+
+  /** @return bool */
+  public function connected() { return $this->sock->isConnected(); }
+
   /**
    * Calculates and returns next message id, starting with 1.
    *
@@ -118,17 +126,20 @@ public function send($message) {
    * Bind
    *
    * @param  string $user
-   * @param  string $password
+   * @param  util.Secret $password
    * @return void
    * @throws peer.ldap.LDAPException
    */
-  public function bind($user, $password) {
+  public function connect($user, $password) {
+    $this->sock->connect();
+    $this->stream= new BerStream($this->sock->in(), $this->sock->out());
+
     $this->send([
       'req'   => self::REQ_BIND,
       'write' => function($stream) use($user, $password) {
         $stream->writeInt($version= 3);
         $stream->writeString($user);
-        $stream->writeString($password, BerStream::CONTEXT);
+        $stream->writeString($password->reveal(), BerStream::CONTEXT);
       },
       'res'   => self::RES_BIND,
       'read'  => [self::RES_BIND => function($stream) {
@@ -149,10 +160,10 @@ public function bind($user, $password) {
    * @param  string $base
    * @return var
    */
-  public function search($base) {
-    return $this->send([
+  public function search($scope, $base, $filter, $attributes= [], $attrsOnly= 0, $sizeLimit= 0, $timeLimit= 0, $sort= [], $deref= LDAP_DEREF_NEVER) {
+    $r= $this->send([
       'req'   => self::REQ_SEARCH,
-      'write' => function($stream) use($base) {
+      'write' => function($stream) use($base, $filter, $attributes) {
         $stream->writeString($base);
         $stream->writeEnumeration(self::SCOPE_ONE_LEVEL);
         $stream->writeEnumeration(self::NEVER_DEREF_ALIASES);
@@ -160,11 +171,21 @@ public function search($base) {
         $stream->writeInt(0);
         $stream->writeBoolean(false);
 
-        $stream->startSequence(0x87);
-        $stream->write('objectClass');
+        // substring filter {{{
+        $stream->startSequence(0xa4);
+
+        $stream->writeString('cn');
+        $stream->startSequence();
+        $stream->writeString('Friebe', 0x80);
+        $stream->endSequence();
+
         $stream->endSequence();
+        // }}}
 
         $stream->startSequence();
+        foreach ($attributes as $attribute) {
+          $stream->writeString($attribute);
+        }
         $stream->endSequence();
       },
       'res'   => [self::RES_SEARCH_ENTRY, self::RES_SEARCH],
@@ -187,7 +208,7 @@ public function search($base) {
             $stream->finishSequence();
           } while ($stream->remaining());
           $stream->finishSequence();
-          return ['name' => $name, 'attr' => $attributes];
+          return ['dn' => $name, 'attr' => $attributes];
         },
         self::RES_SEARCH => function($stream) {
           $status= $stream->readEnumeration();
@@ -198,6 +219,7 @@ public function search($base) {
         }
       ]
     ]);
+    return new LDAPSearchResult(new Entries($r));
   }
 
   /**

From ec35815c5e422b73a74618a847a16afc28acd7b6 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 17:08:29 +0200
Subject: [PATCH 21/32] Add parser for LDAP filters

See http://www.ldapexplorer.com/en/manual/109010000-ldap-filter-syntax.htm
---
 .../php/peer/ldap/filter/AndFilter.class.php  |   9 ++
 .../ldap/filter/ApproximateFilter.class.php   |  10 ++
 .../peer/ldap/filter/EqualityFilter.class.php |  10 ++
 .../ldap/filter/ExtensibleFilter.class.php    |  11 ++
 .../php/peer/ldap/filter/Filter.class.php     |   5 +
 .../php/peer/ldap/filter/Filters.class.php    |  85 ++++++++++
 .../filter/GreaterThanEqualsFilter.class.php  |  10 ++
 .../filter/LessThanEqualsFilter.class.php     |  10 ++
 .../php/peer/ldap/filter/NotFilter.class.php  |   9 ++
 .../php/peer/ldap/filter/OrFilter.class.php   |   9 ++
 .../peer/ldap/filter/PresenceFilter.class.php |   9 ++
 .../ldap/filter/SubstringFilter.class.php     |  12 ++
 .../unittest/filter/FiltersTest.class.php     | 151 ++++++++++++++++++
 13 files changed, 340 insertions(+)
 create mode 100755 src/main/php/peer/ldap/filter/AndFilter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/ApproximateFilter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/EqualityFilter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/ExtensibleFilter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/Filter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/Filters.class.php
 create mode 100755 src/main/php/peer/ldap/filter/GreaterThanEqualsFilter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/LessThanEqualsFilter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/NotFilter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/OrFilter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/PresenceFilter.class.php
 create mode 100755 src/main/php/peer/ldap/filter/SubstringFilter.class.php
 create mode 100755 src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php

diff --git a/src/main/php/peer/ldap/filter/AndFilter.class.php b/src/main/php/peer/ldap/filter/AndFilter.class.php
new file mode 100755
index 00000000..6791cfe1
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/AndFilter.class.php
@@ -0,0 +1,9 @@
+<?php namespace peer\ldap\filter;
+
+class AndFilter implements Filter {
+  public $filters;
+
+  public function __construct(... $filters) {
+    $this->filters= $filters;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/ApproximateFilter.class.php b/src/main/php/peer/ldap/filter/ApproximateFilter.class.php
new file mode 100755
index 00000000..0576e27d
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/ApproximateFilter.class.php
@@ -0,0 +1,10 @@
+<?php namespace peer\ldap\filter;
+
+class ApproximateFilter implements Filter {
+  public $attribute, $value;
+
+  public function __construct($attribute, $value) {
+    $this->attribute= $attribute;
+    $this->value= $value;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/EqualityFilter.class.php b/src/main/php/peer/ldap/filter/EqualityFilter.class.php
new file mode 100755
index 00000000..203e1431
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/EqualityFilter.class.php
@@ -0,0 +1,10 @@
+<?php namespace peer\ldap\filter;
+
+class EqualityFilter implements Filter {
+  public $attribute, $value;
+
+  public function __construct($attribute, $value) {
+    $this->attribute= $attribute;
+    $this->value= $value;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/ExtensibleFilter.class.php b/src/main/php/peer/ldap/filter/ExtensibleFilter.class.php
new file mode 100755
index 00000000..19a3c624
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/ExtensibleFilter.class.php
@@ -0,0 +1,11 @@
+<?php namespace peer\ldap\filter;
+
+class ExtensibleFilter implements Filter {
+  public $attribute, $rule, $value;
+
+  public function __construct($attribute, $rule, $value) {
+    $this->attribute= $attribute;
+    $this->rule= $rule;
+    $this->value= $value;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/Filter.class.php b/src/main/php/peer/ldap/filter/Filter.class.php
new file mode 100755
index 00000000..feeaf21d
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/Filter.class.php
@@ -0,0 +1,5 @@
+<?php namespace peer\ldap\filter;
+
+interface Filter {
+
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/Filters.class.php b/src/main/php/peer/ldap/filter/Filters.class.php
new file mode 100755
index 00000000..6fd22915
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/Filters.class.php
@@ -0,0 +1,85 @@
+<?php namespace peer\ldap\filter;
+
+/**
+ * Parses LDAP filters
+ *
+ * @test  xp://peer.ldap.unittest.filter.FiltersTest
+ */
+class Filters {
+
+  /**
+   * Returns all braced expressions belonging together
+   *
+   * @param  string $input
+   * @return iterable
+   */
+  private function all($input) {
+    $o= $b= 0;
+    $l= strlen($input);
+
+    while ($o < $l) {
+
+      // (&(objectClass=person)(cn~=Test))(cn=Test)
+      // ^                               ^
+      $s= $o;
+      do {
+        $o+= strcspn($input, '()', $o);
+        if ('(' === $input{$o}) $b++; else if (')' === $input{$o}) $b--;
+      } while ($o++ < $l && $b > 0);
+
+      yield $this->parse(substr($input, $s, $o));
+    }
+  }
+
+  /**
+   * Parses a string
+   *
+   * @param  string $input
+   * @param  peer.ldap.filter.Filter
+   */
+  public function parse($input) {
+    if ('(' === $input{0}) {
+      return $this->parse(substr($input, 1, -1));
+    } else if ('&' === $input{0}) {
+      return new AndFilter(...$this->all(substr($input, 1)));
+    } else if ('|' === $input{0}) {
+      return new OrFilter(...$this->all(substr($input, 1)));
+    } else if ('!' === $input{0}) {
+      return new NotFilter($this->parse(substr($input, 1)));
+    }
+
+    if (!preg_match('/([a-zA-Z0-9:;_.-]+[a-zA-Z0-9;_.-]+)([~><:]?=)(.+)/', $input, $matches)) {
+      throw new FormatException('Invalid filter `'.$input.'`');
+    }
+
+    switch ($matches[2]) {
+      case '=':
+        if ('*' === $matches[3]) return new PresenceFilter($matches[1]);
+
+        $s= preg_split('/(?<!\\\)\*/', $matches[3]);
+        if (1 === sizeof($s)) {
+          return new EqualityFilter($matches[1], $matches[3]);
+        } else {
+          $initial= array_shift($s);
+          $final= array_pop($s);
+          return new SubstringFilter($matches[1], '' === $initial ? null : $initial, $s, '' === $final ? null : $final);
+        }
+
+      case '~=':
+        return new ApproximateFilter($matches[1], $matches[3]);
+
+      case '>=':
+        return new GreaterThanEqualsFilter($matches[1], $matches[3]);
+
+      case '<=':
+        return new LessThanEqualsFilter($matches[1], $matches[3]);
+
+      case ':=':
+        list($attribute, $rule)= explode(':', $matches[1], 2);
+        return new ExtensibleFilter($attribute, $rule, $matches[3]);
+
+      default:
+        throw new FormatException('Invalid filter `'.$input.'`: Unrecognized operator `'.$matches[2].'`');
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/GreaterThanEqualsFilter.class.php b/src/main/php/peer/ldap/filter/GreaterThanEqualsFilter.class.php
new file mode 100755
index 00000000..e3af02d3
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/GreaterThanEqualsFilter.class.php
@@ -0,0 +1,10 @@
+<?php namespace peer\ldap\filter;
+
+class GreaterThanEqualsFilter implements Filter {
+  public $attribute, $value;
+
+  public function __construct($attribute, $value) {
+    $this->attribute= $attribute;
+    $this->value= $value;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/LessThanEqualsFilter.class.php b/src/main/php/peer/ldap/filter/LessThanEqualsFilter.class.php
new file mode 100755
index 00000000..f35eb672
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/LessThanEqualsFilter.class.php
@@ -0,0 +1,10 @@
+<?php namespace peer\ldap\filter;
+
+class LessThanEqualsFilter implements Filter {
+  public $attribute, $value;
+
+  public function __construct($attribute, $value) {
+    $this->attribute= $attribute;
+    $this->value= $value;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/NotFilter.class.php b/src/main/php/peer/ldap/filter/NotFilter.class.php
new file mode 100755
index 00000000..43cd8e84
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/NotFilter.class.php
@@ -0,0 +1,9 @@
+<?php namespace peer\ldap\filter;
+
+class NotFilter implements Filter {
+  public $filter;
+
+  public function __construct($filter) {
+    $this->filter= $filter;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/OrFilter.class.php b/src/main/php/peer/ldap/filter/OrFilter.class.php
new file mode 100755
index 00000000..4f6cfc27
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/OrFilter.class.php
@@ -0,0 +1,9 @@
+<?php namespace peer\ldap\filter;
+
+class OrFilter implements Filter {
+  public $filters;
+
+  public function __construct(... $filters) {
+    $this->filters= $filters;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/PresenceFilter.class.php b/src/main/php/peer/ldap/filter/PresenceFilter.class.php
new file mode 100755
index 00000000..989c1e55
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/PresenceFilter.class.php
@@ -0,0 +1,9 @@
+<?php namespace peer\ldap\filter;
+
+class PresenceFilter implements Filter {
+  public $attribute;
+
+  public function __construct($attribute) {
+    $this->attribute= $attribute;
+  }
+}
\ No newline at end of file
diff --git a/src/main/php/peer/ldap/filter/SubstringFilter.class.php b/src/main/php/peer/ldap/filter/SubstringFilter.class.php
new file mode 100755
index 00000000..cdbe5d7d
--- /dev/null
+++ b/src/main/php/peer/ldap/filter/SubstringFilter.class.php
@@ -0,0 +1,12 @@
+<?php namespace peer\ldap\filter;
+
+class SubstringFilter implements Filter {
+  public $attribute, $initial, $any, $final;
+
+  public function __construct($attribute, $initial, $any, $final) {
+    $this->attribute= $attribute;
+    $this->initial= $initial;
+    $this->any= $any;
+    $this->final= $final;
+  }
+}
\ No newline at end of file
diff --git a/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php b/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php
new file mode 100755
index 00000000..f7b0e66b
--- /dev/null
+++ b/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php
@@ -0,0 +1,151 @@
+<?php namespace peer\ldap\unittest\filter;
+
+use peer\ldap\filter\AndFilter;
+use peer\ldap\filter\ApproximateFilter;
+use peer\ldap\filter\EqualityFilter;
+use peer\ldap\filter\ExtensibleFilter;
+use peer\ldap\filter\Filters;
+use peer\ldap\filter\GreaterThanEqualsFilter;
+use peer\ldap\filter\LessThanEqualsFilter;
+use peer\ldap\filter\NotFilter;
+use peer\ldap\filter\OrFilter;
+use peer\ldap\filter\PresenceFilter;
+use peer\ldap\filter\SubstringFilter;
+use unittest\TestCase;
+
+class FiltersTest extends TestCase {
+
+  #[@test]
+  public function can_create() {
+    new Filters();
+  }
+
+  #[@test]
+  public function presence() {
+    $this->assertEquals(
+      new PresenceFilter('objectClass'),
+      (new Filters())->parse('objectClass=*')
+    );
+  }
+
+  #[@test, @values([
+  #  'person',
+  #  '\*person',
+  #  'person\*',
+  #])]
+  public function equality($value) {
+    $this->assertEquals(
+      new EqualityFilter('objectClass', $value),
+      (new Filters())->parse('objectClass='.$value)
+    );
+  }
+
+  #[@test]
+  public function substring_initial() {
+    $this->assertEquals(
+      new SubstringFilter('objectClass', 'person', [], null),
+      (new Filters())->parse('objectClass=person*')
+    );
+  }
+
+  #[@test]
+  public function substring_final() {
+    $this->assertEquals(
+      new SubstringFilter('objectClass', null, [], 'person'),
+      (new Filters())->parse('objectClass=*person')
+    );
+  }
+
+  #[@test]
+  public function substring_initial_and_final() {
+    $this->assertEquals(
+      new SubstringFilter('objectClass', 'a', [], 'b'),
+      (new Filters())->parse('objectClass=a*b')
+    );
+  }
+
+  #[@test, @values([
+  #  ['a*b*c*d', ['a', ['b', 'c'], 'd']],
+  #  ['*b*c*d', [null, ['b', 'c'], 'd']],
+  #  ['a*b*c*', ['a', ['b', 'c'], null]],
+  #])]
+  public function substring_any($input, $expected) {
+    $this->assertEquals(
+      new SubstringFilter('objectClass', ...$expected),
+      (new Filters())->parse('objectClass='.$input)
+    );
+  }
+
+  #[@test]
+  public function approximate() {
+    $this->assertEquals(
+      new ApproximateFilter('cn', 'Test'),
+      (new Filters())->parse('cn~=Test')
+    );
+  }
+
+  #[@test]
+  public function greater_than() {
+    $this->assertEquals(
+      new GreaterThanEqualsFilter('storageQuota', '100'),
+      (new Filters())->parse('storageQuota>=100')
+    );
+  }
+
+  #[@test]
+  public function less_than() {
+    $this->assertEquals(
+      new LessThanEqualsFilter('storageQuota', '100'),
+      (new Filters())->parse('storageQuota<=100')
+    );
+  }
+
+  #[@test]
+  public function extensible() {
+    $this->assertEquals(
+      new ExtensibleFilter('userAccountControl', '1.2.840.113556.1.4.804', '65568'),
+      (new Filters())->parse('userAccountControl:1.2.840.113556.1.4.804:=65568')
+    );
+  }
+
+  #[@test]
+  public function braces() {
+    $this->assertEquals(
+      new EqualityFilter('cn', 'Test'),
+      (new Filters())->parse('(cn=Test)')
+    );
+  }
+
+  #[@test]
+  public function not() {
+    $this->assertEquals(
+      new NotFilter(new EqualityFilter('cn', 'Test')),
+      (new Filters())->parse('!(cn=Test)')
+    );
+  }
+
+  #[@test]
+  public function and() {
+    $this->assertEquals(
+      new AndFilter(
+        new EqualityFilter('objectClass', 'person'),
+        new ApproximateFilter('cn', 'Test')
+      ),
+      (new Filters())->parse('&(objectClass=person)(cn~=Test)')
+    );
+  }
+
+  #[@test]
+  public function and_and_or() {
+    $this->assertEquals(
+      new OrFilter(
+        new AndFilter(
+          new EqualityFilter('objectClass', 'person'),
+          new ApproximateFilter('cn', 'Test')
+        ),
+        new EqualityFilter('cn', 'Test')
+      ),
+      (new Filters())->parse('|(&(objectClass=person)(cn~=Test))(cn=Test)')
+    );
+  }
+}
\ No newline at end of file

From bbf6dbc62b83a59fb341804e38c49145d12eceb2 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 17:17:21 +0200
Subject: [PATCH 22/32] Add tests for invalid LDAP filter syntax

---
 .../php/peer/ldap/filter/Filters.class.php    | 58 +++++++++----------
 .../unittest/filter/FiltersTest.class.php     | 13 +++++
 2 files changed, 42 insertions(+), 29 deletions(-)

diff --git a/src/main/php/peer/ldap/filter/Filters.class.php b/src/main/php/peer/ldap/filter/Filters.class.php
index 6fd22915..7789d581 100755
--- a/src/main/php/peer/ldap/filter/Filters.class.php
+++ b/src/main/php/peer/ldap/filter/Filters.class.php
@@ -1,5 +1,7 @@
 <?php namespace peer\ldap\filter;
 
+use lang\FormatException;
+
 /**
  * Parses LDAP filters
  *
@@ -38,7 +40,7 @@ private function all($input) {
    * @param  peer.ldap.filter.Filter
    */
   public function parse($input) {
-    if ('(' === $input{0}) {
+    if ('(' === $input{0} && ')' === $input{strlen($input) - 1}) {
       return $this->parse(substr($input, 1, -1));
     } else if ('&' === $input{0}) {
       return new AndFilter(...$this->all(substr($input, 1)));
@@ -46,40 +48,38 @@ public function parse($input) {
       return new OrFilter(...$this->all(substr($input, 1)));
     } else if ('!' === $input{0}) {
       return new NotFilter($this->parse(substr($input, 1)));
-    }
-
-    if (!preg_match('/([a-zA-Z0-9:;_.-]+[a-zA-Z0-9;_.-]+)([~><:]?=)(.+)/', $input, $matches)) {
-      throw new FormatException('Invalid filter `'.$input.'`');
-    }
-
-    switch ($matches[2]) {
-      case '=':
-        if ('*' === $matches[3]) return new PresenceFilter($matches[1]);
+    } else if (preg_match('/^([a-zA-Z0-9:;_.-]+[a-zA-Z0-9;_.-]+)([~><:]?=)(.+)$/', $input, $matches)) {
+      switch ($matches[2]) {
+        case '=':
+          if ('*' === $matches[3]) return new PresenceFilter($matches[1]);
 
-        $s= preg_split('/(?<!\\\)\*/', $matches[3]);
-        if (1 === sizeof($s)) {
-          return new EqualityFilter($matches[1], $matches[3]);
-        } else {
-          $initial= array_shift($s);
-          $final= array_pop($s);
-          return new SubstringFilter($matches[1], '' === $initial ? null : $initial, $s, '' === $final ? null : $final);
-        }
+          $s= preg_split('/(?<!\\\)\*/', $matches[3]);
+          if (1 === sizeof($s)) {
+            return new EqualityFilter($matches[1], $matches[3]);
+          } else {
+            $initial= array_shift($s);
+            $final= array_pop($s);
+            return new SubstringFilter($matches[1], '' === $initial ? null : $initial, $s, '' === $final ? null : $final);
+          }
 
-      case '~=':
-        return new ApproximateFilter($matches[1], $matches[3]);
+        case '~=':
+          return new ApproximateFilter($matches[1], $matches[3]);
 
-      case '>=':
-        return new GreaterThanEqualsFilter($matches[1], $matches[3]);
+        case '>=':
+          return new GreaterThanEqualsFilter($matches[1], $matches[3]);
 
-      case '<=':
-        return new LessThanEqualsFilter($matches[1], $matches[3]);
+        case '<=':
+          return new LessThanEqualsFilter($matches[1], $matches[3]);
 
-      case ':=':
-        list($attribute, $rule)= explode(':', $matches[1], 2);
-        return new ExtensibleFilter($attribute, $rule, $matches[3]);
+        case ':=':
+          list($attribute, $rule)= explode(':', $matches[1], 2);
+          return new ExtensibleFilter($attribute, $rule, $matches[3]);
 
-      default:
-        throw new FormatException('Invalid filter `'.$input.'`: Unrecognized operator `'.$matches[2].'`');
+        default:
+          throw new FormatException('Invalid filter `'.$input.'`: Unrecognized operator `'.$matches[2].'`');
+      }
     }
+
+    throw new FormatException('Invalid filter `'.$input.'`: Expected `(`, `&`, `|`, `!` or criteria');
   }
 }
\ No newline at end of file
diff --git a/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php b/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php
index f7b0e66b..996342f3 100755
--- a/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php
+++ b/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php
@@ -1,5 +1,6 @@
 <?php namespace peer\ldap\unittest\filter;
 
+use lang\FormatException;
 use peer\ldap\filter\AndFilter;
 use peer\ldap\filter\ApproximateFilter;
 use peer\ldap\filter\EqualityFilter;
@@ -148,4 +149,16 @@ public function and_and_or() {
       (new Filters())->parse('|(&(objectClass=person)(cn~=Test))(cn=Test)')
     );
   }
+
+  #[@test, @expect(FormatException::class), @values([
+  #  'cn=',
+  #  '!cn=',
+  #  '^cn=value',
+  #  'cn^=value',
+  #  '(cn=value',
+  #  'GET / HTTP/1.1',
+  #])]
+  public function invalid($syntax) {
+   (new Filters())->parse($syntax);
+  }
 }
\ No newline at end of file

From 98419f482ea7e5ec16da40ddfe2c35bb3dd60de6 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 17:19:54 +0200
Subject: [PATCH 23/32] Fix PHP 5.6 compatibility

syntax error, unexpected "and" (T_LOGICAL_AND), expecting identifier (T_STRING)
---
 .../php/peer/ldap/unittest/filter/FiltersTest.class.php     | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php b/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php
index 996342f3..cabee915 100755
--- a/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php
+++ b/src/test/php/peer/ldap/unittest/filter/FiltersTest.class.php
@@ -118,7 +118,7 @@ public function braces() {
   }
 
   #[@test]
-  public function not() {
+  public function logical_not() {
     $this->assertEquals(
       new NotFilter(new EqualityFilter('cn', 'Test')),
       (new Filters())->parse('!(cn=Test)')
@@ -126,7 +126,7 @@ public function not() {
   }
 
   #[@test]
-  public function and() {
+  public function logical_and() {
     $this->assertEquals(
       new AndFilter(
         new EqualityFilter('objectClass', 'person'),
@@ -137,7 +137,7 @@ public function and() {
   }
 
   #[@test]
-  public function and_and_or() {
+  public function logical_and_and_or() {
     $this->assertEquals(
       new OrFilter(
         new AndFilter(

From 4bfd8aaac999e5c7b9cb1ae061a3bf99dc9802ba Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 20:08:47 +0200
Subject: [PATCH 24/32] Rewrite all() to return an array

Should fix HHVM incompatibility
---
 src/main/php/peer/ldap/filter/Filters.class.php | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/main/php/peer/ldap/filter/Filters.class.php b/src/main/php/peer/ldap/filter/Filters.class.php
index 7789d581..d0a3f42b 100755
--- a/src/main/php/peer/ldap/filter/Filters.class.php
+++ b/src/main/php/peer/ldap/filter/Filters.class.php
@@ -13,12 +13,12 @@ class Filters {
    * Returns all braced expressions belonging together
    *
    * @param  string $input
-   * @return iterable
+   * @return peer.ldap.filter.Filter[]
    */
   private function all($input) {
     $o= $b= 0;
     $l= strlen($input);
-
+    $r= [];
     while ($o < $l) {
 
       // (&(objectClass=person)(cn~=Test))(cn=Test)
@@ -29,8 +29,9 @@ private function all($input) {
         if ('(' === $input{$o}) $b++; else if (')' === $input{$o}) $b--;
       } while ($o++ < $l && $b > 0);
 
-      yield $this->parse(substr($input, $s, $o));
+      $r[]= $this->parse(substr($input, $s, $o));
     }
+    return $r;
   }
 
   /**

From c8945e5afac9a636c689d837291fac5f3a360131 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 21:23:31 +0200
Subject: [PATCH 25/32] Add public "kind" member

---
 src/main/php/peer/ldap/filter/AndFilter.class.php         | 2 ++
 src/main/php/peer/ldap/filter/ApproximateFilter.class.php | 2 ++
 src/main/php/peer/ldap/filter/EqualityFilter.class.php    | 2 ++
 src/main/php/peer/ldap/filter/ExtensibleFilter.class.php  | 8 +++++---
 .../peer/ldap/filter/GreaterThanEqualsFilter.class.php    | 2 ++
 .../php/peer/ldap/filter/LessThanEqualsFilter.class.php   | 2 ++
 src/main/php/peer/ldap/filter/NotFilter.class.php         | 2 ++
 src/main/php/peer/ldap/filter/OrFilter.class.php          | 2 ++
 src/main/php/peer/ldap/filter/PresenceFilter.class.php    | 2 ++
 src/main/php/peer/ldap/filter/SubstringFilter.class.php   | 2 ++
 10 files changed, 23 insertions(+), 3 deletions(-)

diff --git a/src/main/php/peer/ldap/filter/AndFilter.class.php b/src/main/php/peer/ldap/filter/AndFilter.class.php
index 6791cfe1..9d7bf934 100755
--- a/src/main/php/peer/ldap/filter/AndFilter.class.php
+++ b/src/main/php/peer/ldap/filter/AndFilter.class.php
@@ -1,6 +1,8 @@
 <?php namespace peer\ldap\filter;
 
 class AndFilter implements Filter {
+  public $kind= 'and';
+
   public $filters;
 
   public function __construct(... $filters) {
diff --git a/src/main/php/peer/ldap/filter/ApproximateFilter.class.php b/src/main/php/peer/ldap/filter/ApproximateFilter.class.php
index 0576e27d..0048f7d7 100755
--- a/src/main/php/peer/ldap/filter/ApproximateFilter.class.php
+++ b/src/main/php/peer/ldap/filter/ApproximateFilter.class.php
@@ -1,6 +1,8 @@
 <?php namespace peer\ldap\filter;
 
 class ApproximateFilter implements Filter {
+  public $kind= 'approximate';
+
   public $attribute, $value;
 
   public function __construct($attribute, $value) {
diff --git a/src/main/php/peer/ldap/filter/EqualityFilter.class.php b/src/main/php/peer/ldap/filter/EqualityFilter.class.php
index 203e1431..de9322ab 100755
--- a/src/main/php/peer/ldap/filter/EqualityFilter.class.php
+++ b/src/main/php/peer/ldap/filter/EqualityFilter.class.php
@@ -1,6 +1,8 @@
 <?php namespace peer\ldap\filter;
 
 class EqualityFilter implements Filter {
+  public $kind= 'equality';
+
   public $attribute, $value;
 
   public function __construct($attribute, $value) {
diff --git a/src/main/php/peer/ldap/filter/ExtensibleFilter.class.php b/src/main/php/peer/ldap/filter/ExtensibleFilter.class.php
index 19a3c624..4b648c69 100755
--- a/src/main/php/peer/ldap/filter/ExtensibleFilter.class.php
+++ b/src/main/php/peer/ldap/filter/ExtensibleFilter.class.php
@@ -1,10 +1,12 @@
 <?php namespace peer\ldap\filter;
 
 class ExtensibleFilter implements Filter {
-  public $attribute, $rule, $value;
+  public $kind= 'extensible';
 
-  public function __construct($attribute, $rule, $value) {
-    $this->attribute= $attribute;
+  public $type, $rule, $value, $attributes;
+
+  public function __construct($type, $rule, $value, $attributes= false) {
+    $this->type= $type;
     $this->rule= $rule;
     $this->value= $value;
   }
diff --git a/src/main/php/peer/ldap/filter/GreaterThanEqualsFilter.class.php b/src/main/php/peer/ldap/filter/GreaterThanEqualsFilter.class.php
index e3af02d3..eead0519 100755
--- a/src/main/php/peer/ldap/filter/GreaterThanEqualsFilter.class.php
+++ b/src/main/php/peer/ldap/filter/GreaterThanEqualsFilter.class.php
@@ -1,6 +1,8 @@
 <?php namespace peer\ldap\filter;
 
 class GreaterThanEqualsFilter implements Filter {
+  public $kind= 'greaterthanequals';
+
   public $attribute, $value;
 
   public function __construct($attribute, $value) {
diff --git a/src/main/php/peer/ldap/filter/LessThanEqualsFilter.class.php b/src/main/php/peer/ldap/filter/LessThanEqualsFilter.class.php
index f35eb672..45b0629a 100755
--- a/src/main/php/peer/ldap/filter/LessThanEqualsFilter.class.php
+++ b/src/main/php/peer/ldap/filter/LessThanEqualsFilter.class.php
@@ -1,6 +1,8 @@
 <?php namespace peer\ldap\filter;
 
 class LessThanEqualsFilter implements Filter {
+  public $kind= 'lessthanequals';
+
   public $attribute, $value;
 
   public function __construct($attribute, $value) {
diff --git a/src/main/php/peer/ldap/filter/NotFilter.class.php b/src/main/php/peer/ldap/filter/NotFilter.class.php
index 43cd8e84..a81638e8 100755
--- a/src/main/php/peer/ldap/filter/NotFilter.class.php
+++ b/src/main/php/peer/ldap/filter/NotFilter.class.php
@@ -1,6 +1,8 @@
 <?php namespace peer\ldap\filter;
 
 class NotFilter implements Filter {
+  public $kind= 'not';
+
   public $filter;
 
   public function __construct($filter) {
diff --git a/src/main/php/peer/ldap/filter/OrFilter.class.php b/src/main/php/peer/ldap/filter/OrFilter.class.php
index 4f6cfc27..2b072132 100755
--- a/src/main/php/peer/ldap/filter/OrFilter.class.php
+++ b/src/main/php/peer/ldap/filter/OrFilter.class.php
@@ -1,6 +1,8 @@
 <?php namespace peer\ldap\filter;
 
 class OrFilter implements Filter {
+  public $kind= 'or';
+
   public $filters;
 
   public function __construct(... $filters) {
diff --git a/src/main/php/peer/ldap/filter/PresenceFilter.class.php b/src/main/php/peer/ldap/filter/PresenceFilter.class.php
index 989c1e55..43130235 100755
--- a/src/main/php/peer/ldap/filter/PresenceFilter.class.php
+++ b/src/main/php/peer/ldap/filter/PresenceFilter.class.php
@@ -1,6 +1,8 @@
 <?php namespace peer\ldap\filter;
 
 class PresenceFilter implements Filter {
+  public $kind= 'presence';
+
   public $attribute;
 
   public function __construct($attribute) {
diff --git a/src/main/php/peer/ldap/filter/SubstringFilter.class.php b/src/main/php/peer/ldap/filter/SubstringFilter.class.php
index cdbe5d7d..76d42ffe 100755
--- a/src/main/php/peer/ldap/filter/SubstringFilter.class.php
+++ b/src/main/php/peer/ldap/filter/SubstringFilter.class.php
@@ -1,6 +1,8 @@
 <?php namespace peer\ldap\filter;
 
 class SubstringFilter implements Filter {
+  public $kind= 'substring';
+
   public $attribute, $initial, $any, $final;
 
   public function __construct($attribute, $initial, $any, $final) {

From a0d6abb274e9857aeb0c180160d094fbad8a7ce7 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 21:23:50 +0200
Subject: [PATCH 26/32] Use filters API

---
 src/main/php/peer/ldap/util/LdapProtocol.class.php | 13 +++----------
 1 file changed, 3 insertions(+), 10 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index d61842f5..83496857 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -4,6 +4,7 @@
 use peer\Socket;
 use peer\ldap\LDAPException;
 use peer\ldap\LDAPSearchResult;
+use peer\ldap\filter\Filters;
 
 class LdapProtocol {
   const REQ_BIND = 0x60;
@@ -51,6 +52,7 @@ public function __construct($scheme, $host, $port, $params) {
     } else {
       $this->sock= new Socket($host, $port);
     }
+    $this->filters= new Filters();
   }
 
   /** @return string */
@@ -171,16 +173,7 @@ public function search($scope, $base, $filter, $attributes= [], $attrsOnly= 0, $
         $stream->writeInt(0);
         $stream->writeBoolean(false);
 
-        // substring filter {{{
-        $stream->startSequence(0xa4);
-
-        $stream->writeString('cn');
-        $stream->startSequence();
-        $stream->writeString('Friebe', 0x80);
-        $stream->endSequence();
-
-        $stream->endSequence();
-        // }}}
+        $stream->writeFilter($this->filters->parse($filter));
 
         $stream->startSequence();
         foreach ($attributes as $attribute) {

From b599a3cae9763981d45a6b4f03f43465e91c208b Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 21:24:01 +0200
Subject: [PATCH 27/32] Implement writing filters

---
 .../php/peer/ldap/util/BerStream.class.php    | 113 +++++++++++++++++-
 1 file changed, 112 insertions(+), 1 deletion(-)

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
index f5659050..5dfa6528 100755
--- a/src/main/php/peer/ldap/util/BerStream.class.php
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -246,8 +246,119 @@ public function writeBuffer($buffer, $tag) {
     $this->write($buffer);
   }
 
+  /**
+   * Writes a filter
+   *
+   * @param  peer.ldap.filter.Filter
+   * @return void
+   */
+  public function writeFilter($filter) {
+    $this->{'writeFilter'.$filter->kind}($filter);
+  }
+
+  /** FILTER_AND = 0xa0 */
+  private function writeFilterAnd($and) {
+    $this->startSequence(0xa0);
+    foreach ($and->filters as $filter) {
+      $this->{'writeFilter'.$filter->kind}($filter);
+    }
+    $this->endSequence();
+  }
+
+  /** FILTER_AND = 0xa1 */
+  private function writeFilterOr($or) {
+    $this->startSequence(0xa1);
+    foreach ($or->filters as $filter) {
+      $this->{'writeFilter'.$filter->kind}($filter);
+    }
+    $this->endSequence();
+  }
+
+  /** FILTER_AND = 0xa2 */
+  private function writeFilterNot($not) {
+    $this->startSequence(0xa2);
+    $this->{'writeFilter'.$not->filter->kind}($not->filter);
+    $this->endSequence();
+  }
+
+  /** FILTER_EQUALITY = 0xa3 */
+  private function writeFilterEquality($eq) {
+    $this->startSequence(0xa4);
+    $this->writeString($eq->attribute);
+    $this->writeBuffer($eq->value, self::OCTETSTRING);
+    $this->endSequence();
+  }
+
+  /** FILTER_SUBSTRINGS = 0xa4 */
+  private function writeFilterSubstring($substr) {
+    $this->startSequence(0xa4);
+    $this->writeString($substr->attribute);
+
+    $this->startSequence();
+    if (null !== $substr->initial) {
+      $this->writeString($substr->initial, 0x80);
+    }
+    foreach ($substr->any as $string) {
+      $this->writeString($substr->any, 0x81);
+    }
+    if (null !== $substr->final) {
+      $this->writeString($substr->final, 0x82);
+    }
+    $this->endSequence();
+
+    $this->endSequence();
+  }
+
+  /** FILTER_GE = 0xa5 */
+  private function writeFilterGreaterThanEquals($ge) {
+    $this->startSequence(0xa5);
+    $this->writeString($ge->attribute);
+    $this->writeString($ge->value);
+    $this->endSequence();
+  }
+
+  /** FILTER_LE = 0xa6 */
+  private function writeFilterLessThanEquals($le) {
+    $this->startSequence(0xa6);
+    $this->writeString($le->attribute);
+    $this->writeString($le->value);
+    $this->endSequence();
+  }
+
+  /** FILTER_PRESENT = 0x87 */
+  private function writeFilterPresent($present) {
+    $this->startSequence(0x87);
+    for ($i= 0, $l= strlen($present->attribute); $i < $l; $i++) {
+      $this->writeByte($i);
+    }
+    $this->endSequence();
+  }
+
+  /** FILTER_APPROX = 0xa8 */
+  private function writeFilterApproximate($approx) {
+    $this->startSequence(0xa8);
+    $this->writeString($approx->attribute);
+    $this->writeString($approx->value);
+    $this->endSequence();
+  }
+
+  /** FILTER_EXT = 0xa9 */
+  private function writeFilterExtensible($approx) {
+    $this->startSequence(0xa09);
+
+    $this->writeString($approx->rule, 0x81);
+    $this->writeString($approx->type, 0x82);
+    $this->writeString($approx->value, 0x83);
+    if ($approx->attributes) {
+      $this->writeBoolean($approx->type, 0x84);
+    }
+    $this->endSequence();
+  }
+
   /**
    * Ends current sequences
+   *
+   * @return void
    */
   public function endSequence() {
     $length= $this->encodeLength(strlen($this->write[0]) - 1);
@@ -267,7 +378,7 @@ public function flush() {
   }
 
   public function read($l) {
-    $t= debug_backtrace();
+    // $t= debug_backtrace();
     $chunk= $this->in->read($l);
     $this->read[0]-= strlen($chunk);
     // fprintf(STDOUT, "%s   READ %d bytes from %s, remain %d\n", str_repeat('   ', sizeof($this->read)), $l, $t[1]['function'], $this->read[0]);

From 3b183592a4985b8546ea90295408987068b27b50 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 22:59:17 +0200
Subject: [PATCH 28/32] Add peek()

---
 .../php/peer/ldap/util/BerStream.class.php    | 28 +++++++++++++++----
 1 file changed, 22 insertions(+), 6 deletions(-)

diff --git a/src/main/php/peer/ldap/util/BerStream.class.php b/src/main/php/peer/ldap/util/BerStream.class.php
index 5dfa6528..c20f7429 100755
--- a/src/main/php/peer/ldap/util/BerStream.class.php
+++ b/src/main/php/peer/ldap/util/BerStream.class.php
@@ -41,6 +41,7 @@ class BerStream {
 
   protected $write= [''];
   protected $read= [0];
+  protected $buffer= null;
 
   protected $in, $out;
 
@@ -283,7 +284,7 @@ private function writeFilterNot($not) {
 
   /** FILTER_EQUALITY = 0xa3 */
   private function writeFilterEquality($eq) {
-    $this->startSequence(0xa4);
+    $this->startSequence(0xa3);
     $this->writeString($eq->attribute);
     $this->writeBuffer($eq->value, self::OCTETSTRING);
     $this->endSequence();
@@ -378,15 +379,29 @@ public function flush() {
   }
 
   public function read($l) {
+    $chunk= $this->buffer;
+    $this->buffer= null;
+    while (strlen($chunk) < $l) {
+      $bytes= $this->in->read($l - strlen($chunk));
+      $this->read[0]-= strlen($bytes);
+      $chunk.= $bytes;
+    }
+
     // $t= debug_backtrace();
-    $chunk= $this->in->read($l);
-    $this->read[0]-= strlen($chunk);
     // fprintf(STDOUT, "%s   READ %d bytes from %s, remain %d\n", str_repeat('   ', sizeof($this->read)), $l, $t[1]['function'], $this->read[0]);
+
     return $chunk;
   }
 
+  public function peek() {
+    if (null === $this->buffer) {
+      $this->buffer= $this->in->read(1);
+    }
+    return unpack('Ctag', $this->buffer)['tag'];
+  }
+
   public function readTag($expected) {
-    $head= unpack('Ctag', $this->in->read(1));
+    $head= unpack('Ctag', $this->read(1));
     $test= (array)$expected;
     if (!in_array($head['tag'], $test)) {
       throw new \lang\IllegalStateException(sprintf(
@@ -445,10 +460,11 @@ public function readEnumeration() {
   /**
    * Reads a string
    *
+   * @param  int $tag
    * @return string
    */
-  public function readString() {
-    $this->readTag(self::OCTETSTRING);
+  public function readString($tag= self::OCTETSTRING) {
+    $this->readTag($tag);
     return $this->read($this->decodeLength());
   }
 

From c4a71671d562277bf6579600628d167dda0557f0 Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 22:59:32 +0200
Subject: [PATCH 29/32] Implement RES_EXTENSION

---
 .../php/peer/ldap/util/LdapProtocol.class.php | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 83496857..e8c10269 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -41,7 +41,8 @@ class LdapProtocol {
   const STATUS_OK = 0;
 
   protected static $continue= [
-    self::RES_SEARCH_ENTRY => true
+    self::RES_SEARCH_ENTRY => true,
+    self::RES_EXTENSION => true,
   ];
 
   protected $messageId= 0;
@@ -181,7 +182,7 @@ public function search($scope, $base, $filter, $attributes= [], $attrsOnly= 0, $
         }
         $stream->endSequence();
       },
-      'res'   => [self::RES_SEARCH_ENTRY, self::RES_SEARCH],
+      'res'   => [self::RES_SEARCH_ENTRY, self::RES_SEARCH, self::RES_EXTENSION],
       'read'  => [
         self::RES_SEARCH_ENTRY => function($stream) {
           $name= $stream->readString();
@@ -195,11 +196,11 @@ public function search($scope, $base, $filter, $attributes= [], $attrsOnly= 0, $
             $attributes[$attr]= [];
             do {
               $attributes[$attr][]= $stream->readString();
-            } while ($stream->remaining());
+            } while ($stream->remaining() > 0);
             $stream->finishSequence();
 
             $stream->finishSequence();
-          } while ($stream->remaining());
+          } while ($stream->remaining() > 0);
           $stream->finishSequence();
           return ['dn' => $name, 'attr' => $attributes];
         },
@@ -209,6 +210,16 @@ public function search($scope, $base, $filter, $attributes= [], $attrsOnly= 0, $
           $error= $stream->readString();
 
           $this->handleResponse($status, $error ?: 'Search failed');
+        },
+        self::RES_EXTENSION => function($stream) {
+          $status= $stream->readEnumeration();
+          $matchedDN= $stream->readString();
+          $error= $stream->readString();
+
+          $name= 0x8a === $stream->peek() ? $stream->readString(0x8a) : null;
+          // $value= 0x8b === $stream->peek() ? $stream->readString(0x8b) : null;
+
+          $this->handleResponse($status, ($error ?: 'Search failed').($name ? ': '.$name : ''));
         }
       ]
     ]);

From b080bde017b6db3d94dcf8810c3e57680694edbd Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Sun, 9 Sep 2018 23:01:54 +0200
Subject: [PATCH 30/32] Fix size

---
 src/main/php/peer/ldap/util/Entries.class.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/php/peer/ldap/util/Entries.class.php b/src/main/php/peer/ldap/util/Entries.class.php
index 87472ddc..1f7af388 100755
--- a/src/main/php/peer/ldap/util/Entries.class.php
+++ b/src/main/php/peer/ldap/util/Entries.class.php
@@ -10,7 +10,7 @@ public function __construct($list) {
   }
 
   /** @return int */
-  public function size() { return sizeof($this->list); }
+  public function size() { return sizeof($this->list) - 1; }
 
   /**
    * Gets first entry

From 9416b66e8278d9b003ed1d19421ac3acef318e7f Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Mon, 10 Sep 2018 09:47:42 +0200
Subject: [PATCH 31/32] Extract wrapping in LDAPSearchResult to LDAPConnection
 class

---
 src/main/php/peer/ldap/LDAPConnection.class.php    | 8 ++++----
 src/main/php/peer/ldap/util/LdapLibrary.class.php  | 2 +-
 src/main/php/peer/ldap/util/LdapProtocol.class.php | 4 +++-
 3 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/src/main/php/peer/ldap/LDAPConnection.class.php b/src/main/php/peer/ldap/LDAPConnection.class.php
index dec8b44c..887751d2 100755
--- a/src/main/php/peer/ldap/LDAPConnection.class.php
+++ b/src/main/php/peer/ldap/LDAPConnection.class.php
@@ -133,7 +133,7 @@ private function error($message) {
    * @see     php://ldap_search
    */
   public function search($base, $filter, $attributes= [], $attrsOnly= 0, $sizeLimit= 0, $timeLimit= 0, $deref= LDAP_DEREF_NEVER) {
-    return $this->proto->search(
+    return new LDAPSearchResult($this->proto->search(
       LDAPQuery::SCOPE_SUB,
       $base,
       $filter,
@@ -143,7 +143,7 @@ public function search($base, $filter, $attributes= [], $attrsOnly= 0, $sizeLimi
       $timeLimit,
       null,
       $deref
-    );
+    ));
   }
   
   /**
@@ -153,7 +153,7 @@ public function search($base, $filter, $attributes= [], $attrsOnly= 0, $sizeLimi
    * @return  peer.ldap.LDAPSearchResult search result object
    */
   public function searchBy(LDAPQuery $filter) {
-    return $this->proto->search(
+    return new LDAPSearchResult($this->proto->search(
       $filter->getScope(),
       $filter->getBase(),
       $filter->getFilter(),
@@ -163,7 +163,7 @@ public function searchBy(LDAPQuery $filter) {
       $filter->getTimelimit(),
       $filter->getSort(),
       $filter->getDeref()
-    );
+    ));
   }
   
   /**
diff --git a/src/main/php/peer/ldap/util/LdapLibrary.class.php b/src/main/php/peer/ldap/util/LdapLibrary.class.php
index cca6e5b9..6f185413 100755
--- a/src/main/php/peer/ldap/util/LdapLibrary.class.php
+++ b/src/main/php/peer/ldap/util/LdapLibrary.class.php
@@ -133,7 +133,7 @@ public function search($scope, $base, $filter, $attributes= [], $attrsOnly= 0, $
       $entries= new LDAPEntries($this->handle, $res);
     }
 
-    return new LDAPSearchResult($entries);
+    return $entries;
   }
 
   /**
diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index e8c10269..23c13480 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -223,11 +223,13 @@ public function search($scope, $base, $filter, $attributes= [], $attrsOnly= 0, $
         }
       ]
     ]);
-    return new LDAPSearchResult(new Entries($r));
+    return new Entries($r);
   }
 
   /**
    * Closes the connection
+   *
+   * @return void
    */
   public function close() {
     $this->stream->close();

From 6278ac6104c2bdce6361cb65d262dc3e54dd327d Mon Sep 17 00:00:00 2001
From: Timm Friebe <thekid@thekid.de>
Date: Mon, 10 Sep 2018 11:06:28 +0200
Subject: [PATCH 32/32] QA: Apidocs, whitespace

---
 .../php/peer/ldap/util/LdapProtocol.class.php | 74 ++++++++++---------
 1 file changed, 41 insertions(+), 33 deletions(-)

diff --git a/src/main/php/peer/ldap/util/LdapProtocol.class.php b/src/main/php/peer/ldap/util/LdapProtocol.class.php
index 23c13480..ab2ec1e7 100755
--- a/src/main/php/peer/ldap/util/LdapProtocol.class.php
+++ b/src/main/php/peer/ldap/util/LdapProtocol.class.php
@@ -7,46 +7,54 @@
 use peer\ldap\filter\Filters;
 
 class LdapProtocol {
-  const REQ_BIND = 0x60;
-  const REQ_UNBIND = 0x42;
-  const REQ_SEARCH = 0x63;
-  const REQ_MODIFY = 0x66;
-  const REQ_ADD = 0x68;
-  const REQ_DELETE = 0x4a;
-  const REQ_MODRDN = 0x6c;
-  const REQ_COMPARE = 0x6e;
-  const REQ_ABANDON = 0x50;
-  const REQ_EXTENSION = 0x77;
-
-  const RES_BIND = 0x61;
-  const RES_SEARCH_ENTRY = 0x64;
-  const RES_SEARCH_REF = 0x73;
-  const RES_SEARCH = 0x65;
-  const RES_MODIFY = 0x67;
-  const RES_ADD = 0x69;
-  const RES_DELETE = 0x6b;
-  const RES_MODRDN = 0x6d;
-  const RES_COMPARE = 0x6f;
-  const RES_EXTENSION = 0x78;
-
-  const SCOPE_BASE_OBJECT = 0;
-  const SCOPE_ONE_LEVEL   = 1;
-  const SCOPE_SUBTREE     = 2;
-
-  const NEVER_DEREF_ALIASES = 0;
-  const DEREF_IN_SEARCHING = 1;
-  const DEREF_BASE_OBJECT = 2;
-  const DEREF_ALWAYS = 3;
-
-  const STATUS_OK = 0;
+  const REQ_BIND            = 0x60;
+  const REQ_UNBIND          = 0x42;
+  const REQ_SEARCH          = 0x63;
+  const REQ_MODIFY          = 0x66;
+  const REQ_ADD             = 0x68;
+  const REQ_DELETE          = 0x4a;
+  const REQ_MODRDN          = 0x6c;
+  const REQ_COMPARE         = 0x6e;
+  const REQ_ABANDON         = 0x50;
+  const REQ_EXTENSION       = 0x77;
+
+  const RES_BIND            = 0x61;
+  const RES_SEARCH_ENTRY    = 0x64;
+  const RES_SEARCH_REF      = 0x73;
+  const RES_SEARCH          = 0x65;
+  const RES_MODIFY          = 0x67;
+  const RES_ADD             = 0x69;
+  const RES_DELETE          = 0x6b;
+  const RES_MODRDN          = 0x6d;
+  const RES_COMPARE         = 0x6f;
+  const RES_EXTENSION       = 0x78;
+
+  const SCOPE_BASE_OBJECT   = 0x00;
+  const SCOPE_ONE_LEVEL     = 0x01;
+  const SCOPE_SUBTREE       = 0x02;
+
+  const NEVER_DEREF_ALIASES = 0x00;
+  const DEREF_IN_SEARCHING  = 0x01;
+  const DEREF_BASE_OBJECT   = 0x02;
+  const DEREF_ALWAYS        = 0x03;
+
+  const STATUS_OK           = 0x00;
 
   protected static $continue= [
     self::RES_SEARCH_ENTRY => true,
-    self::RES_EXTENSION => true,
+    self::RES_EXTENSION    => true,
   ];
 
   protected $messageId= 0;
 
+  /**
+   * Instantiate protocol
+   *
+   * @param  string $scheme
+   * @param  string $host
+   * @param  int $port
+   * @param  var $params
+   */
   public function __construct($scheme, $host, $port, $params) {
     if ('ldaps' === $scheme) {
       $this->sock= new SSLSocket($host, $port);