diff --git a/sexp-format.c b/sexp-format.c
index 2480613cc4cb86d9d1d98805664b944700c18640..22b7bc4a98deed5755581a5b992c7e6f15cab732 100644
--- a/sexp-format.c
+++ b/sexp-format.c
@@ -30,6 +30,7 @@
 #include "sexp.h"
 #include "buffer.h"
 
+#include <assert.h>
 #include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -39,109 +40,203 @@
 # include "bignum.h"
 #endif
 
-static int
+/* Code copied from sexp-conv.c: sexp_put_length */
+static unsigned
 format_prefix(struct nettle_buffer *buffer,
 	      unsigned length)
 {
-  unsigned prefix_length;
-  char prefix[10];
+  unsigned digit = 1;
+  unsigned prefix_length = 1;
+  
+  for (;;)
+    {
+      unsigned next = digit * 10;
+      if (next > length)
+	break;
 
-  /* NOTE: Using the return value of sprintf is not entirely
-   * portable. */
-  prefix_length = snprintf(prefix, sizeof(prefix), "%u:", length);
-  if (prefix_length >= sizeof(prefix))
-    return 0;
+      prefix_length++;
+      digit = next;
+    }
 
-  return nettle_buffer_write(buffer, prefix_length, prefix);
+  if (buffer)
+    {
+      for (; digit; length %= digit, digit /= 10)
+	if (!NETTLE_BUFFER_PUTC(buffer, '0' + length / digit))
+	  return 0;
+      
+      if (!NETTLE_BUFFER_PUTC(buffer, ':'))
+	return 0;
+    }
+
+  return prefix_length + 1;
 }
 
-static int
+static unsigned
 format_length_string(struct nettle_buffer *buffer,
 		     unsigned length, const char *s)
 {
-  return format_prefix(buffer, length)
-    && nettle_buffer_write(buffer, length, s);
-}
+  unsigned done = format_prefix(buffer, length);
+  if (!done)
+    return 0;
 
-static uint8_t *
-format_space(struct nettle_buffer *buffer,
-	     unsigned length)
-{
-  return format_prefix(buffer, length)
-    ? nettle_buffer_space(buffer, length) : NULL;
+  if (buffer && !nettle_buffer_write(buffer, length, s))
+    return 0;
+  
+  return done + length;
 }
 
-static int
+static unsigned
 format_string(struct nettle_buffer *buffer,
 	      const char *s)
 {
   return format_length_string(buffer, strlen(s), s);
 }
 
-int
-sexp_format(struct nettle_buffer *buffer, const char *format, ...)
+unsigned
+sexp_vformat(struct nettle_buffer *buffer, const char *format, va_list args)
 {
-  va_list args;
   unsigned nesting = 0;
-  
-  va_start(args, format);
+  unsigned done = 0;
 
   for (;;)
     switch (*format++)
       {
       case '\0':
-	if (nesting)
-	  {
-	  fail:
-	    va_end(args);
-	    return 0;
-	  }
-	else
-	  {
-	    va_end(args);
-	    return 1;
-	  }
+	assert(!nesting);
+	    
+	return done;
+
       case '(':
-	if (!NETTLE_BUFFER_PUTC(buffer, '('))
-	  goto fail;
+	if (buffer && !NETTLE_BUFFER_PUTC(buffer, '('))
+	  return 0;
 
+	done++;
 	nesting++;
 	break;
 
       case ')':
-	if (!nesting)
-	  abort();
-	if (!NETTLE_BUFFER_PUTC(buffer, ')'))
-	  goto fail;
-	
+	assert (nesting);
+	if (buffer && !NETTLE_BUFFER_PUTC(buffer, ')'))
+	  return 0;
+
+	done++;
 	nesting--;
 	break;
 
       case '%':
 	switch (*format++)
 	  {
+	  case 'z':
+	    {
+	      const char *s = va_arg(args, const char *);
+	      unsigned length = format_string(buffer, s);
+
+	      if (!length)
+		return 0;
+
+	      done += length;
+	      break;
+	    }
 	  case 's':
 	    {
+	      unsigned length = va_arg(args, unsigned);
+	      const char *s = va_arg(args, const char *);
+	      unsigned prefix_length = format_prefix(buffer, length);
+	      
+	      if (!prefix_length)
+		return 0;
+
+	      done += prefix_length;
+
+	      if (buffer && !nettle_buffer_write(buffer, length, s))
+		return 0;
+
+	      done += length;
+
+	      break;
+	    }
+	  case 'l':
+	    {
+	      unsigned length = va_arg(args, unsigned);
 	      const char *s = va_arg(args, const char *);
-	      format_string(buffer, s);
+
+	      if (buffer && !nettle_buffer_write(buffer, length, s))
+		return 0;
+	      
+	      done += length;
+	      break;
+	    }
+	  case 'i':
+	    {
+	      uint32_t x = va_arg(args, uint32_t);
+	      unsigned length;
+	      
+	      if (x < 0x100)
+		length = 1;
+	      else if (x < 0x10000L)
+		length = 2;
+	      else if (x < 0x1000000L)
+		length = 3;
+	      else
+		length = 4;
+
+	      if (buffer && !(NETTLE_BUFFER_PUTC(buffer, '0' + length)
+			      && NETTLE_BUFFER_PUTC(buffer, ':')))
+		return 0;
+
+	      done += (2 + length);
+
+	      if (buffer)
+		switch(length)
+		{
+		case 4:
+		  if (!NETTLE_BUFFER_PUTC(buffer, x >> 24))
+		    return 0;
+		  /* Fall through */
+		case 3:
+		  if (!NETTLE_BUFFER_PUTC(buffer, (x >> 16) & 0xff))
+		    return 0;
+		  /* Fall through */
+		case 2:
+		  if (!NETTLE_BUFFER_PUTC(buffer, (x >> 8) & 0xff))
+		    return 0;
+		  /* Fall through */
+		case 1:
+		  if (!NETTLE_BUFFER_PUTC(buffer, x & 0xff))
+		    return 0;
+		  break;
+		default:
+		  abort();
+		}
 	      break;
 	    }
 	  case 'b':
 	    {
 #if HAVE_LIBGMP
 	      const MP_INT *n = va_arg(args, const MP_INT *);
-	      uint8_t *space;
 	      unsigned length;
+	      unsigned prefix_length;
 	      
-	      if (mpz_sgn(n) < 0)
-		goto fail;
+	      assert(mpz_sgn(n) >= 0);
 
 	      length = nettle_mpz_sizeinbase_256(n);
+	      prefix_length = format_prefix(buffer, length);
+	      if (!prefix_length)
+		return 0;
+
+	      done += prefix_length;
 
-	      space = format_space(buffer, length);
-	      if (!space)
-		goto fail;
-	      nettle_mpz_get_str_256(length, space, n);
+	      if (buffer)
+		{
+		  uint8_t *space = nettle_buffer_space(buffer, length);
+		  if (!space)
+		    return 0;
+		  
+		  nettle_mpz_get_str_256(length, space, n);
+		}
+
+	      done += length;
+	      
 #else /* ! HAVE_LIBGMP */
 	      abort();
 #endif /* ! HAVE_LIBGMP */
@@ -152,3 +247,16 @@ sexp_format(struct nettle_buffer *buffer, const char *format, ...)
 	  }
       }
 }
+
+unsigned
+sexp_format(struct nettle_buffer *buffer, const char *format, ...)
+{
+  va_list args;
+  unsigned done;
+  
+  va_start(args, format);
+  done = sexp_vformat(buffer, format, args);
+  va_end(args);
+
+  return done;
+}
diff --git a/sexp.h b/sexp.h
index 24d1906315be2bffccc565904d3f306697284571..21e88eafe004d892cc98cb58310ca9e0f996890e 100644
--- a/sexp.h
+++ b/sexp.h
@@ -27,6 +27,7 @@
 #define NETTLE_SEXP_H_INCLUDED
 
 #include <inttypes.h>
+#include <stdarg.h>
 
 enum sexp_type
   { SEXP_ATOM, SEXP_LIST, SEXP_END };
@@ -94,6 +95,8 @@ sexp_iterator_check_types(struct sexp_iterator *iterator,
  *
  * For a matching key, the corresponding iterator is initialized
  * pointing at the start of REST.
+ *
+ * On success, exits the current list.
  */
 int
 sexp_iterator_assoc(struct sexp_iterator *iterator,
@@ -108,15 +111,42 @@ sexp_iterator_assoc(struct sexp_iterator *iterator,
 /* Declared for real in buffer.h */
 struct nettle_buffer;
 
-int
-sexp_format(struct nettle_buffer *buffer, const char *format, ...);
+/* Returns the number of output characters, or 0 on out of memory. If
+ * buffer == NULL, just compute length.
+ *
+ * Format strings can contained matched parentheses, and the following
+ * formatting specifiers:
+ *
+ *   %z   NUL-terminated string, const uint8_t *.
+ *
+ *   %s   String represented as unsigned length, const uint8_t *data.
+ *
+ *   %i   Non-negative small integer, uint32_t.
+ *
+ *   %b   Non-negative bignum, mpz_t.
+ *
+ *   %l   Literal string (no length added), typically a balanced
+ *        subexpression. Represented as unsigned length, const uint8_t
+ *        *data.
+ */
 
-int
+unsigned
+sexp_format(struct nettle_buffer *buffer,
+	    const char *format, ...);
+
+unsigned
+sexp_vformat(struct nettle_buffer *buffer,
+	     const char *format, va_list args);
+
+/* FIXME: Add argument LINE_WIDTH. If non-zero, break lines to at most
+ * that width. */
+
+unsigned
 sexp_transport_format(struct nettle_buffer *buffer,
-		      /* If non-zero, break lines to at most
-		       * line_length characters. */
-		      unsigned line_length,
 		      const char *format, ...);
 
+unsigned
+sexp_transport_vformat(struct nettle_buffer *buffer,
+		       const char *format, va_list args);
 
 #endif /* NETTLE_SEXP_H_INCLUDED */