/*
 * conn.c
 * $Id: conn.c,v 1.11 2003/12/06 17:33:39 ttate Exp $
 */

#include "ruby.h"
#include "rbldap.h"
#include <sys/time.h>
#include <unistd.h>

static VALUE rb_ldap_sort_obj = Qnil;

VALUE rb_cLDAP_Conn;

static void
rb_ldap_conn_free(RB_LDAP_DATA *ldapdata)
{
  if( ldapdata->ldap && ldapdata->bind ){
    ldap_unbind(ldapdata->ldap);
  };
};

static void
rb_ldap_conn_mark(RB_LDAP_DATA *ldapdata)
{
  /* empty */
};

VALUE
rb_ldap_conn_new(VALUE klass, LDAP *cldap)
{
  VALUE conn;
  RB_LDAP_DATA *ldapdata;

  conn = Data_Make_Struct(klass, RB_LDAP_DATA,
			  rb_ldap_conn_mark,rb_ldap_conn_free,ldapdata);
  ldapdata->ldap = cldap;
  ldapdata->err  = 0;
  ldapdata->bind = 0;

  return conn;
};


VALUE
rb_ldap_conn_s_allocate(VALUE klass)
{
  return rb_ldap_conn_new(klass,(LDAP*)0);
}

VALUE
rb_ldap_conn_initialize(int argc, VALUE argv[], VALUE self)
{
  LDAP *cldap;
  char *chost;
  int cport;
  RB_LDAP_DATA *ldapdata;

  VALUE arg1, arg2;

  Data_Get_Struct(self, RB_LDAP_DATA, ldapdata);
  if( ldapdata->ldap ){
    return Qnil;
  }

  switch( rb_scan_args(argc,argv,"02",&arg1,&arg2) ){
  case 0:
    chost = ALLOCA_N(char, strlen("localhost")+1);
    strcpy(chost, "localhost");
    cport = LDAP_PORT;
    break;
  case 1:
    chost = StringValueCStr(arg1);
    cport = LDAP_PORT;
    break;
  case 2:
    chost = StringValueCStr(arg1);
    cport = NUM2INT(arg2);
    break;
  default:
    rb_bug("rb_ldap_conn_new");
  };
  
  cldap = ldap_init(chost,cport);
  if( !cldap )
    rb_raise(rb_eLDAP_ResultError,"can't initialize a ldap session");
  ldapdata->ldap = cldap;

  return Qnil;
};

VALUE
rb_ldap_conn_s_open(int argc, VALUE argv[], VALUE klass)
{
  LDAP *cldap;
  char *chost;
  int cport;

  VALUE arg1,arg2;
  VALUE conn;

  switch( rb_scan_args(argc,argv,"02",&arg1,&arg2) ){
  case 0:
    chost = ALLOCA_N(char, strlen("localhost")+1);
    strcpy(chost, "localhost");
    cport = LDAP_PORT;
    break;
  case 1:
    chost = StringValueCStr(arg1);
    cport = LDAP_PORT;
    break;
  case 2:
    chost = StringValueCStr(arg1);
    cport = NUM2INT(arg2);
    break;
  default:
    rb_bug("rb_ldap_conn_new");
  };

  cldap = ldap_open(chost,cport);
  if( !cldap )
    rb_raise(rb_eLDAP_ResultError,"can't open a ldap session");
  conn = rb_ldap_conn_new(klass, cldap);


  return conn;
};

VALUE
rb_ldap_conn_start_tls_s(int argc, VALUE argv[], VALUE self)
{
#ifdef HAVE_LDAP_START_TLS_S
  VALUE arg1, arg2;
  RB_LDAP_DATA *ldapdata;
  LDAPControl **serverctrls;
  LDAPControl **clientctrls;
  int version = 3;

  switch( rb_scan_args(argc, argv, "02", &arg1, &arg2) ){
  case 0:
    serverctrls = NULL;
    clientctrls = NULL;
    break;
  case 1:
  case 2:
    rb_notimplement();
  default:
    rb_bug("rb_ldap_conn_start_tls_s");
  };

  GET_LDAP_DATA(self, ldapdata);
  ldapdata->err = ldap_start_tls_s(ldapdata->ldap, serverctrls, clientctrls);
  Check_LDAP_Result(ldapdata->err);
#else
  rb_notimplement();
#endif
  return Qnil;
};


VALUE
rb_ldap_conn_simple_bind_s(int argc, VALUE argv[], VALUE self)
{
  RB_LDAP_DATA *ldapdata;

  VALUE arg1, arg2;
  char *dn = NULL;
  char *passwd = NULL;

  GET_LDAP_DATA(self,ldapdata);
  if( ldapdata->bind ){
    rb_raise(rb_eLDAP_Error, "already binded.");
  };
  switch( rb_scan_args(argc, argv, "02", &arg1, &arg2) ){
  case 0:
    break;
  case 1:
    if( arg1 == Qnil ){
      dn = NULL;
    }
    else{
      dn = StringValueCStr(arg1);
    }
    break;
  case 2:
    if( arg1 == Qnil ){
      dn = NULL;
    }
    else{
      dn = StringValueCStr(arg1);
    }
    if( arg2 == Qnil ){
      passwd = NULL;
    }
    else{
      passwd = StringValueCStr(arg2);
    }
    break;
  default:
    rb_bug("rb_ldap_conn_simple_bind_s");
  }

  ldapdata->err = ldap_simple_bind_s(ldapdata->ldap, dn, passwd);
  Check_LDAP_Result(ldapdata->err);
  ldapdata->bind = 1;

  if( rb_block_given_p() ){
    rb_ensure(rb_yield, self, rb_ldap_conn_unbind, self);
    return Qnil;
  }
  else{
    return self;
  };
};

VALUE
rb_ldap_conn_bind_s(int argc, VALUE argv[], VALUE self)
{
  RB_LDAP_DATA *ldapdata;

  VALUE arg1, arg2, arg3;
  char *dn = NULL;
  char *passwd = NULL;
  int method = LDAP_AUTH_SIMPLE;

  GET_LDAP_DATA(self,ldapdata);
  if( ldapdata->bind ){
    rb_raise(rb_eLDAP_Error, "already binded.");
  };
  switch( rb_scan_args(argc, argv, "03", &arg1, &arg2, &arg3) ){
  case 0:
    break;
  case 1:
    dn = StringValueCStr(arg1);
    break;
  case 2:
    dn = StringValueCStr(arg1);
    passwd = StringValueCStr(arg2);
    break;
  case 3:
    dn = StringValueCStr(arg1);
    passwd = StringValueCStr(arg2);
    method = NUM2INT(arg3);
    break;
  default:
    rb_bug("rb_ldap_conn_bind_s");
  }

  ldapdata->err = ldap_bind_s(ldapdata->ldap, dn, passwd, method);
  Check_LDAP_Result(ldapdata->err);
  ldapdata->bind = 1;

  if( rb_block_given_p() ){
    rb_ensure(rb_yield, self, rb_ldap_conn_unbind, self);
    return Qnil;
  }
  else{
    return self;
  };
};


VALUE
rb_ldap_conn_unbind(VALUE self)
{
  RB_LDAP_DATA *ldapdata;

  VALUE result;

  GET_LDAP_DATA(self,ldapdata);
  ldapdata->err = ldap_unbind(ldapdata->ldap);
  ldapdata->bind = 0;
  ldapdata->ldap = NULL;
  Check_LDAP_Result(ldapdata->err);

  return Qnil;
};

VALUE
rb_ldap_conn_set_option(VALUE self, VALUE opt, VALUE data)
{
  /* ldap_set_option() is defined in IETF draft */
#ifdef HAVE_LDAP_SET_OPTION
  RB_LDAP_DATA *ldapdata;
  int  idata;
  void *optdata;
  int copt;

  GET_LDAP_DATA(self, ldapdata);
  copt = NUM2INT(opt);

  switch( copt ){
  case LDAP_OPT_REFERRALS:
     optdata = (void*)NUM2INT(data);
     break;
  case LDAP_OPT_DEREF:
  case LDAP_OPT_SIZELIMIT:
  case LDAP_OPT_TIMELIMIT:
  case LDAP_OPT_RESTART:
  case LDAP_OPT_PROTOCOL_VERSION:
  case LDAP_OPT_ERROR_NUMBER:
#ifdef LDAP_OPT_SSL
  case LDAP_OPT_SSL:
#endif
    idata = NUM2INT(data);
    optdata = &idata;
    break;
  case LDAP_OPT_HOST_NAME:
  case LDAP_OPT_ERROR_STRING:
#ifdef LDAP_OPT_MATCHED_DN
  case LDAP_OPT_MATCHED_DN:
#endif
#ifdef USE_OPENLDAP2
#ifdef LDAP_OPT_X_TLS
  case LDAP_OPT_X_TLS:
#endif
#ifdef LDAP_OPT_X_TLS_CACERTFILE
  case LDAP_OPT_X_TLS_CACERTFILE:
#endif
#ifdef LDAP_OPT_X_TLS_CACERTDIR
  case LDAP_OPT_X_TLS_CACERTDIR:
#endif
#ifdef LDAP_OPT_X_TLS_CERT
  case LDAP_OPT_X_TLS_CERT:
#endif
#ifdef LDAP_OPT_X_TLS_CERTFILE
  case LDAP_OPT_X_TLS_CERTFILE:
#endif
#ifdef LDAP_OPT_X_TLS_KEYFILE
  case LDAP_OPT_X_TLS_KEYFILE:
#endif
#ifdef LDAP_OPT_X_TLS_PROTOCOL
  case LDAP_OPT_X_TLS_PROTOCOL:
#endif
#ifdef LDAP_OPT_X_TLS_REQUIRE_CERT
  case LDAP_OPT_X_TLS_REQUIRE_CERT:
#endif
#ifdef LDAP_OPT_X_TLS_CIPHER_SUITE
  case LDAP_OPT_X_TLS_CIPHER_SUITE:
#endif
#ifdef LDAP_OPT_X_TLS_RANDOM_FILE
  case LDAP_OPT_X_TLS_RANDOM_FILE:
#endif
#endif
    optdata = StringValueCStr(data);
    break;
#ifdef LDAP_OPT_API_INFO
  case LDAP_OPT_API_INFO:
    rb_raise(rb_eLDAP_Error, "option is READ-ONLY");
    /* optdata = (void*)rb_ldap_get_apiinfo(data); */
    break;
#endif
  default:
    rb_notimplement();
  }
  ldapdata->err = ldap_set_option(ldapdata->ldap, copt, optdata);
  Check_LDAP_OPT_Result(ldapdata->err);

  return self;
#else
  rb_notimplement();
#endif
};


VALUE
rb_ldap_conn_get_option(VALUE self, VALUE opt)
{
#ifdef HAVE_LDAP_GET_OPTION
  RB_LDAP_DATA *ldapdata;
  long *data;
  int copt;
  VALUE val;

  GET_LDAP_DATA(self, ldapdata);
  copt = NUM2INT(opt);

#if defined(LDAP_OPT_API_INFO) && defined(LDAP_API_INFO_VERSION)
  if( copt == LDAP_OPT_API_INFO ){
    LDAPAPIInfo *info;

    info = ALLOCA_N(LDAPAPIInfo, 1);
    /* This is from the Netscape SDK docs for 4.1* */
    info->ldapai_info_version = LDAP_API_INFO_VERSION; 
    ldapdata->err = ldap_get_option(NULL, copt, (void*)info);
    data = (long*)info;
  }
  else {
    data = (void*)ALLOCA_N(char, LDAP_GET_OPT_MAX_BUFFER_SIZE);
    ldapdata->err = ldap_get_option(ldapdata->ldap, copt, (void*)data);
  }
#else
  data = (void*)ALLOCA_N(char, LDAP_GET_OPT_MAX_BUFFER_SIZE);
  ldapdata->err = ldap_get_option(ldapdata->ldap, copt, (void*)data);
#endif

  if( ldapdata->err == LDAP_OPT_SUCCESS ){
    switch( copt ){
    case LDAP_OPT_DEREF:
    case LDAP_OPT_SIZELIMIT:
    case LDAP_OPT_TIMELIMIT:
    case LDAP_OPT_REFERRALS:
    case LDAP_OPT_RESTART:
    case LDAP_OPT_PROTOCOL_VERSION:
    case LDAP_OPT_ERROR_NUMBER:
      val = INT2NUM((int)(*data));
      break;

    case LDAP_OPT_HOST_NAME:
    case LDAP_OPT_ERROR_STRING:
#ifdef LDAP_OPT_MATCHED_DN
    case LDAP_OPT_MATCHED_DN:
#endif
#ifdef USE_OPENLDAP2
#ifdef LDAP_OPT_X_TLS
    case LDAP_OPT_X_TLS:
#endif
#ifdef LDAP_OPT_X_TLS_CACERTFILE
    case LDAP_OPT_X_TLS_CACERTFILE:
#endif
#ifdef LDAP_OPT_X_TLS_CACERTDIR
    case LDAP_OPT_X_TLS_CACERTDIR:
#endif
#ifdef LDAP_OPT_X_TLS_CERT
    case LDAP_OPT_X_TLS_CERT:
#endif
#ifdef LDAP_OPT_X_TLS_CERTFILE
    case LDAP_OPT_X_TLS_CERTFILE:
#endif
#ifdef LDAP_OPT_X_TLS_KEYFILE
    case LDAP_OPT_X_TLS_KEYFILE:
#endif
#ifdef LDAP_OPT_X_TLS_REQUIRE_CERT
    case LDAP_OPT_X_TLS_REQUIRE_CERT:
#endif
#ifdef LDAP_OPT_X_TLS_PROTOCOL
    case LDAP_OPT_X_TLS_PROTOCOL:
#endif
#ifdef LDAP_OPT_X_TLS_CIPHER_SUITE
    case LDAP_OPT_X_TLS_CIPHER_SUITE:
#endif
#ifdef LDAP_OPT_X_TLS_RANDOM_FILE
    case LDAP_OPT_X_TLS_RANDOM_FILE:
#endif
#endif
      if( data ){
	val = rb_tainted_str_new2((char*)(*data));
      }
      else{
	val = Qnil;
      };
    break;
#ifdef LDAP_OPT_API_INFO
    case LDAP_OPT_API_INFO:
      val = rb_ldap_apiinfo_new((LDAPAPIInfo*)data);
      break;
#endif
    default:
      rb_notimplement();
    };

    return val;
  }
  else{
    rb_raise(rb_eLDAP_Error, ldap_err2string(ldapdata->err));
  };
#else
  rb_notimplement();
#endif
};


VALUE
rb_ldap_conn_perror(VALUE self, VALUE msg)
{
  RB_LDAP_DATA *ldapdata;
  char *cmsg;
  char *str;

  GET_LDAP_DATA(self,ldapdata);
  cmsg = StringValueCStr(msg);
#if defined(HAVE_LDAP_PERROR) && (! defined(USE_NETSCAPE_SDK))
  ldap_perror(ldapdata->ldap, cmsg);
#else
  str = ldap_err2string(ldapdata->err);
  fprintf(stderr, "%s: %s\n", cmsg, str);
#endif

  return Qnil;
};

VALUE
rb_ldap_conn_result2error(VALUE self, VALUE msg)
{
  RB_LDAP_DATA *ldapdata;
  RB_LDAPENTRY_DATA *edata;
  int cdofree = 0;

  GET_LDAP_DATA(self, ldapdata);
  Check_Kind(msg, rb_cLDAP_Entry);
  GET_LDAPENTRY_DATA(msg, edata);

  ldapdata->err = ldap_result2error(ldapdata->ldap, edata->msg, cdofree);
  return INT2NUM(ldapdata->err);
};

VALUE
rb_ldap_conn_err2string(VALUE self, VALUE err)
{
  RB_LDAP_DATA *ldapdata;
  int c_err = NUM2INT(err);
  char *str;

  GET_LDAP_DATA(self, ldapdata);
  str = ldap_err2string(c_err);
  return (str ? rb_tainted_str_new2(str) : Qnil);
};

VALUE
rb_ldap_conn_get_errno(VALUE self)
{
  RB_LDAP_DATA *ldapdata;
  int cerr;
  VALUE err;

  GET_LDAP_DATA(self,ldapdata);

#ifdef USE_NETSCAPE_SDK
  cerr = ldap_get_lderrno(ldapdata->ldap,NULL,NULL);
  err = INT2NUM(cerr);
#else
# ifdef USE_OPENLDAP1
  cerr = NUM2INT(ldapdata->ldap->ld_errno);
  err = INT2NUM(cerr);
# else
  rb_notimplement();
# endif
#endif

  return err;
};

static VALUE
rb_ldap_conn_invalidate_entry(VALUE msg)
{
  RB_LDAPENTRY_DATA *edata;
  GET_LDAPENTRY_DATA(msg, edata);
  edata->ldap = NULL;
  edata->msg = NULL;
  return Qnil;
};


static int
rb_ldap_internal_strcmp(const char *left, const char *right)
{
  VALUE res;

  if( rb_ldap_sort_obj == Qtrue ){
    res = rb_funcall(rb_tainted_str_new2(left), rb_intern("<=>"), 1,
		     rb_tainted_str_new2(right));
  }
  else if( rb_ldap_sort_obj != Qnil ){
    res = rb_funcall(rb_ldap_sort_obj, rb_intern("call"), 2,
		     rb_tainted_str_new2(left), rb_tainted_str_new2(right));
  }
  else{
    res = 0;
  };

  return INT2NUM(res);
};

static int
rb_ldap_conn_search_i(int argc, VALUE argv[], VALUE self,
		      RB_LDAP_DATA **ldapdata, LDAPMessage **cmsg)
{
  VALUE base, scope, filter, attrs, attrsonly, sec, usec, s_attr, s_proc;

  RB_LDAPENTRY_DATA *edata;
  LDAP *cldap;
  char *cbase;
  int cscope;
  char *cfilter;
  char **cattrs;
  char *sort_attr;
  int cattrsonly;
  VALUE msg;
  int i,j;
  struct timeval tv;

  GET_LDAP_DATA(self, (*ldapdata));
  cldap = (*ldapdata)->ldap;

  cattrs = NULL;
  cattrsonly = 0;
  tv.tv_sec = 0;
  tv.tv_usec = 0;
  sort_attr = NULL;
  rb_ldap_sort_obj = Qnil;

  switch( rb_scan_args(argc,argv,"36",
		       &base,&scope,&filter,&attrs,&attrsonly,&sec,&usec,&s_attr,&s_proc) ){
  case 9:
    rb_ldap_sort_obj = s_proc; /* Ruby's GC never start in a C function */
  case 8:
    if( rb_ldap_sort_obj == Qnil ){
      rb_ldap_sort_obj = Qtrue;
    }
    sort_attr = StringValueCStr(s_attr);
  case 7:
    tv.tv_usec = NUM2INT(usec);
  case 6:
    tv.tv_sec = NUM2INT(sec);
  case 5:
    cattrsonly = (attrsonly==Qtrue) ? 1 : 0;
  case 4:
    if( TYPE(attrs) == T_ARRAY ){
      if( RARRAY(attrs)->len == 0 ){
	cattrs = NULL;
      }
      else{
	cattrs = ALLOCA_N(char*,(RARRAY(attrs)->len + 1));
	for( i=0; i < RARRAY(attrs)->len; i++ ){
	  cattrs[i] = StringValueCStr(RARRAY(attrs)->ptr[i]);
	};
	cattrs[RARRAY(attrs)->len] = NULL;
      }
    }
    else{
      cattrs = NULL;
    };
  case 3:
    cbase = StringValueCStr(base);
    cscope = NUM2INT(scope);
    cfilter = StringValueCStr(filter);
    break;
  default:
    rb_bug("rb_ldap_conn_search_s");
  };

  (*cmsg) = NULL;
  if( tv.tv_sec == 0 && tv.tv_usec == 0 ){
    (*ldapdata)->err = ldap_search_s(cldap, cbase, cscope, cfilter,
				     cattrs, cattrsonly, cmsg);
  }
  else{
    (*ldapdata)->err = ldap_search_st(cldap, cbase, cscope, cfilter,
				      cattrs, cattrsonly, &tv, cmsg);
  }
  if( !(cmsg && (*cmsg)) ){
      rb_raise(rb_eRuntimeError, "no message");
  }
  Check_LDAP_Result((*ldapdata)->err);

#ifdef HAVE_LDAP_SORT_ENTRIES
  if( rb_ldap_sort_obj != Qnil ){
    ldap_sort_entries((*ldapdata)->ldap, cmsg,
		      sort_attr, rb_ldap_internal_strcmp);
  };
#endif
  rb_ldap_sort_obj = Qnil;

  return (*ldapdata)->err;
}

static VALUE
rb_ldap_conn_search_b(VALUE rdata)
{
    void **data = (void**)rdata;
    LDAP *cldap = (LDAP*)data[0];
    LDAPMessage *cmsg = (LDAPMessage*)data[1];
    LDAPMessage *e;
    VALUE m;

    for( e = ldap_first_entry(cldap, cmsg); e != NULL; e = ldap_next_entry(cldap, e) ){
	m = rb_ldap_entry_new(cldap, e);
	rb_ensure(rb_yield, m, rb_ldap_conn_invalidate_entry, m);
    }
    return Qnil;
}

static VALUE
rb_ldap_msgfree(VALUE data)
{
    LDAPMessage *cmsg = (LDAPMessage*)data;
    ldap_msgfree(cmsg);
    return Qnil;
}

VALUE
rb_ldap_conn_search_s(int argc, VALUE argv[], VALUE self)
{
    RB_LDAP_DATA *ldapdata;
    LDAPMessage *cmsg;
    LDAP *cldap;
    LDAPMessage *e;

    rb_ldap_conn_search_i(argc, argv, self, &ldapdata, &cmsg);
    cldap = ldapdata->ldap;

    if( ldapdata->err == LDAP_SUCCESS ){
	void *pass_data[] = {(void*)cldap, (void*)cmsg};
	rb_ensure(rb_ldap_conn_search_b, (VALUE)pass_data,
		  rb_ldap_msgfree, (VALUE)cmsg);
    };

    return self;
}

static VALUE
rb_ldap_conn_search2_b(VALUE rdata)
{
    void **data = (void*)rdata;
    LDAP *cldap = (LDAP*)data[0];
    LDAPMessage *cmsg = (LDAPMessage*)data[1];
    VALUE ary = (VALUE)data[2];
    LDAPMessage *e;
    VALUE m;
    VALUE hash;

    for( e = ldap_first_entry(cldap, cmsg); e != NULL; e = ldap_next_entry(cldap, e) ){
	m = rb_ldap_entry_new(cldap, e);
	hash  = rb_ldap_entry_to_hash(m);
	rb_ary_push(ary, hash);
	if( rb_block_given_p() ){
	    rb_ensure(rb_yield, hash, rb_ldap_conn_invalidate_entry, m);
	}
    }
    return Qnil;
}

VALUE
rb_ldap_conn_search2_s(int argc, VALUE argv[], VALUE self)
{
  RB_LDAP_DATA *ldapdata;
  LDAPMessage *cmsg;
  LDAPMessage *e;
  LDAP *cldap;
  VALUE ary = Qnil;

  rb_ldap_conn_search_i(argc, argv, self, &ldapdata, &cmsg);
  cldap = ldapdata->ldap;

  ary = rb_ary_new();
  if( ldapdata->err == LDAP_SUCCESS ){
      void *pass_data[] = {(void*)cldap, (void*)cmsg, (void*)ary};
      rb_ensure(rb_ldap_conn_search2_b, (VALUE)pass_data,
		rb_ldap_msgfree, (VALUE)cmsg);
  }

  if( rb_block_given_p() ){
    return self;
  }
  else{
    return ary;
  }
}

#if defined(HAVE_LDAPCONTROL) && defined(HAVE_LDAP_SEARCH_EXT_S)
static int
rb_ldap_conn_search_ext_i(int argc, VALUE argv[], VALUE self,
			  RB_LDAP_DATA **ldapdata, LDAPMessage **cmsg)
{
  VALUE base, scope, filter, attrs, attrsonly;
  VALUE serverctrls, clientctrls, sec, usec, limit, s_attr, s_proc;

  RB_LDAPENTRY_DATA *edata;
  LDAP *cldap;
  char *cbase;
  int cscope;
  int climit;
  char *cfilter;
  char **cattrs;
  char *sort_attr;
  int cattrsonly;
  VALUE msg;
  int i,j;
  struct timeval tv;
  LDAPControl **sctrls, **cctrls;

  GET_LDAP_DATA(self, (*ldapdata));
  cldap = (*ldapdata)->ldap;

  cattrs = NULL;
  cattrsonly = 0;
  cctrls = NULL;
  sctrls = NULL;
  tv.tv_sec = 0;
  tv.tv_usec = 0;
  sort_attr = NULL;
  rb_ldap_sort_obj = Qnil;
  climit = 0;

  switch( rb_scan_args(argc,argv,"39",
		       &base,&scope,&filter,&attrs,&attrsonly,
		       &serverctrls,&clientctrls,&sec,&usec,&limit,
		       &s_attr,&s_proc) ){
  case 12:
    rb_ldap_sort_obj = s_proc; /* Ruby's GC never start in a C function */
  case 11:
    if( rb_ldap_sort_obj == Qnil ){
      rb_ldap_sort_obj = Qtrue;
    }
    sort_attr = StringValueCStr(s_attr);
  case 10:
    climit = NUM2INT(limit);
  case 9:
    tv.tv_usec = NUM2INT(usec);
  case 8:
    tv.tv_sec = NUM2INT(sec);
  case 7:
    cctrls = rb_ldap_get_controls(clientctrls);
  case 6:
    sctrls = rb_ldap_get_controls(serverctrls);
  case 5:
    cattrsonly = (attrsonly==Qtrue) ? 1 : 0;
  case 4:
    if( TYPE(attrs) == T_ARRAY ){
      cattrs = ALLOCA_N(char*,(RARRAY(attrs)->len + 1));
      for( i=0; i < RARRAY(attrs)->len; i++ ){
	cattrs[i] = StringValueCStr(RARRAY(attrs)->ptr[i]);
      };
      cattrs[RARRAY(attrs)->len] = NULL;
    }
    else{
      cattrs = NULL;
    };
  case 3:
    cbase = StringValueCStr(base);
    cscope = NUM2INT(scope);
    cfilter = StringValueCStr(filter);
    break;
  default:
    rb_bug("rb_ldap_conn_search_s");
  };

  (*cmsg) = NULL;
  if( tv.tv_sec == 0 && tv.tv_usec == 0 ){
    (*ldapdata)->err = ldap_search_ext_s(cldap, cbase, cscope, cfilter,
					 cattrs, cattrsonly,
					 sctrls, cctrls,
					 NULL, climit, cmsg);
  }
  else{
    (*ldapdata)->err = ldap_search_ext_s(cldap, cbase, cscope, cfilter,
					 cattrs, cattrsonly,
					 sctrls, cctrls,
					 &tv, climit, cmsg);
  }
  Check_LDAP_Result((*ldapdata)->err);

#ifdef HAVE_LDAP_SORT_ENTRIES
  if( rb_ldap_sort_obj != Qnil ){
    ldap_sort_entries((*ldapdata)->ldap, cmsg,
		      sort_attr, rb_ldap_internal_strcmp);
  };
#endif
  rb_ldap_sort_obj = Qnil;

  return (*ldapdata)->err;
};

VALUE
rb_ldap_conn_search_ext_s(int argc, VALUE argv[], VALUE self)
{
  RB_LDAP_DATA *ldapdata;
  LDAPMessage *cmsg;
  LDAP *cldap;
  LDAPMessage *e;

  rb_ldap_conn_search_ext_i(argc, argv, self, &ldapdata, &cmsg);
  cldap = ldapdata->ldap;

  if( ldapdata->err == LDAP_SUCCESS ){
      void *pass_data[] = {(void*)cldap, (void*)cmsg};
      rb_ensure(rb_ldap_conn_search_b, (VALUE)pass_data,
		rb_ldap_msgfree, (VALUE)cmsg);
  };

  return self;
};

VALUE
rb_ldap_conn_search_ext2_s(int argc, VALUE argv[], VALUE self)
{
  RB_LDAP_DATA *ldapdata;
  LDAPMessage *cmsg;
  LDAPMessage *e;
  LDAP *cldap;
  VALUE ary;

  rb_ldap_conn_search_ext_i(argc, argv, self, &ldapdata, &cmsg);
  cldap = ldapdata->ldap;

  ary = rb_ary_new();
  if( ldapdata->err == LDAP_SUCCESS ){
      void *pass_data[] = {(void*)cldap, (void*)cmsg, (void*)ary};
      rb_ensure(rb_ldap_conn_search2_b, (VALUE)pass_data,
		rb_ldap_msgfree, (VALUE)cmsg);
  }

  if( rb_block_given_p() ){
    return self;
  }
  else{
    return ary;
  }
}
#endif


VALUE
rb_ldap_conn_add_s(VALUE self, VALUE dn, VALUE attrs)
{
  RB_LDAP_DATA *ldapdata;
  char *c_dn;
  LDAPMod **c_attrs;
  int i;

  switch( TYPE(attrs) ){
  case T_HASH:
    attrs = rb_ldap_hash2mods(rb_mLDAP, INT2NUM(LDAP_MOD_ADD), attrs);
    break;
  case T_ARRAY:
    break;
  default:
    rb_raise(rb_eTypeError, "must be a hash or array");
  };

  GET_LDAP_DATA(self, ldapdata);
  c_dn = StringValueCStr(dn);
  c_attrs = ALLOCA_N(LDAPMod*, (RARRAY(attrs)->len + 1));

  for ( i=0; i < RARRAY(attrs)->len; i++ ){
    VALUE mod = RARRAY(attrs)->ptr[i];
    RB_LDAPMOD_DATA *moddata;
    Check_Kind(mod, rb_cLDAP_Mod);
    GET_LDAPMOD_DATA(mod, moddata);
    c_attrs[i] = moddata->mod;
  };
  c_attrs[i] = NULL;

  ldapdata->err = ldap_add_s(ldapdata->ldap, c_dn, c_attrs);
  Check_LDAP_Result(ldapdata->err);

  return self;
};

#if defined(HAVE_LDAPCONTROL) && defined(HAVE_LDAP_ADD_EXT_S)
VALUE
rb_ldap_conn_add_ext_s(VALUE self, VALUE dn, VALUE attrs,
		       VALUE serverctrls, VALUE clientctrls)
{
  RB_LDAP_DATA *ldapdata;
  char *c_dn;
  LDAPMod **c_attrs;
  int i;
  LDAPControl **sctrls, **cctrls;

  switch( TYPE(attrs) ){
  case T_HASH:
    attrs = rb_ldap_hash2mods(rb_mLDAP, INT2NUM(LDAP_MOD_ADD), attrs);
    break;
  case T_ARRAY:
    break;
  default:
    rb_raise(rb_eTypeError, "must be a hash or array");
  };

  GET_LDAP_DATA(self, ldapdata);
  c_dn = StringValueCStr(dn);
  c_attrs = ALLOCA_N(LDAPMod*, (RARRAY(attrs)->len + 1));
  sctrls = rb_ldap_get_controls(serverctrls);
  cctrls = rb_ldap_get_controls(clientctrls);

  for ( i=0; i < RARRAY(attrs)->len; i++ ){
    VALUE mod = RARRAY(attrs)->ptr[i];
    RB_LDAPMOD_DATA *moddata;
    Check_Kind(mod, rb_cLDAP_Mod);
    GET_LDAPMOD_DATA(mod, moddata);
    c_attrs[i] = moddata->mod;
  };
  c_attrs[i] = NULL;

  ldapdata->err = ldap_add_ext_s(ldapdata->ldap, c_dn, c_attrs, sctrls, cctrls);
  Check_LDAP_Result(ldapdata->err);

  return self;
}
#endif

VALUE
rb_ldap_conn_modify_s(VALUE self, VALUE dn, VALUE attrs)
{
  RB_LDAP_DATA *ldapdata;
  char *c_dn;
  LDAPMod **c_attrs;
  int i;

  switch( TYPE(attrs) ){
  case T_HASH:
    attrs = rb_ldap_hash2mods(rb_mLDAP, INT2NUM(LDAP_MOD_REPLACE), attrs);
    break;
  case T_ARRAY:
    break;
  default:
    rb_raise(rb_eTypeError, "must be a hash or array");
  };

  GET_LDAP_DATA(self, ldapdata);
  c_dn = StringValueCStr(dn);
  c_attrs = ALLOC_N(LDAPMod*, RARRAY(attrs)->len + 1);

  for ( i=0; i < RARRAY(attrs)->len; i++ ){
    VALUE mod = RARRAY(attrs)->ptr[i];
    RB_LDAPMOD_DATA *moddata;
    Check_Kind(mod, rb_cLDAP_Mod);
    GET_LDAPMOD_DATA(mod, moddata);
    c_attrs[i] = moddata->mod;
  };
  c_attrs[i] = NULL;

  ldapdata->err = ldap_modify_s(ldapdata->ldap, c_dn, c_attrs);
  Check_LDAP_Result(ldapdata->err);

  return self;
};

#if defined(HAVE_LDAPCONTROL) && defined(HAVE_LDAP_MODIFY_EXT_S)
VALUE
rb_ldap_conn_modify_ext_s(VALUE self, VALUE dn, VALUE attrs,
			  VALUE serverctrls, VALUE clientctrls)
{
  RB_LDAP_DATA *ldapdata;
  char *c_dn;
  LDAPMod **c_attrs;
  int i;
  LDAPControl **sctrls, **cctrls;

  switch( TYPE(attrs) ){
  case T_HASH:
    attrs = rb_ldap_hash2mods(rb_mLDAP, INT2NUM(LDAP_MOD_REPLACE), attrs);
    break;
  case T_ARRAY:
    break;
  default:
    rb_raise(rb_eTypeError, "must be a hash or array");
  };

  GET_LDAP_DATA(self, ldapdata);
  c_dn = StringValueCStr(dn);
  c_attrs = ALLOC_N(LDAPMod*, RARRAY(attrs)->len + 1);
  sctrls = rb_ldap_get_controls(serverctrls);
  cctrls = rb_ldap_get_controls(clientctrls);

  for ( i=0; i < RARRAY(attrs)->len; i++ ){
    VALUE mod = RARRAY(attrs)->ptr[i];
    RB_LDAPMOD_DATA *moddata;
    Check_Kind(mod, rb_cLDAP_Mod);
    GET_LDAPMOD_DATA(mod, moddata);
    c_attrs[i] = moddata->mod;
  };
  c_attrs[i] = NULL;

  ldapdata->err = ldap_modify_ext_s(ldapdata->ldap, c_dn, c_attrs, sctrls, cctrls);
  Check_LDAP_Result(ldapdata->err);

  return self;
}
#endif

VALUE
rb_ldap_conn_modrdn_s(VALUE self, VALUE dn, VALUE newrdn, VALUE delete_p)
{
  RB_LDAP_DATA *ldapdata;
  char *c_dn;
  char *c_newrdn;
  int c_delete_p;

  GET_LDAP_DATA(self, ldapdata);
  c_dn = StringValueCStr(dn);
  c_newrdn = StringValueCStr(newrdn);
  c_delete_p = (delete_p == Qtrue) ? 1 : 0;

  ldapdata->err = ldap_modrdn2_s(ldapdata->ldap, c_dn, c_newrdn, c_delete_p);
  Check_LDAP_Result(ldapdata->err);

  return self;
};

VALUE
rb_ldap_conn_delete_s(VALUE self, VALUE dn)
{
  RB_LDAP_DATA *ldapdata;
  char *c_dn;

  GET_LDAP_DATA(self, ldapdata);
  c_dn = StringValueCStr(dn);

  ldapdata->err = ldap_delete_s(ldapdata->ldap, c_dn);
  Check_LDAP_Result(ldapdata->err);

  return self;
};

#if defined(HAVE_LDAPCONTROL) && defined(HAVE_LDAP_DELETE_EXT_S)
VALUE
rb_ldap_conn_delete_ext_s(VALUE self, VALUE dn,
			  VALUE serverctrls, VALUE clientctrls)
{
  RB_LDAP_DATA *ldapdata;
  char *c_dn;
  LDAPControl **sctrls, **cctrls;

  GET_LDAP_DATA(self, ldapdata);
  c_dn = StringValueCStr(dn);
  sctrls = rb_ldap_get_controls(serverctrls);
  cctrls = rb_ldap_get_controls(clientctrls);

  ldapdata->err = ldap_delete_ext_s(ldapdata->ldap, c_dn, sctrls, cctrls);
  Check_LDAP_Result(ldapdata->err);

  return self;
}
#endif

#if defined(HAVE_LDAP_COMPARE_S)
VALUE
rb_ldap_conn_compare_s(VALUE self, VALUE dn, VALUE attr, VALUE val)
{
  RB_LDAP_DATA *ldapdata;
  char *c_dn, *c_attr, *c_val;

  GET_LDAP_DATA(self, ldapdata);
  c_dn = StringValueCStr(dn);
  c_attr = StringValueCStr(attr);
  c_val = StringValueCStr(val);

  ldapdata->err = ldap_compare_s(ldapdata->ldap, c_dn, c_attr, c_val);
  Check_LDAP_Result(ldapdata->err);

  return self;
}
#endif

#if defined(HAVE_LDAPCONTROL) && defined(HAVE_LDAP_COMPARE_EXT_S)
VALUE
rb_ldap_conn_compare_ext_s(VALUE self, VALUE dn, VALUE attr, VALUE val,
			   VALUE serverctrls, VALUE clientctrls)
{
  RB_LDAP_DATA *ldapdata;
  char *c_dn, *c_attr;
  struct berval bval;
  LDAPControl **sctrls, **cctrls;

  GET_LDAP_DATA(self, ldapdata);
  c_dn = StringValueCStr(dn);
  c_attr = StringValueCStr(attr);
  bval.bv_val = StringValueCStr(val);
  bval.bv_len = RSTRING(val)->len;
  sctrls = rb_ldap_get_controls(serverctrls);
  cctrls = rb_ldap_get_controls(clientctrls);

  ldapdata->err = ldap_compare_ext_s(ldapdata->ldap,
				     c_dn, c_attr, &bval,
				     sctrls, cctrls);
  Check_LDAP_Result(ldapdata->err);

  return self;
}
#endif

VALUE
rb_ldap_conn_err(VALUE self)
{
  RB_LDAP_DATA *ldapdata;

  GET_LDAP_DATA(self, ldapdata);
  return INT2NUM(ldapdata->err);
};

void
Init_ldap_conn()
{
  rb_ldap_sort_obj = Qnil;

  rb_cLDAP_Conn = rb_define_class_under(rb_mLDAP,"Conn",rb_cData);
#if RUBY_VERSION_CODE < 170
  rb_define_singleton_method(rb_cLDAP_Conn,"new",rb_ldap_class_new,-1);
#endif
#if RUBY_VERSION_CODE >= 173
  rb_define_alloc_func(rb_cLDAP_Conn,rb_ldap_conn_s_allocate);
#else
  rb_define_singleton_method(rb_cLDAP_Conn,"allocate",rb_ldap_conn_s_allocate,0);
#endif
  rb_define_singleton_method(rb_cLDAP_Conn,"open",rb_ldap_conn_s_open,-1);
  rb_ldap_conn_define_method("initialize", rb_ldap_conn_initialize,-1);
  rb_ldap_conn_define_method("start_tls", rb_ldap_conn_start_tls_s, -1);
  rb_ldap_conn_define_method("simple_bind", rb_ldap_conn_simple_bind_s, -1);
  rb_ldap_conn_define_method("bind", rb_ldap_conn_bind_s, -1);
  rb_ldap_conn_define_method("unbind", rb_ldap_conn_unbind, 0);
  rb_ldap_conn_define_method("set_option", rb_ldap_conn_set_option, 2);
  rb_ldap_conn_define_method("get_option", rb_ldap_conn_get_option, 1);
  rb_ldap_conn_define_method("search", rb_ldap_conn_search_s, -1);
  rb_ldap_conn_define_method("search2", rb_ldap_conn_search2_s, -1);
  rb_ldap_conn_define_method("add", rb_ldap_conn_add_s, 2);
  rb_ldap_conn_define_method("modify", rb_ldap_conn_modify_s, 2);
  rb_ldap_conn_define_method("modrdn", rb_ldap_conn_modrdn_s, 3);
  rb_ldap_conn_define_method("delete", rb_ldap_conn_delete_s, 1);
#if defined(HAVE_LDAP_COMPARE_S)
  rb_ldap_conn_define_method("compare", rb_ldap_conn_compare_s, 3);
#endif
  rb_ldap_conn_define_method("perror", rb_ldap_conn_perror, 1);
  rb_ldap_conn_define_method("err2string", rb_ldap_conn_err2string, 1);
  rb_ldap_conn_define_method("result2error", rb_ldap_conn_result2error, 1);
  rb_ldap_conn_define_method("err", rb_ldap_conn_err, 0);

#if defined(HAVE_LDAPCONTROL)
#if defined(HAVE_LDAP_SEARCH_EXT_S)
  rb_ldap_conn_define_method("search_ext", rb_ldap_conn_search_ext_s, -1);
  rb_ldap_conn_define_method("search_ext2", rb_ldap_conn_search_ext2_s, -1);
#endif
#if defined(HAVE_LDAP_ADD_EXT_S)
  rb_ldap_conn_define_method("add_ext", rb_ldap_conn_add_ext_s, 4);
#endif
#if defined(HAVE_LDAP_MODIFY_EXT_S)
  rb_ldap_conn_define_method("modify_ext", rb_ldap_conn_modify_ext_s, 4);
#endif
#if defined(HAVE_LDAP_DELETE_EXT_S)
  rb_ldap_conn_define_method("delete_ext", rb_ldap_conn_delete_ext_s, 3);
#endif
#if defined(HAVE_LDAP_COMPARE_EXT_S)
  rb_ldap_conn_define_method("compare_ext", rb_ldap_conn_compare_ext_s, 5);
#endif
#endif
};
