Commits

Michael Granger committed e49a9da

Adding the toplevel patch

  • Participants

Comments (0)

Files changed (3)

+^\.hg
+^\.mq
+syntax: glob
+status
+guards
+toplevel
+# HG changeset patch
+# Parent 031b7ced24141ca537d1a4cf9fd4d5543410ca9d
+Implementing top-level OpenLDAP functions
+
+diff --git a/Rakefile b/Rakefile
+--- a/Rakefile
++++ b/Rakefile
+@@ -50,8 +50,8 @@
+ 
+ 	self.require_ruby_version( '>= 1.8.7' )
+ 
+-	self.hg_sign_tags = true if self.plugin?( :mercurial )
+-	self.yard_opts = [ '--protected', '--verbose' ] if self.plugin?( :yard )
++	self.hg_sign_tags = true if self.respond_to?( :hg_sign_tags= )
++	self.yard_opts = [ '--protected', '--verbose' ] if self.respond_to?( :yard_opts= )
+ 
+ 	self.rdoc_locations << "deveiate:/usr/local/www/public/code/#{remote_rdoc_dir}"
+ end
+diff --git a/ext/openldap.c b/ext/openldap.c
+--- a/ext/openldap.c
++++ b/ext/openldap.c
+@@ -43,9 +43,170 @@
+ 
+ VALUE ropenldap_mOpenLDAP;
+ 
++/* --------------------------------------------------------------
++ * Logging Functions
++ * -------------------------------------------------------------- */
++
++/*
++ * Log a message to the given +context+ object's logger.
++ */
++void
++#ifdef HAVE_STDARG_PROTOTYPES
++ropenldap_log_with_context( VALUE context, const char *level, const char *fmt, ... ) 
++#else
++ropenldap_log_with_context( VALUE context, const char *level, const char *fmt, va_dcl ) 
++#endif
++{
++	char buf[BUFSIZ];
++	va_list	args;
++	VALUE logger = Qnil;
++	VALUE message = Qnil;
++
++	va_start( args, fmt );
++	vsnprintf( buf, BUFSIZ, fmt, args );
++	message = rb_str_new2( buf );
++	
++	logger = rb_funcall( context, rb_intern("log"), 0, 0 );
++	rb_funcall( logger, rb_intern(level), 1, message );
++
++	va_end( args );
++}
++
++
++/* 
++ * Log a message to the global logger.
++ */
++void
++#ifdef HAVE_STDARG_PROTOTYPES
++ropenldap_log( const char *level, const char *fmt, ... ) 
++#else
++ropenldap_log( const char *level, const char *fmt, va_dcl ) 
++#endif
++{
++	char buf[BUFSIZ];
++	va_list	args;
++	VALUE logger = Qnil;
++	VALUE message = Qnil;
++
++	va_init_list( args, fmt );
++	vsnprintf( buf, BUFSIZ, fmt, args );
++	message = rb_str_new2( buf );
++	
++	logger = rb_funcall( ropenldap_mOpenLDAP, rb_intern("logger"), 0, 0 );
++	rb_funcall( logger, rb_intern(level), 1, message );
++
++	va_end( args );
++}
++
++
++
++/*
++ * Convert an array of string pointers to a Ruby Array of Strings.
++ */
++static VALUE
++ropenldap_rb_string_array( char **strings )
++{
++	VALUE ary = rb_ary_new();
++	char **iter;
++
++	/* If there aren't any pointers, just return the empty Array */
++	if ( !strings ) return ary;
++
++	for ( iter = strings ; *iter != NULL ; iter++ ) {
++		ropenldap_log( "debug", "  adding %s to string array", *iter );
++		rb_ary_push( ary, rb_str_new2(*iter) );
++	}
++	
++	return ary;
++}
++
++
++/*
++ * call-seq:
++ *    OpenLDAP.split_url( str )   -> array
++ *
++ * Split an LDAP URL into an array of its parts:
++ * - uri_scheme
++ * - host
++ * - port
++ * - base
++ * - attrs
++ * - scope
++ * - filter
++ * - exts
++ * - crit_exts
++ */
++static VALUE
++ropenldap_s_split_url( VALUE _, VALUE urlstring )
++{
++	const char *url = StringValueCStr( urlstring );
++	LDAPURLDesc *urldesc;
++	VALUE rval = Qnil, obj = Qnil;
++
++	if ( !ldap_is_ldap_url(url) )
++		rb_raise( rb_eArgError, "Not an LDAP URL." );
++
++	/* Parse the URL */
++	if ( ldap_url_parse(url, &urldesc) != 0 )
++		rb_raise( rb_eRuntimeError, "Error parsing %s as an LDAP URL!", url );
++
++	rval = rb_ary_new2( 9 );
++
++	/* Scheme */
++	if ( urldesc->lud_scheme ) {
++		ropenldap_log( "debug", "  parsed scheme: %s", urldesc->lud_scheme );
++		RARRAY_PTR( rval )[0] = rb_str_new2( urldesc->lud_scheme );
++		OBJ_INFECT( urlstring, RARRAY_PTR(rval)[0] );
++	}
++
++	ropenldap_log( "debug", "  rval is: %s", RSTRING_PTR(rb_inspect(rval)) );
++
++	/* LDAP host to contact */
++	if ( urldesc->lud_host ) {
++		ropenldap_log( "debug", "  parsed host: %s", urldesc->lud_host );
++		RARRAY_PTR( rval )[1] = rb_str_new2( urldesc->lud_host );
++		OBJ_INFECT( RARRAY_PTR(rval)[1], urlstring );
++	}
++
++	/* Port */
++	RARRAY_PTR( rval )[2] = INT2FIX( urldesc->lud_port );
++
++	/* Base DN */
++	if ( urldesc->lud_dn ) {
++		ropenldap_log( "debug", "  parsed DN: %s", urldesc->lud_dn );
++		RARRAY_PTR( rval )[3] = rb_str_new2( urldesc->lud_dn );
++		OBJ_INFECT( RARRAY_PTR(rval)[3], urlstring );
++	}
++
++	/* Numeric scope (LDAP_SCOPE_*) */
++	RARRAY_PTR( rval )[5] = INT2FIX( urldesc->lud_scope );
++
++	/* Filter */
++	if ( urldesc->lud_filter ) {
++		ropenldap_log( "debug", "  parsed filter: %s", urldesc->lud_filter );
++		RARRAY_PTR( rval )[6] = rb_str_new2( urldesc->lud_filter );
++		OBJ_INFECT( RARRAY_PTR(rval)[6], urlstring );
++	}
++
++	/* Critical extension/s flag */
++	RARRAY_PTR( rval )[8] = urldesc->lud_crit_exts ? Qtrue : Qfalse;  /* true if any extension is critical */
++
++	/* lists of attributes and LDAP extensions */
++	RARRAY_PTR( rval )[4] = ropenldap_rb_string_array( urldesc->lud_attrs );
++	RARRAY_PTR( rval )[7] = ropenldap_rb_string_array( urldesc->lud_exts );
++
++	ldap_free_urldesc( urldesc );
++
++	return rval;
++}
++
++
+ void
+ Init_openldap_ext( void )
+ {
+ 	ropenldap_mOpenLDAP = rb_define_module( "OpenLDAP" );
++
++	rb_define_singleton_method( ropenldap_mOpenLDAP, "split_url", ropenldap_s_split_url, 1 );
++
+ }
+ 
+diff --git a/ext/openldap.h b/ext/openldap.h
+--- a/ext/openldap.h
++++ b/ext/openldap.h
+@@ -8,6 +8,7 @@
+ #include <stdio.h>
+ #include <string.h>
+ #include <inttypes.h>
++#include <assert.h>
+ 
+ #include <ldap.h>
+ 
+@@ -44,6 +45,19 @@
+  * Declarations
+  * -------------------------------------------------------------- */
+ 
++#ifdef HAVE_STDARG_PROTOTYPES
++#include <stdarg.h>
++#define va_init_list(a,b) va_start(a,b)
++void ropenldap_log_with_context( VALUE, const char *, const char *, ... );
++void ropenldap_log( const char *, const char *, ... );
++#else
++#include <varargs.h>
++#define va_init_list(a,b) va_start(a)
++void ropenldap_log_with_context( VALUE, const char *, const char *, va_dcl );
++void ropenldap_log( const char *, const char *, va_dcl );
++#endif
++
++
+ 
+ /* --------------------------------------------------------------
+  * Initializers
+diff --git a/lib/openldap.rb b/lib/openldap.rb
+--- a/lib/openldap.rb
++++ b/lib/openldap.rb
+@@ -12,7 +12,69 @@
+ 	# Version-control revision constant
+ 	REVISION = %q$Revision$
+ 
++	require 'openldap/utils'
+ 
++	### Logging
++	# Log levels
++	LOG_LEVELS = {
++		'debug' => Logger::DEBUG,
++		'info'  => Logger::INFO,
++		'warn'  => Logger::WARN,
++		'error' => Logger::ERROR,
++		'fatal' => Logger::FATAL,
++	}.freeze
++	LOG_LEVEL_NAMES = LOG_LEVELS.invert.freeze
++
++	@default_logger = Logger.new( $stderr )
++	@default_logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
++
++	@default_log_formatter = OpenLDAP::LogFormatter.new( @default_logger )
++	@default_logger.formatter = @default_log_formatter
++
++	@logger = @default_logger
++
++
++	class << self
++		# @return [Logger::Formatter] the log formatter that will be used when the logging 
++		#    subsystem is reset
++		attr_accessor :default_log_formatter
++
++		# @return [Logger] the logger that will be used when the logging subsystem is reset
++		attr_accessor :default_logger
++
++		# @return [Logger] the logger that's currently in effect
++		attr_accessor :logger
++		alias_method :log, :logger
++		alias_method :log=, :logger=
++	end
++
++
++	### Reset the global logger object to the default
++	### @return [void]
++	def self::reset_logger
++		self.logger = self.default_logger
++		self.logger.level = Logger::WARN
++		self.logger.formatter = self.default_log_formatter
++	end
++
++
++	### Returns +true+ if the global logger has not been set to something other than
++	### the default one.
++	def self::using_default_logger?
++		return self.logger == self.default_logger
++	end
++
++
++	### Get the library version.
++	### @return [String] the library's version
++	def self::version_string( include_buildnum=false )
++		vstring = "%s %s" % [ self.name, VERSION ]
++		vstring << " (build %s)" % [ REVISION[/: ([[:xdigit:]]+)/, 1] || '0' ] if include_buildnum
++		return vstring
++	end
++
++
++	### Load the extension
+ 	begin
+ 		require 'openldap_ext'
+ 	rescue LoadError => err
+diff --git a/lib/openldap/mixins.rb b/lib/openldap/mixins.rb
+new file mode 100644
+--- /dev/null
++++ b/lib/openldap/mixins.rb
+@@ -0,0 +1,65 @@
++#!/usr/bin/ruby
++
++require 'rbconfig'
++require 'erb'
++require 'etc'
++require 'logger'
++
++require 'openldap'
++
++
++#--
++# A collection of mixins shared between OpenLDAP classes. Stolen mostly from ThingFish.
++#
++module OpenLDAP # :nodoc:
++
++	# Add logging to a OpenLDAP class. Including classes get #log and #log_debug methods.
++	module Loggable
++
++		LEVEL = {
++			:debug => Logger::DEBUG,
++			:info  => Logger::INFO,
++			:warn  => Logger::WARN,
++			:error => Logger::ERROR,
++			:fatal => Logger::FATAL,
++		  }
++
++		### A logging proxy class that wraps calls to the logger into calls that include
++		### the name of the calling class.
++		class ClassNameProxy # :nodoc:
++
++			### Create a new proxy for the given +klass+.
++			def initialize( klass, force_debug=false )
++				@classname   = klass.name
++				@force_debug = force_debug
++			end
++
++			### Delegate calls the global logger with the class name as the 'progname' 
++			### argument.
++			def method_missing( sym, msg=nil, &block )
++				return super unless LEVEL.key?( sym )
++				sym = :debug if @force_debug
++				OpenLDAP.logger.add( LEVEL[sym], msg, @classname, &block )
++			end
++		end # ClassNameProxy
++
++		#########
++		protected
++		#########
++
++		### Return the proxied logger.
++		def log
++			@log_proxy ||= ClassNameProxy.new( self.class )
++		end
++
++		### Return a proxied "debug" logger that ignores other level specification.
++		def log_debug
++			@log_debug_proxy ||= ClassNameProxy.new( self.class, true )
++		end
++	end # module Loggable
++
++
++end # module OpenLDAP
++
++# vim: set nosta noet ts=4 sw=4:
++
+diff --git a/lib/openldap/utils.rb b/lib/openldap/utils.rb
+new file mode 100644
+--- /dev/null
++++ b/lib/openldap/utils.rb
+@@ -0,0 +1,127 @@
++#!/usr/bin/ruby
++
++require 'logger'
++require 'erb'
++require 'bigdecimal'
++require 'date'
++
++require 'openldap'
++require 'openldap/mixins'
++
++
++module OpenLDAP # :nodoc:
++
++	# A alternate formatter for Logger instances.
++	class LogFormatter < Logger::Formatter
++
++		# The format to output unless debugging is turned on
++		DEFAULT_FORMAT = "[%1$s.%2$06d %3$d/%4$s] %5$5s -- %7$s\n"
++
++		# The format to output if debugging is turned on
++		DEFAULT_DEBUG_FORMAT = "[%1$s.%2$06d %3$d/%4$s] %5$5s {%6$s} -- %7$s\n"
++
++
++		### Initialize the formatter with a reference to the logger so it can check for log level.
++		def initialize( logger, format=DEFAULT_FORMAT, debug=DEFAULT_DEBUG_FORMAT ) # :notnew:
++			@logger       = logger
++			@format       = format
++			@debug_format = debug
++
++			super()
++		end
++
++		######
++		public
++		######
++
++		# The Logger object associated with the formatter
++		attr_accessor :logger
++
++		# The logging format string
++		attr_accessor :format
++
++		# The logging format string that's used when outputting in debug mode
++		attr_accessor :debug_format
++
++
++		### Log using either the DEBUG_FORMAT if the associated logger is at ::DEBUG level or
++		### using FORMAT if it's anything less verbose.
++		def call( severity, time, progname, msg )
++			args = [
++				time.strftime( '%Y-%m-%d %H:%M:%S' ),                         # %1$s
++				time.usec,                                                    # %2$d
++				Process.pid,                                                  # %3$d
++				Thread.current == Thread.main ? 'main' : Thread.object_id,    # %4$s
++				severity,                                                     # %5$s
++				progname,                                                     # %6$s
++				msg                                                           # %7$s
++			]
++
++			if @logger.level == Logger::DEBUG
++				return self.debug_format % args
++			else
++				return self.format % args
++			end
++		end
++	end # class LogFormatter
++
++
++	# An alternate formatter for Logger instances that outputs +dd+ HTML
++	# fragments.
++	class HtmlLogFormatter < Logger::Formatter
++		include ERB::Util  # for html_escape()
++
++		# The default HTML fragment that'll be used as the template for each log message.
++		HTML_LOG_FORMAT = %q{
++		<div class="log-message %5$s">
++			<span class="log-time">%1$s.%2$06d</span>
++			[
++				<span class="log-pid">%3$d</span>
++				/
++				<span class="log-tid">%4$s</span>
++			]
++			<span class="log-level">%5$s</span>
++			:
++			<span class="log-name">%6$s</span>
++			<span class="log-message-text">%7$s</span>
++		</div>
++		}
++
++		### Override the logging formats with ones that generate HTML fragments
++		def initialize( logger, format=HTML_LOG_FORMAT ) # :notnew:
++			@logger = logger
++			@format = format
++			super()
++		end
++
++
++		######
++		public
++		######
++
++		# The HTML fragment that will be used as a format() string for the log
++		attr_accessor :format
++
++
++		### Return a log message composed out of the arguments formatted using the
++		### formatter's format string
++		def call( severity, time, progname, msg )
++			args = [
++				time.strftime( '%Y-%m-%d %H:%M:%S' ),                         # %1$s
++				time.usec,                                                    # %2$d
++				Process.pid,                                                  # %3$d
++				Thread.current == Thread.main ? 'main' : Thread.object_id,    # %4$s
++				severity.downcase,                                                     # %5$s
++				progname,                                                     # %6$s
++				html_escape( msg ).gsub(/\n/, '<br />')                       # %7$s
++			]
++
++			return self.format % args
++		end
++
++	end # class HtmlLogFormatter
++
++end # module OpenLDAP
++
++# vim: set nosta noet ts=4 sw=4:
++
+diff --git a/spec/lib/constants.rb b/spec/lib/constants.rb
+new file mode 100644
+--- /dev/null
++++ b/spec/lib/constants.rb
+@@ -0,0 +1,20 @@
++#!/usr/bin/env ruby
++
++require 'pathname'
++
++require 'openldap'
++
++
++module OpenLDAP::TestConstants
++
++	unless defined?( TEST_LDAP_URI )
++
++		TEST_LDAP_URI = 'ldap://ldap.acme.com/dc=acme,dc=com'
++
++		constants.each do |cname|
++			const_get(cname).freeze
++		end
++	end
++
++end
++
+diff --git a/spec/lib/helpers.rb b/spec/lib/helpers.rb
+new file mode 100644
+--- /dev/null
++++ b/spec/lib/helpers.rb
+@@ -0,0 +1,112 @@
++#!/usr/bin/ruby
++# coding: utf-8
++
++BEGIN {
++	require 'rbconfig'
++	require 'pathname'
++	basedir = Pathname.new( __FILE__ ).dirname.parent
++
++	libdir = basedir + "lib"
++	extdir = libdir + Config::CONFIG['sitearch']
++
++	$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
++	$LOAD_PATH.unshift( extdir.to_s ) unless $LOAD_PATH.include?( extdir.to_s )
++}
++
++require 'pp'
++require 'yaml'
++require 'logger'
++
++require 'openldap'
++require 'openldap/mixins'
++
++require 'spec/lib/constants'
++
++### Return a string-comparable version vector from +version+.
++def vvec( version )
++	return version.split( '.' ).map( &:to_i ).pack( 'N' )
++end
++
++
++### RSpec helper functions.
++module OpenLDAP::SpecHelpers
++	include OpenLDAP::TestConstants
++
++	class ArrayLogger
++		### Create a new ArrayLogger that will append content to +array+.
++		def initialize( array )
++			@array = array
++		end
++
++		### Write the specified +message+ to the array.
++		def write( message )
++			@array << message
++		end
++
++		### No-op -- this is here just so Logger doesn't complain
++		def close; end
++
++	end # class ArrayLogger
++
++
++	unless defined?( LEVEL )
++		LEVEL = {
++			:debug => Logger::DEBUG,
++			:info  => Logger::INFO,
++			:warn  => Logger::WARN,
++			:error => Logger::ERROR,
++			:fatal => Logger::FATAL,
++		  }
++	end
++
++
++	###############
++	module_function
++	###############
++
++	### Reset the logging subsystem to its default state.
++	def reset_logging
++		OpenLDAP.reset_logger
++	end
++
++
++	### Alter the output of the default log formatter to be pretty in SpecMate output
++	def setup_logging( level=Logger::FATAL )
++
++		# Turn symbol-style level config into Logger's expected Fixnum level
++		if OpenLDAP::Loggable::LEVEL.key?( level )
++			level = OpenLDAP::Loggable::LEVEL[ level ]
++		end
++
++		logger = Logger.new( $stderr )
++		OpenLDAP.logger = logger
++		OpenLDAP.logger.level = level
++
++		# Only do this when executing from a spec in TextMate
++		if ENV['HTML_LOGGING'] || (ENV['TM_FILENAME'] && ENV['TM_FILENAME'] =~ /_spec\.rb/)
++			Thread.current['logger-output'] = []
++			logdevice = ArrayLogger.new( Thread.current['logger-output'] )
++			OpenLDAP.logger = Logger.new( logdevice )
++			# OpenLDAP.logger.level = level
++			OpenLDAP.logger.formatter = OpenLDAP::HtmlLogFormatter.new( OpenLDAP.logger )
++		end
++	end
++
++
++end
++
++
++### Mock with Rspec
++Rspec.configure do |config|
++	config.mock_with :rspec
++
++	config.extend( OpenLDAP::TestConstants )
++
++	config.include( OpenLDAP::TestConstants )
++	config.include( OpenLDAP::SpecHelpers )
++
++	config.filter_run_excluding( :ruby_1_9_only => true ) if vvec( RUBY_VERSION ) >= vvec( '1.9.0' )
++end
++
++# vim: set nosta noet ts=4 sw=4:
++
+diff --git a/spec/openldap_spec.rb b/spec/openldap_spec.rb
+--- a/spec/openldap_spec.rb
++++ b/spec/openldap_spec.rb
+@@ -5,16 +5,59 @@
+ 	basedir = Pathname( __FILE__ ).dirname.parent
+ 	libdir = basedir + 'lib'
+ 
++	$LOAD_PATH.unshift( basedir.to_s ) unless $LOAD_PATH.include?( basedir.to_s )
+ 	$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
+ }
+ 
+ require 'rspec'
++require 'spec/lib/helpers'
+ require 'openldap'
+ 
+ describe OpenLDAP do
+ 
+-	it "is well-tested" do
+-		fail "it isn't"
++	before( :all ) do
++		setup_logging( :debug )
++	end
++
++	after( :all ) do
++		reset_logging()
++	end
++
++
++    # typedef struct ldap_url_desc {
++    #     char *      lud_scheme;     /* URI scheme */
++    #     char *      lud_host;       /* LDAP host to contact */
++    #     int         lud_port;       /* port on host */
++    #     char *      lud_dn;         /* base for search */
++    #     char **     lud_attrs;      /* list of attributes */
++    #     int         lud_scope;      /* a LDAP_SCOPE_... value */
++    #     char *      lud_filter;     /* LDAP search filter */
++    #     char **     lud_exts;       /* LDAP extensions */
++    #     int         lud_crit_exts;  /* true if any extension is critical */
++    #     /* may contain additional fields for internal use */
++    # } LDAPURLDesc;
++	it "can split an LDAP URL into its components" do
++		OpenLDAP.split_url( 'ldap://ldap.acme.com/dc=acme,dc=com' ).should == [
++			'ldap',
++			'ldap.acme.com',
++			389,
++			'dc=acme,dc=com',
++		]
++	end
++
++	it "raises an argument error when asked to split a String that isn't an LDAP URL" do
++		expect {
++			OpenLDAP.split_url( 'your cat is incredibly soft' )
++		}.to raise_error( ArgumentError, /not an ldap url/i )
++	end
++
++	it "propagates taintedness from a split URL to its parts" do
++		url = 'ldap://ldap.acme.com/dc=acme,dc=com'
++		url.taint
++
++		result = OpenLDAP.split_url( url )
++
++		result[0].should be_tainted()
+ 	end
+ 
+ end