Hi,

On 9/25/14, 3:56 PM, I wrote:
On 9/25/14 3:50 PM, Heikki Linnakangas wrote:
Are you planning to post the main patch rebased on top of this soon? As
in the next day or two? Otherwise I'll mark this as "Returned with
feedback" for this commitfest.

Yes.  With good luck I'll get you a rebased one today, otherwise it'll
have to wait until tomorrow.

Missed that promise by a day since something unexpected came up yesterday. Attached is v3 of the patch. The changes are:

  - Rebased on top of the current master
- Added a function pgp_armor_header_keys() to list all keys present in an armor
  - Changed pgp_armor_header() to use a StringInfo instead of an mbuf
- Fixed the "error is returned" problem in the documentation pointed out earlier


.marko
*** a/contrib/pgcrypto/Makefile
--- b/contrib/pgcrypto/Makefile
***************
*** 26,32 **** MODULE_big       = pgcrypto
  OBJS          = $(SRCS:.c=.o) $(WIN32RES)
  
  EXTENSION = pgcrypto
! DATA = pgcrypto--1.1.sql pgcrypto--1.0--1.1.sql pgcrypto--unpackaged--1.0.sql
  PGFILEDESC = "pgcrypto - cryptographic functions"
  
  REGRESS = init md5 sha1 hmac-md5 hmac-sha1 blowfish rijndael \
--- 26,32 ----
  OBJS          = $(SRCS:.c=.o) $(WIN32RES)
  
  EXTENSION = pgcrypto
! DATA = pgcrypto--1.2.sql pgcrypto--1.1--1.2.sql pgcrypto--1.0--1.1.sql 
pgcrypto--unpackaged--1.0.sql
  PGFILEDESC = "pgcrypto - cryptographic functions"
  
  REGRESS = init md5 sha1 hmac-md5 hmac-sha1 blowfish rijndael \
*** a/contrib/pgcrypto/expected/pgp-armor.out
--- b/contrib/pgcrypto/expected/pgp-armor.out
***************
*** 102,104 **** em9va2E=
--- 102,423 ----
  -----END PGP MESSAGE-----
  ');
  ERROR:  Corrupt ascii-armor
+ -- corrupt
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ foo:
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+ ERROR:  Corrupt ascii-armor
+ -- empty
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ foo: 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+  pgp_armor_header 
+ ------------------
+  
+ (1 row)
+ 
+ -- simple
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ foo: bar
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+  pgp_armor_header 
+ ------------------
+  bar
+ (1 row)
+ 
+ -- uninteresting keys, part 1
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ foo: found
+ bar: ignored
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+  pgp_armor_header 
+ ------------------
+  found
+ (1 row)
+ 
+ -- uninteresting keys, part 2
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ bar: ignored
+ foo: found
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+  pgp_armor_header 
+ ------------------
+  found
+ (1 row)
+ 
+ -- uninteresting keys, part 3
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ bar: ignored
+ foo: found
+ bar: ignored
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+  pgp_armor_header 
+ ------------------
+  found
+ (1 row)
+ 
+ -- insane keys, part 1
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ insane:key : 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'insane:key ');
+  pgp_armor_header 
+ ------------------
+  
+ (1 row)
+ 
+ -- insane keys, part 2
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ insane:key : text value here
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'insane:key ');
+  pgp_armor_header 
+ ------------------
+  text value here
+ (1 row)
+ 
+ -- long value
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ long: this value is more than 76 characters long, but it should still parse 
correctly as that''s permitted by RFC 4880
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'long');
+                                                 pgp_armor_header              
                                   
+ 
-----------------------------------------------------------------------------------------------------------------
+  this value is more than 76 characters long, but it should still parse 
correctly as that's permitted by RFC 4880
+ (1 row)
+ 
+ -- long value, split up
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ long: this value is more than 76 characters long, but it should still 
+ long: parse correctly as that''s permitted by RFC 4880
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'long');
+                                                 pgp_armor_header              
                                   
+ 
-----------------------------------------------------------------------------------------------------------------
+  this value is more than 76 characters long, but it should still parse 
correctly as that's permitted by RFC 4880
+ (1 row)
+ 
+ -- long value, split up, part 2
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ long: this value is more than 
+ long: 76 characters long, but it should still 
+ long: parse correctly as that''s permitted by RFC 4880
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'long');
+                                                 pgp_armor_header              
                                   
+ 
-----------------------------------------------------------------------------------------------------------------
+  this value is more than 76 characters long, but it should still parse 
correctly as that's permitted by RFC 4880
+ (1 row)
+ 
+ -- long value, split up, part 3
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ ignored: 
+ long: this value is more than 
+ ignored: 
+ long: 76 characters long, but it should still 
+ ignored: 
+ long: parse correctly as that''s permitted by RFC 4880
+ ignored: 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'long');
+                                                 pgp_armor_header              
                                   
+ 
-----------------------------------------------------------------------------------------------------------------
+  this value is more than 76 characters long, but it should still parse 
correctly as that's permitted by RFC 4880
+ (1 row)
+ 
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ Comment: dat1.blowfish.sha1.mdc.s2k3.z0
+ 
+ jA0EBAMCfFNwxnvodX9g0jwB4n4s26/g5VmKzVab1bX1SmwY7gvgvlWdF3jKisvS
+ yA6Ce1QTMK3KdL2MPfamsTUSAML8huCJMwYQFfE=
+ =JcP+
+ -----END PGP MESSAGE-----
+ ', 'Comment');
+         pgp_armor_header        
+ --------------------------------
+  dat1.blowfish.sha1.mdc.s2k3.z0
+ (1 row)
+ 
+ -- corrupt
+ select pgp_armor_header_keys('
+ -----BEGIN PGP MESSAGE-----
+ foo:
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ');
+ ERROR:  Corrupt ascii-armor
+ -- empty
+ select pgp_armor_header_keys('
+ -----BEGIN PGP MESSAGE-----
+ foo: 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ');
+  pgp_armor_header_keys 
+ -----------------------
+  foo
+ (1 row)
+ 
+ -- simple
+ select pgp_armor_header_keys('
+ -----BEGIN PGP MESSAGE-----
+ foo: bar
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ');
+  pgp_armor_header_keys 
+ -----------------------
+  foo
+ (1 row)
+ 
+ -- duplicates should be eliminated
+ select pgp_armor_header_keys('
+ -----BEGIN PGP MESSAGE-----
+ nodups: 
+ long: this value is more than 
+ nodups: 
+ long: 76 characters long, but it should still 
+ nodups: 
+ long: parse correctly as that''s permitted by RFC 4880
+ nodups: 
+ reallynodups: 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ');
+  pgp_armor_header_keys 
+ -----------------------
+  nodups
+  long
+  reallynodups
+ (3 rows)
+ 
+ -- test header generation
+ select armor('zooka', array['foo'], array['bar']);
+             armor            
+ -----------------------------
+  -----BEGIN PGP MESSAGE-----+
+  foo: bar                   +
+                             +
+  em9va2E=                   +
+  =D5cR                      +
+  -----END PGP MESSAGE-----  +
+  
+ (1 row)
+ 
+ select armor('zooka', array['Version', 'Comment'], array['Created by 
pgcrypto', 'PostgreSQL, the world''s most most advanced open source database']);
+                                   armor                                   
+ --------------------------------------------------------------------------
+  -----BEGIN PGP MESSAGE-----                                             +
+  Version: Created by pgcrypto                                            +
+  Comment: PostgreSQL, the world's most most advanced open source database+
+                                                                          +
+  em9va2E=                                                                +
+  =D5cR                                                                   +
+  -----END PGP MESSAGE-----                                               +
+  
+ (1 row)
+ 
+ select pgp_armor_header(armor('zooka', array['Version', 'Comment'], 
array['Created by pgcrypto', 'PostgreSQL, the world''s most most advanced open 
source database']), 'Comment');
+                         pgp_armor_header                         
+ -----------------------------------------------------------------
+  PostgreSQL, the world's most most advanced open source database
+ (1 row)
+ 
+ -- error/corner cases
+ select armor('', array['foo'], array['too', 'many']);
+ ERROR:  mismatched array dimensions
+ select armor('', array['too', 'many'], array['foo']);
+ ERROR:  mismatched array dimensions
+ select armor('', array[['']], array['foo']);
+ ERROR:  wrong number of array subscripts
+ select armor('', array['foo'], array[['']]);
+ ERROR:  wrong number of array subscripts
+ select armor('', array[null], array['foo']);
+ ERROR:  null value not allowed for header key
+ select armor('', array['foo'], array[null]);
+ ERROR:  null value not allowed for header value
+ select armor('', '[0:0]={"foo"}', array['foo']);
+             armor            
+ -----------------------------
+  -----BEGIN PGP MESSAGE-----+
+  foo: foo                   +
+                             +
+  =twTO                      +
+  -----END PGP MESSAGE-----  +
+  
+ (1 row)
+ 
+ select armor('', array['foo'], '[0:0]={"foo"}');
+             armor            
+ -----------------------------
+  -----BEGIN PGP MESSAGE-----+
+  foo: foo                   +
+                             +
+  =twTO                      +
+  -----END PGP MESSAGE-----  +
+  
+ (1 row)
+ 
*** /dev/null
--- b/contrib/pgcrypto/pgcrypto--1.1--1.2.sql
***************
*** 0 ****
--- 1,19 ----
+ /* contrib/pgcrypto/pgcrypto--1.1--1.2.sql */
+ 
+ -- complain if script is sourced in psql, rather than via ALTER EXTENSION
+ \echo Use "ALTER EXTENSION pgcrypto UPDATE TO '1.2'" to load this file. \quit
+ 
+ CREATE FUNCTION armor(bytea, text[], text[])
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pg_armor'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION pgp_armor_header(text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pgp_armor_header'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION pgp_armor_header_keys(text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pgp_armor_header_keys'
+ LANGUAGE C IMMUTABLE STRICT;
*** /dev/null
--- b/contrib/pgcrypto/pgcrypto--1.2.sql
***************
*** 0 ****
--- 1,222 ----
+ /* contrib/pgcrypto/pgcrypto--1.1.sql */
+ 
+ -- complain if script is sourced in psql, rather than via CREATE EXTENSION
+ \echo Use "CREATE EXTENSION pgcrypto" to load this file. \quit
+ 
+ CREATE FUNCTION digest(text, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_digest'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION digest(bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_digest'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION hmac(text, text, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_hmac'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION hmac(bytea, bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_hmac'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION crypt(text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pg_crypt'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION gen_salt(text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pg_gen_salt'
+ LANGUAGE C VOLATILE STRICT;
+ 
+ CREATE FUNCTION gen_salt(text, int4)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pg_gen_salt_rounds'
+ LANGUAGE C VOLATILE STRICT;
+ 
+ CREATE FUNCTION encrypt(bytea, bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_encrypt'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION decrypt(bytea, bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_decrypt'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION encrypt_iv(bytea, bytea, bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_encrypt_iv'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION decrypt_iv(bytea, bytea, bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_decrypt_iv'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION gen_random_bytes(int4)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_random_bytes'
+ LANGUAGE C VOLATILE STRICT;
+ 
+ CREATE FUNCTION gen_random_uuid()
+ RETURNS uuid
+ AS 'MODULE_PATHNAME', 'pg_random_uuid'
+ LANGUAGE C VOLATILE;
+ 
+ --
+ -- pgp_sym_encrypt(data, key)
+ --
+ CREATE FUNCTION pgp_sym_encrypt(text, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_text'
+ LANGUAGE C STRICT;
+ 
+ CREATE FUNCTION pgp_sym_encrypt_bytea(bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_bytea'
+ LANGUAGE C STRICT;
+ 
+ --
+ -- pgp_sym_encrypt(data, key, args)
+ --
+ CREATE FUNCTION pgp_sym_encrypt(text, text, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_text'
+ LANGUAGE C STRICT;
+ 
+ CREATE FUNCTION pgp_sym_encrypt_bytea(bytea, text, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_bytea'
+ LANGUAGE C STRICT;
+ 
+ --
+ -- pgp_sym_decrypt(data, key)
+ --
+ CREATE FUNCTION pgp_sym_decrypt(bytea, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_text'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION pgp_sym_decrypt_bytea(bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_bytea'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ --
+ -- pgp_sym_decrypt(data, key, args)
+ --
+ CREATE FUNCTION pgp_sym_decrypt(bytea, text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_text'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION pgp_sym_decrypt_bytea(bytea, text, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_bytea'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ --
+ -- pgp_pub_encrypt(data, key)
+ --
+ CREATE FUNCTION pgp_pub_encrypt(text, bytea)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_text'
+ LANGUAGE C STRICT;
+ 
+ CREATE FUNCTION pgp_pub_encrypt_bytea(bytea, bytea)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_bytea'
+ LANGUAGE C STRICT;
+ 
+ --
+ -- pgp_pub_encrypt(data, key, args)
+ --
+ CREATE FUNCTION pgp_pub_encrypt(text, bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_text'
+ LANGUAGE C STRICT;
+ 
+ CREATE FUNCTION pgp_pub_encrypt_bytea(bytea, bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_bytea'
+ LANGUAGE C STRICT;
+ 
+ --
+ -- pgp_pub_decrypt(data, key)
+ --
+ CREATE FUNCTION pgp_pub_decrypt(bytea, bytea)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_text'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION pgp_pub_decrypt_bytea(bytea, bytea)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_bytea'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ --
+ -- pgp_pub_decrypt(data, key, psw)
+ --
+ CREATE FUNCTION pgp_pub_decrypt(bytea, bytea, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_text'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_bytea'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ --
+ -- pgp_pub_decrypt(data, key, psw, arg)
+ --
+ CREATE FUNCTION pgp_pub_decrypt(bytea, bytea, text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_text'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text, text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_bytea'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ --
+ -- PGP key ID
+ --
+ CREATE FUNCTION pgp_key_id(bytea)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pgp_key_id_w'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ --
+ -- pgp armor
+ --
+ CREATE FUNCTION armor(bytea)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pg_armor'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION armor(bytea, text[], text[])
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pg_armor'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION dearmor(text)
+ RETURNS bytea
+ AS 'MODULE_PATHNAME', 'pg_dearmor'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION pgp_armor_header(text, text)
+ RETURNS text
+ AS 'MODULE_PATHNAME', 'pgp_armor_header'
+ LANGUAGE C IMMUTABLE STRICT;
+ 
+ CREATE FUNCTION pgp_armor_header_keys(text)
+ RETURNS SETOF text
+ AS 'MODULE_PATHNAME', 'pgp_armor_header_keys'
+ LANGUAGE C IMMUTABLE STRICT;
*** a/contrib/pgcrypto/pgcrypto.control
--- b/contrib/pgcrypto/pgcrypto.control
***************
*** 1,5 ****
  # pgcrypto extension
  comment = 'cryptographic functions'
! default_version = '1.1'
  module_pathname = '$libdir/pgcrypto'
  relocatable = true
--- 1,5 ----
  # pgcrypto extension
  comment = 'cryptographic functions'
! default_version = '1.2'
  module_pathname = '$libdir/pgcrypto'
  relocatable = true
*** a/contrib/pgcrypto/pgp-armor.c
--- b/contrib/pgcrypto/pgp-armor.c
***************
*** 178,184 **** b64_dec_len(unsigned srclen)
   * PGP armor
   */
  
! static const char *armor_header = "-----BEGIN PGP MESSAGE-----\n\n";
  static const char *armor_footer = "\n-----END PGP MESSAGE-----\n";
  
  /* CRC24 implementation from rfc2440 */
--- 178,184 ----
   * PGP armor
   */
  
! static const char *armor_header = "-----BEGIN PGP MESSAGE-----\n";
  static const char *armor_footer = "\n-----END PGP MESSAGE-----\n";
  
  /* CRC24 implementation from rfc2440 */
***************
*** 204,217 **** crc24(const uint8 *data, unsigned len)
  }
  
  void
! pgp_armor_encode(const uint8 *src, int len, StringInfo dst)
  {
        int                     res;
        unsigned        b64len;
        unsigned        crc = crc24(src, len);
  
        appendStringInfoString(dst, armor_header);
  
        /* make sure we have enough room to b64_encode() */
        b64len = b64_enc_len(len);
        enlargeStringInfo(dst, (int) b64len);
--- 204,229 ----
  }
  
  void
! pgp_armor_encode(const uint8 *src, unsigned len, StringInfo dst,
!                                int num_headers, char **keys, char **values)
  {
+       int                     n;
        int                     res;
        unsigned        b64len;
        unsigned        crc = crc24(src, len);
  
        appendStringInfoString(dst, armor_header);
  
+       for (n = 0; n < num_headers; n++)
+       {
+               appendStringInfoString(dst, keys[n]);
+               appendStringInfoChar(dst, ':');
+               appendStringInfoChar(dst, ' ');
+               appendStringInfoString(dst, values[n]);
+               appendStringInfoChar(dst, '\n');
+       }
+       appendStringInfoChar(dst, '\n');
+ 
        /* make sure we have enough room to b64_encode() */
        b64len = b64_enc_len(len);
        enlargeStringInfo(dst, (int) b64len);
***************
*** 371,373 **** pgp_armor_decode(const uint8 *src, int len, StringInfo dst)
--- 383,516 ----
  out:
        return res;
  }
+ 
+ /*
+  * pgp_extract_armor_headers can be used in two different ways:
+  *
+  *   1) If key and valuedst are not NULL, this function finds the value for 
the
+  *      key specified by *key, and copies it to valuedst.  key_len should be
+  *      set to the length of the string in *key, which need not be null
+  *      terminated.  The return value is 1 if the key was found, 0 if it was
+  *      not found, or negative if an error occurred.
+  *   2) If keylistdst is not NULL, *keylistdst is set to a list containing the
+  *      set of armor header keys present in the armor.  The return value is 0,
+  *      or negative if an error occurred.
+  */
+ int
+ pgp_extract_armor_headers(const uint8 *src, unsigned len, const char *key,
+                                                 unsigned key_len, StringInfo 
valuedst,
+                                                 List **keylistdst)
+ {
+       const uint8 *p = src;
+       const uint8 *data_end = src + len;
+       const uint8 *armor_end;
+       const uint8 *eol,
+                               *colon;
+       int                     hlen;
+       int                     res = PXE_PGP_CORRUPT_ARMOR;
+       bool            found = false;
+       List       *keys = NIL;
+ 
+       Assert((valuedst != NULL) != (keylistdst != NULL));
+       Assert((key == NULL) == (valuedst == NULL));
+ 
+       /* armor start */
+       hlen = find_header(src, data_end, &p, 0);
+       if (hlen <= 0)
+               goto out;
+       p += hlen;
+ 
+       /* armor end */
+       hlen = find_header(p, data_end, &armor_end, 1);
+       if (hlen <= 0)
+               goto out;
+ 
+       /* read comments until an empty line or the end of data */
+       while (p < armor_end)
+       {
+               res = PXE_PGP_CORRUPT_ARMOR;
+ 
+               if (*p == '\n' || *p == '\r')
+               {
+                       res = 0;
+                       break;
+               }
+ 
+               eol = memchr(p, '\n', armor_end - p);
+               if (!eol)
+                       goto out;
+ 
+               /* find the next key */
+               colon = p;
+               while (1)
+               {
+                       colon = memchr(colon, ':', eol - colon);
+                       if (!colon)
+                               goto out;
+                       if (colon == eol)
+                               goto out;
+ 
+                       /* if it's not followed by a space, this isn't the full 
key */
+                       if (*(colon + 1) == ' ')
+                               break;
+                       colon = colon + 1;
+               }
+ 
+               /*
+                * See if this the key we're looking for.  Note that even if it 
is, we
+                * need to keep scanning the headers since it might be split 
into
+                * multiple lines.
+                */
+               if (valuedst != NULL &&
+                       key_len == colon - p &&
+                       memcmp(p, key, key_len) == 0)
+               {
+                       appendBinaryStringInfo(valuedst, (const char *) colon + 
2, (int) (eol - colon - 2));
+                       found = true;
+               }
+               else if (keylistdst != NULL)
+               {
+                       char *key;
+                       size_t len = colon - p;
+                       ListCell *lc;
+ 
+                       key = palloc(len + 1);
+                       memcpy(key, p, len);
+                       key[len] = '\0';
+ 
+                       /* check for duplicates */
+                       found = false;
+                       foreach(lc, keys)
+                       {
+                               if (strcmp(key, lfirst(lc)) == 0)
+                               {
+                                       found = true;
+                                       break;
+                               }
+                       }
+                       if (!found)
+                               keys = lappend(keys, key);
+                       else
+                               pfree(key);
+               }
+               /* step to start of next line */
+               p = eol + 1;
+       }
+ 
+ out:
+       if (res < 0)
+       {
+               if (keys != NIL)
+                       list_free_deep(keys);
+               return res;
+       }
+       if (keylistdst != NULL)
+       {
+               *keylistdst = keys;
+               return 0;
+       }
+       else if (valuedst != NULL)
+               return found ? 1 : 0;
+       else
+               elog(ERROR, "unexpected input arguments to 
pgp_extract_armor_headers");
+ }
*** a/contrib/pgcrypto/pgp-pgsql.c
--- b/contrib/pgcrypto/pgp-pgsql.c
***************
*** 32,39 ****
--- 32,42 ----
  #include "postgres.h"
  
  #include "lib/stringinfo.h"
+ #include "catalog/pg_type.h"
  #include "mb/pg_wchar.h"
  #include "utils/builtins.h"
+ #include "utils/array.h"
+ #include "funcapi.h"
  
  #include "mbuf.h"
  #include "px.h"
***************
*** 56,61 **** PG_FUNCTION_INFO_V1(pgp_key_id_w);
--- 59,66 ----
  
  PG_FUNCTION_INFO_V1(pg_armor);
  PG_FUNCTION_INFO_V1(pg_dearmor);
+ PG_FUNCTION_INFO_V1(pgp_armor_header);
+ PG_FUNCTION_INFO_V1(pgp_armor_header_keys);
  
  /*
   * Mix a block of data into RNG.
***************
*** 816,821 **** pgp_pub_decrypt_text(PG_FUNCTION_ARGS)
--- 821,888 ----
   * Wrappers for PGP ascii armor
   */
  
+ static int
+ parse_key_value_arrays(ArrayType *key_array, ArrayType *val_array,
+                                          char ***p_keys, char ***p_values)
+ {
+       int             nkdims = ARR_NDIM(key_array);
+       int             nvdims = ARR_NDIM(val_array);
+       char   **keys,
+                  **values;
+       Datum  *key_datums,
+                  *val_datums;
+       bool   *key_nulls,
+                  *val_nulls;
+       int             key_count,
+                       val_count;
+       int             i;
+ 
+       if (nkdims > 1 || nkdims != nvdims)
+               ereport(ERROR,
+                               (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+                               errmsg("wrong number of array subscripts")));
+       if (nkdims == 0)
+               return 0;
+ 
+       deconstruct_array(key_array,
+                                         TEXTOID, -1, false, 'i',
+                                         &key_datums, &key_nulls, &key_count);
+ 
+       deconstruct_array(val_array,
+                                         TEXTOID, -1, false, 'i',
+                                         &val_datums, &val_nulls, &val_count);
+ 
+       if (key_count != val_count)
+               ereport(ERROR,
+                               (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+                                errmsg("mismatched array dimensions")));
+ 
+       keys = (char **) palloc(sizeof(char *) * key_count);
+       values = (char **) palloc(sizeof(char *) * val_count);
+ 
+       for (i = 0; i < key_count; i++)
+       {
+               char *v;
+ 
+               if (key_nulls[i])
+                       ereport(ERROR,
+                                       
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+                                        errmsg("null value not allowed for 
header key")));
+               if (val_nulls[i])
+                       ereport(ERROR,
+                                       
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+                                        errmsg("null value not allowed for 
header value")));
+               v = TextDatumGetCString(key_datums[i]);
+               keys[i] = pg_server_to_any(v, strlen(v), PG_UTF8);
+               v = TextDatumGetCString(val_datums[i]);
+               values[i] = pg_server_to_any(v, strlen(v), PG_UTF8);
+       }
+ 
+       *p_keys = keys;
+       *p_values = values;
+       return key_count;
+ }
+ 
  Datum
  pg_armor(PG_FUNCTION_ARGS)
  {
***************
*** 823,835 **** pg_armor(PG_FUNCTION_ARGS)
        text       *res;
        int                     data_len;
        StringInfoData buf;
  
        data = PG_GETARG_BYTEA_P(0);
        data_len = VARSIZE(data) - VARHDRSZ;
  
        initStringInfo(&buf);
  
!       pgp_armor_encode((uint8 *) VARDATA(data), data_len, &buf);
  
        res = palloc(VARHDRSZ + buf.len);
        SET_VARSIZE(res, VARHDRSZ + buf.len);
--- 890,914 ----
        text       *res;
        int                     data_len;
        StringInfoData buf;
+       int                     num_headers = 0;
+       char      **keys = NULL,
+                         **values = NULL;
  
        data = PG_GETARG_BYTEA_P(0);
        data_len = VARSIZE(data) - VARHDRSZ;
+       if (PG_NARGS() == 3)
+       {
+               num_headers = parse_key_value_arrays(PG_GETARG_ARRAYTYPE_P(1),
+                                                                               
         PG_GETARG_ARRAYTYPE_P(2),
+                                                                               
         &keys, &values);
+       }
+       else if (PG_NARGS() != 1)
+               elog(ERROR, "unexpected number of arguments %d", PG_NARGS());
  
        initStringInfo(&buf);
  
!       pgp_armor_encode((uint8 *) VARDATA(data), data_len, &buf,
!                                        num_headers, keys, values);
  
        res = palloc(VARHDRSZ + buf.len);
        SET_VARSIZE(res, VARHDRSZ + buf.len);
***************
*** 868,873 **** pg_dearmor(PG_FUNCTION_ARGS)
--- 947,1052 ----
        PG_RETURN_TEXT_P(res);
  }
  
+ Datum
+ pgp_armor_header(PG_FUNCTION_ARGS)
+ {
+       bytea      *data;
+       int                     data_len,
+                               res;
+       text       *key,
+                          *utf8key;
+       StringInfoData buf;
+ 
+       data = PG_GETARG_BYTEA_P(0);
+       data_len = VARSIZE(data) - VARHDRSZ;
+ 
+       key = PG_GETARG_TEXT_P(1);
+       utf8key = convert_to_utf8(key);
+ 
+       initStringInfo(&buf);
+       res = pgp_extract_armor_headers((uint8 *) VARDATA(data), data_len,
+                                                                       
VARDATA(utf8key), VARSIZE(utf8key) - VARHDRSZ,
+                                                                       &buf, 
NULL);
+       if (res < 0)
+               ereport(ERROR,
+                               
(errcode(ERRCODE_EXTERNAL_ROUTINE_INVOCATION_EXCEPTION),
+                                errmsg("%s", px_strerror(res))));
+ 
+       PG_FREE_IF_COPY(data, 0);
+       if (utf8key != key)
+               pfree(utf8key);
+       PG_FREE_IF_COPY(key, 1);
+       if (res == 0)
+       {
+               pfree(buf.data);
+               PG_RETURN_NULL();
+       }
+       else
+       {
+               /* assume it's UTF-8 */
+               char *utf;
+               text *result;
+ 
+               /* 0-terminate the string for cstring_to_text */
+               appendStringInfoChar(&buf, '\x00');
+               utf = pg_any_to_server(buf.data, buf.len - 1, PG_UTF8);
+               result = cstring_to_text(utf);
+               pfree(buf.data);
+               PG_RETURN_TEXT_P(result);
+       }
+ }
+ 
+ Datum
+ pgp_armor_header_keys(PG_FUNCTION_ARGS)
+ {
+       FuncCallContext *funcctx;
+       List *keys;
+ 
+       if (SRF_IS_FIRSTCALL())
+       {
+               bytea      *data;
+               int                     data_len,
+                                       res;
+               MemoryContext oldcontext;
+ 
+               data = PG_GETARG_BYTEA_P(0);
+               data_len = VARSIZE(data) - VARHDRSZ;
+ 
+               funcctx = SRF_FIRSTCALL_INIT();
+ 
+               /* we need the resulting list allocated in the multi call 
context */
+               oldcontext = 
MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+ 
+               res = pgp_extract_armor_headers((uint8 *) VARDATA(data), 
data_len,
+                                                                               
NULL, 0, NULL, &keys);
+               if (res < 0)
+                       ereport(ERROR,
+                                       
(errcode(ERRCODE_EXTERNAL_ROUTINE_INVOCATION_EXCEPTION),
+                                        errmsg("%s", px_strerror(res))));
+ 
+               MemoryContextSwitchTo(oldcontext);
+               funcctx->user_fctx = keys;
+       }
+ 
+       funcctx = SRF_PERCALL_SETUP();
+       keys = (List *) funcctx->user_fctx;
+       if (keys == NIL)
+               SRF_RETURN_DONE(funcctx);
+       else
+       {
+               /* we assume the keys are UTF-8 */
+               char *utf;
+               text *result;
+ 
+               utf = pg_any_to_server(linitial(keys), strlen(linitial(keys)), 
PG_UTF8);
+               result = cstring_to_text(utf);
+               funcctx->user_fctx = list_delete_first(keys);
+               SRF_RETURN_NEXT(funcctx, PointerGetDatum(result));
+       }
+ }
+ 
+ 
+ 
  /*
   * Wrappers for PGP key id
   */
*** a/contrib/pgcrypto/pgp.h
--- b/contrib/pgcrypto/pgp.h
***************
*** 30,35 ****
--- 30,36 ----
   */
  
  #include "lib/stringinfo.h"
+ #include "nodes/pg_list.h"
  
  #include "mbuf.h"
  #include "px.h"
***************
*** 276,283 **** void           pgp_cfb_free(PGP_CFB *ctx);
  int                   pgp_cfb_encrypt(PGP_CFB *ctx, const uint8 *data, int 
len, uint8 *dst);
  int                   pgp_cfb_decrypt(PGP_CFB *ctx, const uint8 *data, int 
len, uint8 *dst);
  
! void          pgp_armor_encode(const uint8 *src, int len, StringInfo dst);
  int                   pgp_armor_decode(const uint8 *src, int len, StringInfo 
dst);
  
  int                   pgp_compress_filter(PushFilter **res, PGP_Context *ctx, 
PushFilter *dst);
  int                   pgp_decompress_filter(PullFilter **res, PGP_Context 
*ctx, PullFilter *src);
--- 277,288 ----
  int                   pgp_cfb_encrypt(PGP_CFB *ctx, const uint8 *data, int 
len, uint8 *dst);
  int                   pgp_cfb_decrypt(PGP_CFB *ctx, const uint8 *data, int 
len, uint8 *dst);
  
! void          pgp_armor_encode(const uint8 *src, unsigned len, StringInfo dst,
!                                                        int num_headers, char 
**keys, char **values);
  int                   pgp_armor_decode(const uint8 *src, int len, StringInfo 
dst);
+ int                   pgp_extract_armor_headers(const uint8 *src, unsigned 
len,
+                                                                         const 
char *key, unsigned key_len,
+                                                                         
StringInfo valuedst, List **keylistdst);
  
  int                   pgp_compress_filter(PushFilter **res, PGP_Context *ctx, 
PushFilter *dst);
  int                   pgp_decompress_filter(PullFilter **res, PGP_Context 
*ctx, PullFilter *src);
*** a/contrib/pgcrypto/sql/pgp-armor.sql
--- b/contrib/pgcrypto/sql/pgp-armor.sql
***************
*** 56,58 **** em9va2E=
--- 56,263 ----
  =ZZZZ
  -----END PGP MESSAGE-----
  ');
+ 
+ -- corrupt
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ foo:
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+ 
+ -- empty
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ foo: 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+ 
+ -- simple
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ foo: bar
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+ 
+ -- uninteresting keys, part 1
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ foo: found
+ bar: ignored
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+ 
+ -- uninteresting keys, part 2
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ bar: ignored
+ foo: found
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+ 
+ -- uninteresting keys, part 3
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ bar: ignored
+ foo: found
+ bar: ignored
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'foo');
+ 
+ -- insane keys, part 1
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ insane:key : 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'insane:key ');
+ 
+ -- insane keys, part 2
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ insane:key : text value here
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'insane:key ');
+ 
+ -- long value
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ long: this value is more than 76 characters long, but it should still parse 
correctly as that''s permitted by RFC 4880
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'long');
+ 
+ -- long value, split up
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ long: this value is more than 76 characters long, but it should still 
+ long: parse correctly as that''s permitted by RFC 4880
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'long');
+ 
+ -- long value, split up, part 2
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ long: this value is more than 
+ long: 76 characters long, but it should still 
+ long: parse correctly as that''s permitted by RFC 4880
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'long');
+ 
+ -- long value, split up, part 3
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ ignored: 
+ long: this value is more than 
+ ignored: 
+ long: 76 characters long, but it should still 
+ ignored: 
+ long: parse correctly as that''s permitted by RFC 4880
+ ignored: 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ', 'long');
+ 
+ select pgp_armor_header('
+ -----BEGIN PGP MESSAGE-----
+ Comment: dat1.blowfish.sha1.mdc.s2k3.z0
+ 
+ jA0EBAMCfFNwxnvodX9g0jwB4n4s26/g5VmKzVab1bX1SmwY7gvgvlWdF3jKisvS
+ yA6Ce1QTMK3KdL2MPfamsTUSAML8huCJMwYQFfE=
+ =JcP+
+ -----END PGP MESSAGE-----
+ ', 'Comment');
+ 
+ -- corrupt
+ select pgp_armor_header_keys('
+ -----BEGIN PGP MESSAGE-----
+ foo:
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ');
+ 
+ -- empty
+ select pgp_armor_header_keys('
+ -----BEGIN PGP MESSAGE-----
+ foo: 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ');
+ 
+ -- simple
+ select pgp_armor_header_keys('
+ -----BEGIN PGP MESSAGE-----
+ foo: bar
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ');
+ 
+ -- duplicates should be eliminated
+ select pgp_armor_header_keys('
+ -----BEGIN PGP MESSAGE-----
+ nodups: 
+ long: this value is more than 
+ nodups: 
+ long: 76 characters long, but it should still 
+ nodups: 
+ long: parse correctly as that''s permitted by RFC 4880
+ nodups: 
+ reallynodups: 
+ 
+ em9va2E=
+ =ZZZZ
+ -----END PGP MESSAGE-----
+ ');
+ 
+ -- test header generation
+ select armor('zooka', array['foo'], array['bar']);
+ select armor('zooka', array['Version', 'Comment'], array['Created by 
pgcrypto', 'PostgreSQL, the world''s most most advanced open source database']);
+ select pgp_armor_header(armor('zooka', array['Version', 'Comment'], 
array['Created by pgcrypto', 'PostgreSQL, the world''s most most advanced open 
source database']), 'Comment');
+ 
+ -- error/corner cases
+ select armor('', array['foo'], array['too', 'many']);
+ select armor('', array['too', 'many'], array['foo']);
+ select armor('', array[['']], array['foo']);
+ select armor('', array['foo'], array[['']]);
+ select armor('', array[null], array['foo']);
+ select armor('', array['foo'], array[null]);
+ select armor('', '[0:0]={"foo"}', array['foo']);
+ select armor('', array['foo'], '[0:0]={"foo"}');
*** a/doc/src/sgml/pgcrypto.sgml
--- b/doc/src/sgml/pgcrypto.sgml
***************
*** 691,703 **** pgp_key_id(bytea) returns text
     </indexterm>
  
  <synopsis>
! armor(data bytea) returns text
  dearmor(data text) returns bytea
  </synopsis>
     <para>
      These functions wrap/unwrap binary data into PGP ASCII-armor format,
      which is basically Base64 with CRC and additional formatting.
     </para>
    </sect3>
  
    <sect3>
--- 691,751 ----
     </indexterm>
  
  <synopsis>
! armor(data bytea [ , keys text[], values text[] ]) returns text
  dearmor(data text) returns bytea
  </synopsis>
     <para>
      These functions wrap/unwrap binary data into PGP ASCII-armor format,
      which is basically Base64 with CRC and additional formatting.
     </para>
+ 
+    <para>
+     For <function>armor</>, if the <parameter>keys</> and <parameter>values</>
+     arrays are specified, their members are written into the armored data as
+     <literal>armor headers</>.  For each member in <parameter>keys</>, the
+     value in <parameter>values</> with the corresponding ordinal is used as
+     the value for that key.  Both arrays must be single-dimensional, and they
+     must be of the same length.  All text is converted into UTF-8.
+    </para>
+   </sect3>
+ 
+   <sect3>
+    <title><function>pgp_armor_header</function></title>
+ 
+    <indexterm>
+     <primary>pgp_armor_header</primary>
+    </indexterm>
+ 
+ <synopsis>
+ pgp_armor_header(data text, key text) returns text
+ </synopsis>
+    <para>
+     <function>pgp_armor_header()</> extracts the <literal>armor header</> with
+     the key <parameter>key</> from <parameter>data</>.  Before matching,
+     <parameter>key</> is converted into UTF-8.  Also all data in the armored
+     text is assumed to be UTF-8.  If part of the data is not valid UTF-8 or
+     <parameter>key</> can not be converted to UTF-8, an error is raised.
+     If the key <parameter>key</> appears multiple times in the armored text,
+     all values are concatenated into the return value.  If the key does not
+     appear in the armored text, the return value is NULL.
+    </para>
+   </sect3>
+ 
+   <sect3>
+    <title><function>pgp_armor_header_keys</function></title>
+ 
+    <indexterm>
+     <primary>pgp_armor_header_keys</primary>
+    </indexterm>
+ 
+ <synopsis>
+ pgp_armor_header_keys(data text) returns setof text
+ </synopsis>
+    <para>
+     <function>pgp_armor_header_keys()</> extracts the list of <literal>armor
+     header</> keys from <parameter>data</>.  The keys are all assumed to be in
+     UTF-8.  If any of the keys is not valid UTF-8, an error is raised.
+    </para>
    </sect3>
  
    <sect3>
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to