Commits

Michael Granger  committed 8b9fac5

Checkpoint commit

  • Participants
  • Parent commits 54edcd4

Comments (0)

Files changed (14)

File .tm_properties

 [ source.c ]
 tabSize                  = 4
 softTabs                 = false
+disableIndentCorrections = true
 
 
 
 GDB_OPTIONS = []
 
+if Rake.application.trace
+	ENV['RUBYOPT'] = 'w'
+end
+
 
 # Load Hoe plugins
 Hoe.plugin :mercurial

File experiments/async_api.rb

 # Single API, no async
 result = conn.search( base_dn, filter, scope )
 
-thr = Thread.new { conn.search( base_dn, filter, scope ) }
-result = thr.value
+result.next( -1 )
+result.next
+
+result.each -> iter
+
+
+conn.network_timeout = nil
+res = conn.search( ... )
+res.each
+

File ext/openldap_ext/connection.c

 #include "openldap.h"
 
 
-#define MILLION_F 1000000.0
-
-
 /* --------------------------------------------------------------
  * Declarations
  * -------------------------------------------------------------- */
 VALUE ropenldap_cOpenLDAPConnection;
 
 
+ID id_base;
+ID id_subtree;
+ID id_onelevel;
+
+
 /* --------------------------------------------------
  *	Memory-management functions
  * -------------------------------------------------- */
 /*
  * Fetch the data pointer and check it for sanity.
  */
-struct ropenldap_connection *
+static struct ropenldap_connection *
 ropenldap_get_conn( VALUE self )
 {
 	struct ropenldap_connection *conn = check_conn( self );
 }
 
 
+/*
+ * Fetch the LDAP handle from the OpenLDAP::Connection object +connection+.
+ */
+LDAP *
+ropenldap_conn_get_ldap( VALUE connection )
+{
+	struct ropenldap_connection *conn = check_conn( connection );
+	return conn->ldap;
+}
+
 
 /* --------------------------------------------------------------
  * Class methods
 
 	} else {
 		rb_raise( ropenldap_eOpenLDAPError,
-				  "Cannot re-initialize a store once it's been created." );
+				  "Cannot re-initialize a connection once it's been created." );
 	}
 
 	return Qnil;
 
 /*
  * call-seq:
+ *    conn.search_timeout   -> float or nil
+ *
+ * Returns the value that defines the time limit after which a search operation
+ * should be terminated by the server.
+ *
+ *    conn.search_timeout
+ *    # => 2.5
+ */
+static VALUE
+ropenldap_conn_search_timeout( VALUE self )
+{
+	struct ropenldap_connection *ptr = ropenldap_get_conn( self );
+	int timeout = 0;
+
+	if ( ldap_get_option(ptr->ldap, LDAP_OPT_TIMELIMIT, &timeout) != LDAP_OPT_SUCCESS )
+		rb_raise( ropenldap_eOpenLDAPError, "couldn't get option: LDAP_OPT_TIMELIMIT" );
+
+	if ( timeout ) {
+		ropenldap_log_obj( self, "debug", "Got  timeout: %d", timeout );
+		return INT2FIX( timeout );
+	} else {
+		ropenldap_log_obj( self, "debug", "No timeout." );
+		return Qnil;
+	}
+}
+
+
+/*
+ * call-seq:
+ *    conn.search_timeout = int or nil
+ *
+ * Set the the value that defines the time limit after which a search operation
+ * should be terminated by the server. Setting this to nil or -1 disables it.
+ *
+ *    conn.search_timeout = 10
+ */
+static VALUE
+ropenldap_conn_search_timeout_eq( VALUE self, VALUE arg )
+{
+	struct ropenldap_connection *ptr = ropenldap_get_conn( self );
+	int seconds = 0;
+
+	if ( NIL_P(arg) ) {
+		seconds = -1;
+	} else {
+		seconds = NUM2INT( arg );
+	}
+
+	ropenldap_log_obj( self, "debug", "Setting network timeout to %d seconds", seconds );
+	if ( ldap_set_option(ptr->ldap, LDAP_OPT_TIMELIMIT, &seconds) != LDAP_OPT_SUCCESS )
+		rb_raise( ropenldap_eOpenLDAPError, "couldn't set option: LDAP_OPT_TIMELIMIT" );
+
+	return arg;
+}
+
+
+/*
+ * call-seq:
  *    conn.bind( bind_dn, password )   -> result
  *    conn.bind( bind_dn, password ) {|result| ... }
  *
 	int res    = 0;
 	char *who  = NULL;
 	struct berval cred = BER_BVNULL;
-	struct berval *s_cred = NULL;
+	int msgid  = 0;
 
 	rb_scan_args( argc, argv, "02", &bind_dn, &password );
 
 	/* TODO:  async for block form, sync otherwise */
 
 	/* TODO: SASL interactive, ANONYMOUS (RFC2245?) */
-	// int ldap_sasl_bind_s(LDAP *ld, const char *dn, const char *mechanism,
-	//                      struct berval *cred, LDAPControl *sctrls[],
-	//                      LDAPControl *cctrls[], struct berval **servercredp);
-	res = ldap_sasl_bind_s( ptr->ldap, who, LDAP_SASL_SIMPLE,
-	                        &cred, NULL, NULL, &s_cred );
+	// int ldap_sasl_bind(LDAP *ld, const char *dn, const char *mechanism,
+	//               struct berval *cred, LDAPControl *sctrls[],
+	//               LDAPControl *cctrls[], int *msgidp);
+	res = ldap_sasl_bind( ptr->ldap, who, LDAP_SASL_SIMPLE, &cred, NULL, NULL, &msgid );
 	if ( !BER_BVISNULL(&cred) ) {
 		ber_memfree( cred.bv_val );
 		BER_BVZERO( &cred );
 	}
+	ropenldap_log_obj( self, "debug", "Rval from ldap_sasl_bind_a: %d", res );
 	ropenldap_check_result( res, "ldap_sasl_bind_s" );
 
 	return Qtrue;
 }
 
 
+/* Return the corresponding constant for the scope Symbol +scope+ */
+static int
+ropenldap_scope_symbol_to_int( VALUE scope )
+{
+	if ( SYM2ID(scope) == id_base ) {
+		return LDAP_SCOPE_BASE;
+	}
+
+	else if ( SYM2ID(scope) == id_subtree ) {
+		return LDAP_SCOPE_SUBTREE;
+	}
+
+	else if ( SYM2ID(scope) == id_onelevel ) {
+		return LDAP_SCOPE_ONELEVEL;
+	}
+
+	else {
+		rb_raise( rb_eArgError, "Invalid scope %s", RSTRING_PTR(rb_inspect(scope)) );
+	}
+}
+
+
+/* Utility method to derive a scope constant from a ruby scope argument
+   (e.g., :base, :onelevel, or their Fixnum equivalents) */
+static int
+ropenldap_get_scope( VALUE scope )
+{
+	switch( TYPE(scope) ) {
+		case T_SYMBOL:
+		  return ropenldap_scope_symbol_to_int( scope );
+		  break;
+
+		case T_FIXNUM:
+		  return FIX2INT( scope );
+
+		default:
+		  rb_raise( rb_eTypeError, "expected Fixnum or Symbol, got a %s",
+		            rb_obj_classname(scope) );
+	}
+
+	// Unreached
+	return 0;
+}
+
+
+/*
+ * call-seq:
+ *    conn.search( base, scope=:subtree, ... )   -> <return value>
+ *
+ * Execute a search given the args.
+ *
+ *    example code
+ */
+static VALUE
+ropenldap_conn_search( int argc, VALUE *argv, VALUE self )
+{
+	struct ropenldap_connection *ptr = ropenldap_get_conn( self );
+	VALUE rb_base        = Qnil,
+	      rb_scope       = Qnil,
+	      rb_filter      = Qnil,
+	      rb_attrs       = Qnil,
+	      rb_attrsonly   = Qnil,
+	      rb_serverctrls = Qnil,
+	      rb_clientctrls = Qnil,
+	      rb_timeout     = Qnil,
+	      rb_sizelimit   = Qnil;
+	char *base = NULL;
+	int scope = LDAP_SCOPE_SUBTREE;
+	char *filter = NULL;
+	char *attrs[] = {};
+	int attrsonly = 0;
+	LDAPControl *serverctrls[] = {}, *clientctrls[] = {};
+	struct timeval timeout = { 0, 0 };
+	int sizelimit = -1;
+	rb_encoding *utf8 = rb_utf8_encoding();
+
+	// Result
+	int rval = -1;
+	int msgid = 0;
+
+	ropenldap_log_obj( self, "debug", "Searching:" );
+	rb_scan_args( argc, argv, "18",
+	              &rb_base, &rb_scope, &rb_filter, &rb_attrs, &rb_attrsonly,
+				  &rb_serverctrls, &rb_clientctrls, &rb_timeout, &rb_sizelimit );
+
+	// Base
+	SafeStringValue( rb_base );
+	rb_base = rb_str_encode( rb_filter, rb_enc_from_encoding(utf8), 0, Qnil );
+	rb_gc_register_address( &rb_base );
+	base = StringValueCStr( rb_base );
+	ropenldap_log_obj( self, "debug", "  search base set to '%s'", base );
+
+	// Scope
+	if ( !NIL_P(rb_scope) ) scope = ropenldap_get_scope( rb_scope );
+	ropenldap_log_obj( self, "debug", "  search scope set to %d", scope );
+
+	// Filter
+	if ( !NIL_P(rb_filter) ) {
+		SafeStringValue( rb_filter );
+		rb_filter = rb_str_encode( rb_filter, rb_enc_from_encoding(utf8), 0, Qnil );
+		rb_gc_register_address( &rb_filter );
+		filter = StringValueCStr( rb_filter );
+	}
+
+	// Do the search
+	rval = ldap_search_ext( ptr->ldap, base, scope, filter, attrs, attrsonly,
+	                        serverctrls, clientctrls, &timeout, sizelimit,
+	                        &msgid );
+
+	// Release all of the strings we were using
+	rb_gc_unregister_address( &rb_base );
+	rb_gc_unregister_address( &rb_filter );
+
+	// Check the results of the search and raise if there was a problem
+	ropenldap_check_result( rval, "ldap_search_ext( %s, %d, %s )", base, scope, filter );
+
+	// Some other stuff
+
+	return Qnil;
+}
+
 
 
 /*
 	ropenldap_mOpenLDAP = rb_define_module( "OpenLDAP" );
 #endif
 
+	id_base     = rb_intern_const( "base" );
+	id_subtree  = rb_intern_const( "subtree" );
+	id_onelevel = rb_intern_const( "onelevel" );
+
 	/* OpenLDAP::Connection */
 	ropenldap_cOpenLDAPConnection =
 		rb_define_class_under( ropenldap_mOpenLDAP, "Connection", rb_cObject );
 	rb_define_alias(  ropenldap_cOpenLDAPConnection, "fileno", "fdno" );
 	rb_define_method( ropenldap_cOpenLDAPConnection, "bind", ropenldap_conn_bind, -1 );
 
+	rb_define_method( ropenldap_cOpenLDAPConnection, "search", ropenldap_conn_search, 1 );
+	rb_define_alias ( ropenldap_cOpenLDAPConnection, "search_ext", "search" );
+
+	/* Options */
 	rb_define_method( ropenldap_cOpenLDAPConnection, "protocol_version",
 	                  ropenldap_conn_protocol_version, 0 );
 	rb_define_method( ropenldap_cOpenLDAPConnection, "protocol_version=",
 	rb_define_method( ropenldap_cOpenLDAPConnection, "network_timeout=",
 	                  ropenldap_conn_network_timeout_eq, 1 );
 
+	rb_define_method( ropenldap_cOpenLDAPConnection, "search_timeout",
+	                  ropenldap_conn_search_timeout, 0 );
+	rb_define_method( ropenldap_cOpenLDAPConnection, "search_timeout=",
+	                  ropenldap_conn_search_timeout_eq, 1 );
+	rb_define_alias ( ropenldap_cOpenLDAPConnection, "timelimit", "search_timeout" );
+	// rb_define_method( ropenldap_cOpenLDAPConnection, "result_timeout",
+	//                   ropenldap_conn_result_timeout, 0 );
+	// rb_define_method( ropenldap_cOpenLDAPConnection, "result_timeout=",
+	//                   ropenldap_conn_result_timeout_eq, 1 );
+
 	rb_define_method( ropenldap_cOpenLDAPConnection, "tls_inplace?",
 	                  ropenldap_conn_tls_inplace_p, 0 );
 
-	/* Options */
+	/* TLS Options */
 	rb_define_method( ropenldap_cOpenLDAPConnection, "tls_cacertfile",
 	                  ropenldap_conn_tls_cacertfile, 0 );
 	rb_define_method( ropenldap_cOpenLDAPConnection, "tls_cacertfile=",
 	rb_define_method( ropenldap_cOpenLDAPConnection, "create_new_tls_context",
 	                  ropenldap_conn_create_new_tls_context, 0 );
 
+
+
 	/* Methods with Ruby front-ends */
 	rb_define_protected_method( ropenldap_cOpenLDAPConnection, "_start_tls",
 	                            ropenldap_conn__start_tls, 0 );

File ext/openldap_ext/extconf.rb

 end
 
 
-# if dirs = dir_config( 'openldap' )
-#	$stderr.puts "Adding rpath pointing to #{dirs.last}lib"
-#	$LDFLAGS << " -Wl,-rpath #{dirs.last}lib"
-# end
+inc_dir, lib_dir = dir_config( 'ldap' )
+if lib_dir
+	$LDFLAGS << " -L%s -Wl,-rpath,%s" % [ lib_dir, lib_dir ]
+end
 
 have_func 'rb_thread_call_without_gvl' or abort "no rb_thread_call_without_gvl()"
 have_func 'rb_thread_call_with_gvl' or abort "no rb_thread_call_with_gvl()"

File ext/openldap_ext/message.c

+/*
+ * Ruby-OpenLDAP -- OpenLDAP::Message class
+ * $Id$
+ *
+ * Authors
+ *
+ * - Michael Granger <ged@FaerieMUD.org>
+ * - Mahlon E. Smith <mahlon@martini.nu>
+ *
+ * Copyright (c) 2013 Michael Granger and Mahlon E. Smith
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice, this
+ *    list of conditions and the following disclaimer in the documentation and/or
+ *    other materials provided with the distribution.
+ *
+ *  * Neither the name of the authors, nor the names of its contributors may be used to
+ *    endorse or promote products derived from this software without specific prior
+ *    written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#include "openldap.h"
+
+
+
+/* --------------------------------------------------------------
+ * Declarations
+ * -------------------------------------------------------------- */
+VALUE ropenldap_cOpenLDAPMessage;
+
+
+
+/* --------------------------------------------------------------
+ * Global functions
+ * -------------------------------------------------------------- */
+
+static void ropenldap_message_gc_mark( struct ropenldap_message * );
+static void ropenldap_message_gc_free( struct ropenldap_message * );
+
+/*
+ * Message constructor
+ */
+VALUE
+ropenldap_new_message( VALUE conn, LDAPMessage *msg )
+{
+	struct ropenldap_message *ptr = ALLOC( struct ropenldap_message );
+
+	ptr->connection = conn;
+	ptr->msg        = msg;
+
+	return Data_Wrap_Struct( ropenldap_cOpenLDAPMessage, ropenldap_message_gc_free,
+	                         ropenldap_message_gc_mark, ptr );
+}
+
+
+/* --------------------------------------------------
+ *	Memory-management functions
+ * -------------------------------------------------- */
+
+/*
+ * GC Mark function
+ */
+static void
+ropenldap_message_gc_mark( struct ropenldap_message *ptr )
+{
+	if ( ptr ) rb_gc_mark( ptr->connection );
+}
+
+
+
+/*
+ * GC Free function
+ */
+static void
+ropenldap_message_gc_free( struct ropenldap_message *ptr )
+{
+	if ( ptr ) {
+		ptr->connection = Qnil;
+		ptr->msg        = NULL;
+
+		xfree( ptr );
+		ptr = NULL;
+	}
+}
+
+
+/*
+ * Object validity checker. Returns the data pointer.
+ */
+static struct ropenldap_message *
+check_message( VALUE self )
+{
+	Check_Type( self, T_DATA );
+
+    if ( !IsMessage(self) ) {
+		rb_raise( rb_eTypeError, "wrong argument type %s (expected an OpenLDAP::Message)",
+				  rb_obj_classname( self ) );
+    }
+
+	return DATA_PTR( self );
+}
+
+
+/*
+ * Fetch the data pointer and check it for sanity.
+ */
+static struct ropenldap_message *
+ropenldap_get_message( VALUE self )
+{
+	struct ropenldap_message *msg = check_message( self );
+
+	if ( !msg ) rb_fatal( "Use of uninitialized OpenLDAP::Message" );
+
+	return msg;
+}
+
+
+
+/* --------------------------------------------------------------
+ * Instance methods
+ * -------------------------------------------------------------- */
+
+static VALUE
+ropenldap_message_count( VALUE self )
+{
+	struct ropenldap_message *ptr = ropenldap_get_message( self );
+	LDAP *ldap = ropenldap_conn_get_ldap( ptr->connection );
+	int count = 0;
+
+	count = ldap_count_messages( ldap, ptr->msg );
+	if ( count == -1 )
+		ropenldap_check_result( count, "ldap_count_messages" );
+
+	return INT2FIX( count );
+}
+
+
+/*
+ * document-class: OpenLDAP::Message
+ */
+void
+ropenldap_init_message( void )
+{
+	ropenldap_log( "debug", "Initializing OpenLDAP::Message" );
+
+#ifdef FOR_RDOC
+	ropenldap_mOpenLDAP = rb_define_module( "OpenLDAP" );
+#endif
+
+	/* OpenLDAP::Result */
+	ropenldap_cOpenLDAPMessage =
+		rb_define_class_under( ropenldap_mOpenLDAP, "Message", rb_cObject );
+
+	rb_define_method( ropenldap_cOpenLDAPMessage, "count", ropenldap_message_count, 0 );
+
+	rb_require( "openldap/message" );
+}
+

File ext/openldap_ext/openldap.c

 	LDAPAPIInfo info;
 	info.ldapai_info_version = LDAP_API_INFO_VERSION;
 
-	if ( ldap_get_option(NULL, LDAP_OPT_API_INFO, &info) != LDAP_SUCCESS )
+	if ( ldap_get_option(NULL, LDAP_OPT_API_INFO, &info) != LDAP_OPT_SUCCESS )
 		rb_raise( ropenldap_eOpenLDAPError, "ldap_get_option(API_INFO) failed." );
 
 	rb_hash_aset( rval, ID2SYM(rb_intern("api_version")), INT2FIX(info.ldapai_api_version) );
-	rb_hash_aset( rval, ID2SYM(rb_intern("protocol_version")), INT2FIX(info.ldapai_protocol_version) );
-	rb_hash_aset( rval, ID2SYM(rb_intern("extensions")), ropenldap_rb_string_array(info.ldapai_extensions) );
+	rb_hash_aset( rval, ID2SYM(rb_intern("protocol_version")),
+	              INT2FIX(info.ldapai_protocol_version) );
+	rb_hash_aset( rval, ID2SYM(rb_intern("extensions")),
+	              ropenldap_rb_string_array(info.ldapai_extensions) );
 	rb_hash_aset( rval, ID2SYM(rb_intern("vendor_name")), rb_str_new2(info.ldapai_vendor_name) );
 	rb_hash_aset( rval, ID2SYM(rb_intern("vendor_version")), INT2FIX(info.ldapai_vendor_version) );
 
 	LDAPAPIInfo info;
 	info.ldapai_info_version = LDAP_API_INFO_VERSION;
 
-	if ( ldap_get_option(NULL, LDAP_OPT_API_INFO, &info) != LDAP_SUCCESS )
+	if ( ldap_get_option(NULL, LDAP_OPT_API_INFO, &info) != LDAP_OPT_SUCCESS )
 		rb_raise( ropenldap_eOpenLDAPError, "ldap_get_option(API_INFO) failed." );
 
 	for( i=0; info.ldapai_extensions[i] != NULL; i++ ) {
 		fi.ldapaif_name = info.ldapai_extensions[i];
 		fi.ldapaif_version = 0;
 
-		if ( ldap_get_option(NULL, LDAP_OPT_API_FEATURE_INFO, &fi) == LDAP_SUCCESS ) {
+		if ( ldap_get_option(NULL, LDAP_OPT_API_FEATURE_INFO, &fi) == LDAP_OPT_SUCCESS ) {
 			if(fi.ldapaif_info_version == LDAP_FEATURE_INFO_VERSION) {
 				rb_hash_aset( rval, rb_str_new2(fi.ldapaif_name), INT2FIX(fi.ldapaif_version) );
 			} else {
 	VALUE rval;
 	char *uris;
 
-	if ( ldap_get_option(NULL, LDAP_OPT_URI, &uris) != LDAP_SUCCESS )
+	if ( ldap_get_option(NULL, LDAP_OPT_URI, &uris) != LDAP_OPT_SUCCESS )
 		rb_raise( ropenldap_eOpenLDAPError, "ldap_get_option(URI) failed." );
 
 	rval = rb_str_new2( uris );
 }
 
 
+static void
+ropenldap_check_api_version()
+{
+	LDAPAPIInfo info;
+	info.ldapai_info_version = LDAP_API_INFO_VERSION;
+
+	if ( ldap_get_option(NULL, LDAP_OPT_API_INFO, &info) != LDAP_OPT_SUCCESS )
+		rb_raise( ropenldap_eOpenLDAPError, "ldap_get_option(API_INFO) failed." );
+
+	if ( info.ldapai_api_version != LDAP_API_VERSION ) {
+		rb_warn( "Extension was compiled for API version %d,\nbut your library is version %d.",
+		         LDAP_API_VERSION, info.ldapai_api_version );
+	 }
+
+	ldap_memfree( info.ldapai_vendor_name );
+	ber_memvfree( (void **)info.ldapai_extensions );
+}
+
 
 void
 Init_openldap_ext( void )
 {
+	ropenldap_check_api_version();
+
 	rb_require( "uri" );
 	ropenldap_rbmURI = rb_const_get( rb_cObject, rb_intern("URI") );
 
 	rb_define_singleton_method( ropenldap_mOpenLDAP, "split_url", ropenldap_s_split_url, 1 );
 	rb_define_singleton_method( ropenldap_mOpenLDAP, "err2string", ropenldap_s_err2string, 1 );
 	rb_define_singleton_method( ropenldap_mOpenLDAP, "api_info", ropenldap_s_api_info, 0 );
-	rb_define_singleton_method( ropenldap_mOpenLDAP, "api_feature_info", ropenldap_s_api_feature_info, 0 );
+	rb_define_singleton_method( ropenldap_mOpenLDAP, "api_feature_info",
+	                            ropenldap_s_api_feature_info, 0 );
 
 	rb_define_singleton_method( ropenldap_mOpenLDAP, "uris", ropenldap_s_uris, 0 );
 

File ext/openldap_ext/openldap.h

 
 #include <ruby.h>
 #include <ruby/thread.h>
+#include <ruby/encoding.h>
+#include <ruby/intern.h>
 
 #include "extconf.h"
 
 
 extern VALUE ropenldap_cOpenLDAPConnection;
 extern VALUE ropenldap_cOpenLDAPResult;
+extern VALUE ropenldap_cOpenLDAPMessage;
 
 extern VALUE ropenldap_eOpenLDAPError;
 
 
 /* OpenLDAP::Connection struct */
 struct ropenldap_connection {
-    LDAP    *ldap;
+    LDAP *ldap;
 };
 
 /* OpenLDAP::Result struct */
 struct ropenldap_result {
-	LDAP        *ldap;
-	int         msgid;
-	LDAPMessage *message;
+	int   msgid;
+	VALUE connection;
+	VALUE abandoned;
+};
+
+struct ropenldap_message {
+	LDAPMessage *msg;
 	VALUE       connection;
-	VALUE       abandoned;
 };
 
+
 /* --------------------------------------------------------------
  * Macros
  * -------------------------------------------------------------- */
 #define IsConnection( obj ) rb_obj_is_kind_of( (obj), ropenldap_cOpenLDAPConnection )
 #define IsResult( obj ) rb_obj_is_kind_of( (obj), ropenldap_cOpenLDAPResult )
+#define IsMessage( obj ) rb_obj_is_kind_of( (obj), ropenldap_cOpenLDAPMessage )
 
 #ifdef UNUSED
 #elif defined(__GNUC__)
 #define BER_BVISNULL(bv)	((bv)->bv_val == NULL)
 #define BER_BVISEMPTY(bv)	((bv)->bv_len == 0)
 
+// Convert decimal seconds to milliseconds
+#define MILLION_F 1000000.0
+
+
+
 
 /* --------------------------------------------------------------
  * Declarations
 void Init_openldap_ext                  _(( void ));
 void ropenldap_init_connection          _(( void ));
 void ropenldap_init_result              _(( void ));
+void ropenldap_init_message             _(( void ));
 
-struct ropenldap_connection *ropenldap_get_conn _(( VALUE ));
+LDAP *ropenldap_conn_get_ldap           _(( VALUE ));
+VALUE ropenldap_new_message             _(( VALUE, LDAPMessage * ));
 
 
 #endif /* __OPENLDAP_H__ */

File ext/openldap_ext/result.c

  *
  * - Michael Granger <ged@FaerieMUD.org>
  *
- * Copyright (c) 2011 Michael Granger
+ * Copyright (c) 2011-2013 Michael Granger
  *
  * All rights reserved.
  *
 ropenldap_result_alloc( VALUE connection, int msgid )
 {
 	struct ropenldap_result *ptr = ALLOC( struct ropenldap_result );
-	struct ropenldap_connection *conn = ropenldap_get_conn( connection );
 
-	ptr->ldap       = conn->ldap;
 	ptr->msgid      = FIX2INT( msgid );
-	ptr->message    = NULL;
 	ptr->connection = connection;
 	ptr->abandoned  = Qfalse;
 
 ropenldap_result_gc_free( struct ropenldap_result *ptr )
 {
 	if ( ptr ) {
-		ptr->ldap       = NULL;
 		ptr->msgid      = 0;
-		ptr->message    = NULL;
 		ptr->connection = Qnil;
 		ptr->abandoned  = Qfalse;
 
 ropenldap_result_abandon( int argc, VALUE *argv, VALUE self )
 {
 	struct ropenldap_result *ptr = ropenldap_get_result( self );
+	LDAP *ldap = ropenldap_conn_get_ldap( ptr->connection );
 	int res;
 
 	/* :TODO: controls */
 
-	res = ldap_abandon_ext( ptr->ldap, ptr->msgid, NULL, NULL );
+	res = ldap_abandon_ext( ldap, ptr->msgid, NULL, NULL );
 	ropenldap_check_result( res, "ldap_abandon_ext" );
 
 	return Qtrue;
 }
 
 
+/*
+ * call-seq:
+ *    next              -> message or nil
+ *    next( timeout )   -> message or nil
+ *
+ * Fetch the next result if it's ready.
+ *
+ */
+static VALUE
+ropenldap_result_next( int argc, VALUE *argv, VALUE self )
+{
+	struct ropenldap_result *ptr = ropenldap_get_result( self );
+	LDAP *ldap = ropenldap_conn_get_ldap( ptr->connection );
+	VALUE timeout = Qnil;
+	VALUE message = Qnil;
+	LDAPMessage *msg = NULL;
+	struct timeval c_timeout = { 0, 0 };
+	int res = 0;
+
+	rb_scan_args( argc, argv, "01", &timeout );
+
+	if ( !NIL_P(timeout) ) {
+		double seconds = NUM2DBL( timeout );
+		c_timeout.tv_sec = (time_t)floor( seconds );
+		c_timeout.tv_usec = (suseconds_t)( fmod(seconds, 1.0) * MILLION_F );
+	}
+
+	// int ldap_result( LDAP *ld, int msgid, int all,
+	//             struct timeval *timeout, LDAPMessage **result );
+	res = ldap_result( ldap, ptr->msgid, 0, &c_timeout, &msg );
+	ropenldap_check_result( res, "ldap_result" );
+
+	message = ropenldap_new_message( ptr->connection, msg );
+
+	return message;
+}
+
 
 
 /*
 	                            ropenldap_result_initialize, 2 );
 
 	rb_define_method( ropenldap_cOpenLDAPResult, "abandon", ropenldap_result_abandon, -1 );
+	rb_define_method( ropenldap_cOpenLDAPResult, "next", ropenldap_result_next, -1 );
 
 	rb_require( "openldap/result" );
 }

File lib/openldap/message.rb

+# -*- ruby -*-
+#encoding: utf-8
+
+require 'uri'
+require 'openldap' unless defined?( OpenLDAP )
+
+# OpenLDAP Message class
+class OpenLDAP::Message
+	extend Loggability
+
+	# Loggability API -- log to the openldap logger.
+	log_to :openldap
+
+end # class OpenLDAP::Message
+
+

File spec/constants.rb

 		TEST_LDAPS_STRING    = 'ldaps://localhost:6364'
 		TEST_LDAPS_URI       = URI( TEST_LDAPS_STRING )
 
-		TEST_LDAPBASE_STRING = "#{TEST_LDAP_STRING}/dc=example,dc=com"
+		TEST_BASE            = 'dc=example,dc=com'
+
+		TEST_LDAPBASE_STRING = "#{TEST_LDAP_STRING}/#{TEST_BASE}"
 		TEST_LDAPBASE_URI    = URI( TEST_LDAPBASE_STRING )
 
-		TEST_ADMIN_ROOT_DN   = "cn=admin,dc=example,dc=com"
+		TEST_ADMIN_ROOT_DN   = "cn=admin,#{TEST_BASE}"
 		TEST_ADMIN_PASSWORD  = 'secret'
 
 		constants.each do |cname|

File spec/helpers.rb

 #!/usr/bin/ruby
 # coding: utf-8
 
+require 'rspec'
+
 require 'erb'
 require 'pathname'
 require 'shellwords'
 	include FileUtils::Verbose if $DEBUG || $VERBOSE
 
 
-	BASEDIR        = Pathname( __FILE__ ).dirname.parent.parent
+	BASEDIR        = Pathname( __FILE__ ).dirname.parent
 
 	TESTING_SLAPD_URI = 'ldap://localhost:6363/dc=example,dc=com'
 	TESTING_SLAPD_SSL_URI = 'ldaps://localhost:6364/dc=example,dc=com'
 	SPEC_LDIF      = SPEC_DATADIR + 'testdata.ldif'
 
 
+	### Output the specified +msg+ if VERBOSE.
+	def trace( *msg )
+		# return unless $VERBOSE
+		$stderr.puts( *msg )
+	end
+
+
 	### Start a localized slapd daemon for testing.
 	def start_testing_slapd
 
 			self.copy_test_files
 			self.generate_ssl_cert
 			self.install_initial_data
+		else
+			trace "Re-using existing test datadir #{TEST_DATADIR}"
 		end
 
 		return self.start_slapd
 	### Stop the slapd started by #start_testing_slapd, if it's still alive.
 	def stop_testing_slapd( pid )
 		if pid
-			$stderr.puts "Shutting down slapd at PID %p" % [ pid ]
+			trace "Shutting down slapd at PID %p" % [ pid ]
 			begin
 				Process.kill( :TERM, pid )
 				Process.waitpid2( pid )
 			rescue Errno::ESRCH
-				$stderr.puts "  not running."
+				trace "  not running."
 				# Not running
 			rescue Errno::EPERM
-				$stderr.puts "  not allowed (not slapd?)."
+				trace "  not allowed (not slapd?)."
 			else
-				$stderr.puts "  killed."
+				trace "  killed."
 			end
 		end
 	end
 
 	### Create the directory used for the testing instance of slapd.
 	def create_test_directories
+		trace "Creating test directory #{TEST_DATADIR}"
 		TEST_DATADIR.mkpath
 		return TEST_WORKDIR
 	end
 	### Generate a self-signed cert for testing SSL/TlS connections
 	### Mostly stolen from https://gist.github.com/nickyp/886884
 	def generate_ssl_cert
-		request_key  = TEST_WORKDIR + 'example.key.org'
 		cert_request = TEST_WORKDIR + 'example.csr'
 		signing_key  = TEST_WORKDIR + 'example.key'
 		cert         = TEST_WORKDIR + 'example.crt'
 			system 'openssl', 'req',
 				'-new',
 				'-subj', '/C=US/ST=Oregon/L=Portland/O=IT/CN=localhost',
-				'-key', request_key.to_s,
+				'-key', signing_key.to_s,
 				'-out', cert_request.to_s
 
 			system 'openssl', 'rsa',
-				'-in', request_key.to_s,
+				'-in', signing_key.to_s,
 				'-out', signing_key.to_s
 
 			system 'openssl', 'x509',
 			'-l', ldiffile
 		]
 
-		$stderr.puts( ">>> ", Shellwords.join(cmd) )
+		trace ">>> ", Shellwords.join(cmd)
 		system( *cmd, chdir: TEST_WORKDIR.to_s ) or
 			raise "Couldn't load initial data: #{Shellwords.join(cmd)}"
-		$stderr.puts "installed."
+		trace "installed."
 	end
 
 
 	### Start the testing slapd and keep track of its PID.
 	def start_slapd
-		$stderr.print "Starting up testing slapd..."
+		trace "Starting up testing slapd..."
 		slapd = self.find_binary( 'slapd' )
 		logio = File.open( TEST_WORKDIR + 'slapd.log', 'w' )
 
 			'-h', "ldap://localhost:6363 ldaps://localhost:6364"
 		]
 
-		$stderr.puts( ">>> ", Shellwords.join(cmd) )
+		trace ">>> ", Shellwords.join( cmd )
 		pid = spawn( *cmd, chdir: TEST_WORKDIR.to_s, [:out,:err] => logio )
 
-		$stderr.puts "started at PID %d" % [ pid ]
+		trace "started at PID %d" % [ pid ]
 		return pid
 	end
 

File spec/openldap/connection_spec.rb

 					@conn.bind( 'cn=nonexistant', 'nopenopenope' )
 				}.to raise_error( OpenLDAP::InvalidCredentials, /bind/i )
 			end
+
+
+			it "requires a base to search" do
+				expect {
+					@conn.search
+				}.to raise_error( ArgumentError, /0 for 1/i )
+			end
+
+			it "returns all entries under the base without a filter" do
+				result = @conn.search( TEST_BASE )
+				result.count.should == 3
+			end
+
+			it "raises an appropriate exception on an invalid filter" do
+				expect {
+					@conn.search( TEST_BASE, :subtree, "(objectClass=*" );
+				}.to raise_error( OpenLDAP::FilterError, /invalid/i )
+			end
+
+
 		end
 
+
 	end
 
 end

File spec/openldap/message_spec.rb

+#!/usr/bin/env rspec -cfd -b
+
+require_relative '../helpers'
+
+require 'rspec'
+require 'openldap/message'
+
+describe OpenLDAP::Message do
+
+	before( :all ) do
+		setup_logging( :fatal )
+	end
+
+	before( :each ) do
+		@ldap = OpenLDAP.connect( 'ldap://localhost' )
+		# @result = @ldap.search_ext( )
+	end
+
+	after( :all ) do
+		reset_logging()
+	end
+
+
+	it "can be created with a connection and a message ID" do
+		result = OpenLDAP::Result.new( )
+	end
+
+end
+