non-ASCII output for ldapsearch [PATCH]

Emmanuel Dreyfus <[email protected]> Fri, 27 Feb 2026 15:59:55 +0000
Newsgroups gmane.network.openldap.devel
Message-ID <[email protected]>
--2tNte84QOFG9kpvm
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

Hello

It would be nice if ldapsearch could display non-ASCII strings in
a configurable charset, instead of always encoding in base64.

I understand the current behavior is a requirement for LDIF
compliance, but I believe we can add a non-LDIF compliant output,
if this is what the user requested.

Attached is a quick patch that will let ldapsearch output in the
charset specified by LDAP_CHARSET environment variable (inspiration
from LESS_CHARSET for less(1)). That makes life much more confortable
when handling non-english data.

It uses iconv() if <iconv.h> is detected. This function has been
in POSIX for a while:
https://pubs.opengroup.org/onlinepubs/007908775/xsh/iconv.html

Opinions? Thoughts? 

-- 
Emmanuel Dreyfus
[email protected]

--2tNte84QOFG9kpvm
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment; filename=ldapsearch_charset.patch

diff --git a/clients/tools/ldapsearch.c b/clients/tools/ldapsearch.c
index 7ab29777e8..6ab5aa5db8 100644
--- a/clients/tools/ldapsearch.c
+++ b/clients/tools/ldapsearch.c
@@ -60,6 +60,9 @@
 #ifdef HAVE_IO_H
 #include <io.h>
 #endif
+#ifdef HAVE_ICONV_H
+#include <iconv.h>
+#endif
 
 #include <ldap.h>
 
@@ -89,6 +92,8 @@ ldap_sort_entries LDAP_P(( LDAP *ld,
 	LDAP_SORT_AD_CMP_PROC *cmp ));
 #endif
 
+typedef int ( *tool_write_ldif_t ) ( int type, char *name, char *value, ber_len_t vallen );
+
 static int scope = LDAP_SCOPE_SUBTREE;
 static int deref = -1;
 static int attrsonly;
@@ -1929,6 +1934,49 @@ done:
 	return( rc2 );
 }
 
+static int
+ldapsearch_write( int type, char *name, char *value, ber_len_t vallen )
+{
+	static iconv_t cd = (iconv_t)-1;
+	char *dst_charset;
+	char dstbuf[4096];
+	char *dst = dstbuf;
+	size_t dstleft = sizeof(dstbuf);
+	char *src = value;
+	size_t srcleft = vallen;
+	size_t inval;
+
+	if ( type != LDIF_PUT_VALUE )
+		return 0;
+
+	dst_charset = getenv("LDAP_CHARSET");
+	if (dst_charset == NULL)
+		goto fallback;
+
+	if (strstr(name, ";binary") != NULL)
+		goto fallback;
+
+	if (cd == (iconv_t)-1) {
+		cd = iconv_open(dst_charset, "utf8");
+		if (cd == (iconv_t)-1)
+			goto fallback;
+	}
+
+	if (iconv(cd, &src, &srcleft, &dst, &dstleft) == 0) {
+		dstbuf[sizeof(dstbuf) - dstleft] = '\0';
+		printf("%s: %s\n", name, dstbuf);
+	} else if (errno == E2BIG) {
+		dstbuf[sizeof(dstbuf) - dstleft] = '\0';
+		printf("%s: %s (...)\n", name, dstbuf);
+	} else {
+		goto fallback;
+	}
+
+	return 0;
+fallback:
+	return tool_write_ldif(type, name, value, vallen);
+}
+
 /* This is the proposed new way of doing things.
  * It is more efficient, but the API is non-standard.
  */
@@ -1946,14 +1994,18 @@ print_entry(
 	struct berval		bv, *bvals, **bvp = &bvals;
 	LDAPControl **ctrls = NULL;
 	FILE		*tmpfp;
+	tool_write_ldif_t write_ldif = tool_write_ldif;;
+
+	if (getenv("LDAP_CHARSET"))
+		write_ldif = ldapsearch_write;
 
 	rc = ldap_get_dn_ber( ld, entry, &ber, &bv );
 
 	if ( ldif < 2 ) {
 		ufn = ldap_dn2ufn( bv.bv_val );
-		tool_write_ldif( LDIF_PUT_COMMENT, NULL, ufn, ufn ? strlen( ufn ) : 0 );
+		write_ldif( LDIF_PUT_COMMENT, NULL, ufn, ufn ? strlen( ufn ) : 0 );
 	}
-	tool_write_ldif( LDIF_PUT_VALUE, "dn", bv.bv_val, bv.bv_len );
+	write_ldif( LDIF_PUT_VALUE, "dn", bv.bv_val, bv.bv_len );
 
 	rc = ldap_get_entry_controls( ld, entry, &ctrls );
 	if( rc != LDAP_SUCCESS ) {
@@ -1971,7 +2023,7 @@ print_entry(
 		if( ufn == NULL ) {
 			ufn = ldap_dn2ufn( bv.bv_val );
 		}
-		tool_write_ldif( LDIF_PUT_VALUE, "ufn", ufn, ufn ? strlen( ufn ) : 0 );
+		write_ldif( LDIF_PUT_VALUE, "ufn", ufn, ufn ? strlen( ufn ) : 0 );
 	}
 
 	if( ufn != NULL ) ldap_memfree( ufn );
@@ -1985,7 +2037,7 @@ print_entry(
 		if (bv.bv_val == NULL) break;
 
 		if ( attrsonly ) {
-			tool_write_ldif( LDIF_PUT_NOVALUE, bv.bv_val, NULL, 0 );
+			write_ldif( LDIF_PUT_NOVALUE, bv.bv_val, NULL, 0 );
 
 		} else if ( bvals ) {
 			for ( i = 0; bvals[i].bv_val != NULL; i++ ) {
@@ -2025,10 +2077,10 @@ print_entry(
 						&tmpfname[strlen(tmpdir) + sizeof(LDAP_DIRSEP) - 1] );
 
 					urlize( url );
-					tool_write_ldif( LDIF_PUT_URL, bv.bv_val, url, strlen( url ));
+					write_ldif( LDIF_PUT_URL, bv.bv_val, url, strlen( url ));
 
 				} else {
-					tool_write_ldif( LDIF_PUT_VALUE, bv.bv_val,
+					write_ldif( LDIF_PUT_VALUE, bv.bv_val,
 						bvals[ i ].bv_val, bvals[ i ].bv_len );
 				}
 			}
diff --git a/configure b/configure
index 5dc5bf1dd5..035cfeafee 100755
--- a/configure
+++ b/configure
@@ -16320,6 +16320,12 @@ if test "x$ac_cv_header_io_h" = xyes
 then :
   printf "%s\n" "#define HAVE_IO_H 1" >>confdefs.h
 
+fi
+ac_fn_c_check_header_compile "$LINENO" "iconv.h" "ac_cv_header_iconv_h" "$ac_includes_default"
+if test "x$ac_cv_header_iconv_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_ICONV_H 1" >>confdefs.h
+
 fi
 ac_fn_c_check_header_compile "$LINENO" "libutil.h" "ac_cv_header_libutil_h" "$ac_includes_default"
 if test "x$ac_cv_header_libutil_h" = xyes
diff --git a/configure.ac b/configure.ac
index bead4d2b9c..6830bd46a1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -815,6 +815,7 @@ AC_CHECK_HEADERS(	\
 	getopt.h		\
 	grp.h			\
 	io.h			\
+	iconv.h			\
 	libutil.h		\
 	limits.h		\
 	locale.h		\
diff --git a/include/portable.hin b/include/portable.hin
index 45e5b6ee13..e4d649fd32 100644
--- a/include/portable.hin
+++ b/include/portable.hin
@@ -313,6 +313,9 @@
 /* define if you have libsodium */
 #undef HAVE_LIBSODIUM
 
+/* Define to 1 if you have the <iconv.h> header file. */
+#undef HAVE_ICONV_H
+
 /* Define to 1 if you have the <libutil.h> header file. */
 #undef HAVE_LIBUTIL_H
 

--2tNte84QOFG9kpvm--