Howard Chu. In addition, "postmap lmdb:/file/name" forgot
entries stored up to and including the duplicate key. File:
util/slmdb.c.
+
+20210605
+
+ Fixed a few more potential dangling pointer cases in the
+ LMDB client, future-proofing code paths that sofar aren't
+ used. File: util/slmdb.c.
+
+ Added LMDB integration tests using the postmmap command.
+ Files: postmap/Makefile.in, postmap/lmdb_abb, postmap/lmdb_abb.ref.
+
+ Cleanup: reset errno in the fail: database methods for
+ consistent error messages. File: util/dict_fail.c.
+
+ Cleanup: new vstream_control() option to give a memory stream
+ ownership of the underlying VSTRING. This simplifies resource
+ management for read-only streams. Files: util/vstream.[hc].
+
+ Cleanup: extpar() returns an error in case of a missing
+ initial '{', instead of aborting. This simplifies the
+ implementation of some callers. File: util/extpar.c.
+
+ Feature: inline pcre, regexp, and cidr table definition in main.cf
+ or master.cf, to improve their usability in matchlists. Files:
+ util/dict_stream.c, util/dict.h, util/dict_pcre.c,
+ util/dict_regexp.c, util/dict_cidr.c, and test files.
more recent Eclipse Public License 2.0. Recipients can choose to take
the software under the license of their choice. Those who are more
comfortable with the IPL can continue with that license.
+
+Major changes with snapshot 20210605
+====================================
+
+Support to inline the content of small cidr, pcre, and regexp tables.
+
+Example:
+
+ smtpd_forbidden_commands =
+ CONNECT GET POST regexp:{{/^[^A-Z]/ Thrash}}
+
+The basic syntax is:
+
+/etc/postfix/main.cf:
+ parameter = .. map-type:{ { rule-1 }, { rule-2 } .. } ..
+
+/etc/postfix/master.cf:
+ .. -o { parameter = .. map-type:{ { rule-1 }, { rule-2 } .. } .. } ..
+
+Postfix ignores whitespace after '{' and before '}', and writes each
+rule as one text line to an in-memory file:
+
+in-memory file:
+ rule-1
+ rule-2
+ ..
+
+Postfix parses the result as if it is a file in /etc/postfix.
+
+Note: if a rule contains $, specify $$, to keep Postfix from trying to
+do $name expansion as it evaluates the parameter value.
Wish list:
+ Make postscreen drop the connection after a non-SMTP command,
+ just like the real Postfix SMTP daemon.
+
Add verp=+= to the qmgr "from=" logging.
Make smtpd_relay_before_recipient_restrictions settable
Note: address information may be enclosed inside "[]" but this form is
not required.
+<b>INLINE SPECIFICATION</b>
+ The contents of a table may be specified in the table name. The basic
+ syntax is:
+
+ <a href="postconf.5.html">main.cf</a>:
+ <i>parameter</i> <b>= .. <a href="pcre_table.5.html">pcre</a>:{ {</b> <i>rule-1</i> <b>}, {</b> <i>rule-2</i> <b>} .. } ..</b>
+
+ <a href="master.5.html">master.cf</a>:
+ <b>.. -o {</b> <i>parameter</i> <b>= .. <a href="pcre_table.5.html">pcre</a>:{ {</b> <i>rule-1</i> <b>}, {</b> <i>rule-2</i> <b>} .. } .. } ..</b>
+
+ Postfix ignores whitespace after '{' and before '}', and writes each
+ <i>rule</i> as one text line to an in-memory file:
+
+ in-memory file:
+ rule-1
+ rule-2
+ ..
+
+ Postfix parses the result as if it is a file in /etc/postfix.
+
<b>EXAMPLE SMTPD ACCESS MAP</b>
/etc/postfix/<a href="postconf.5.html">main.cf</a>:
<a href="postconf.5.html#smtpd_client_restrictions">smtpd_client_restrictions</a> = ... <a href="cidr_table.5.html">cidr</a>:/etc/postfix/client.cidr ...
the expression does not match, substitutions are not available for
negated patterns.
+<b>INLINE SPECIFICATION</b>
+ The contents of a table may be specified in the table name. The basic
+ syntax is:
+
+ <a href="postconf.5.html">main.cf</a>:
+ <i>parameter</i> <b>= .. <a href="pcre_table.5.html">pcre</a>:{ {</b> <i>rule-1</i> <b>}, {</b> <i>rule-2</i> <b>} .. } ..</b>
+
+ <a href="master.5.html">master.cf</a>:
+ <b>.. -o {</b> <i>parameter</i> <b>= .. <a href="pcre_table.5.html">pcre</a>:{ {</b> <i>rule-1</i> <b>}, {</b> <i>rule-2</i> <b>} .. } .. } ..</b>
+
+ Postfix ignores whitespace after '{' and before '}', and writes each
+ <i>rule</i> as one text line to an in-memory file:
+
+ in-memory file:
+ rule-1
+ rule-2
+ ..
+
+ Postfix parses the result as if it is a file in /etc/postfix.
+
+ Note: if a rule contains <b>$</b>, specify <b>$$</b> to keep Postfix from trying to
+ do <i>$name</i> expansion as it evaluates a parameter value.
+
<b>EXAMPLE SMTPD ACCESS MAP</b>
# Protect your outgoing majordomo exploders
/^(?!owner-)(.*)-outgoing@(.*)/ 550 Use ${1}@${2} instead
the expression does not match, substitutions are not available for
negated patterns.
+<b>INLINE SPECIFICATION</b>
+ The contents of a table may be specified in the table name. The basic
+ syntax is:
+
+ <a href="postconf.5.html">main.cf</a>:
+ <i>parameter</i> <b>= .. <a href="pcre_table.5.html">pcre</a>:{ {</b> <i>rule-1</i> <b>}, {</b> <i>rule-2</i> <b>} .. } ..</b>
+
+ <a href="master.5.html">master.cf</a>:
+ <b>.. -o {</b> <i>parameter</i> <b>= .. <a href="pcre_table.5.html">pcre</a>:{ {</b> <i>rule-1</i> <b>}, {</b> <i>rule-2</i> <b>} .. } .. } ..</b>
+
+ Postfix ignores whitespace after '{' and before '}', and writes each
+ <i>rule</i> as one text line to an in-memory file:
+
+ in-memory file:
+ rule-1
+ rule-2
+ ..
+
+ Postfix parses the result as if it is a file in /etc/postfix.
+
+ Note: if a rule contains <b>$</b>, specify <b>$$</b> to keep Postfix from trying to
+ do <i>$name</i> expansion as it evaluates a parameter value.
+
<b>EXAMPLE SMTPD ACCESS MAP</b>
# Disallow sender-specified routing. This is a must if you relay mail
# for other domains.
Note: address information may be enclosed inside "[]" but
this form is not required.
+.SH "INLINE SPECIFICATION"
+.na
+.nf
+.ad
+.fi
+The contents of a table may be specified in the table name.
+The basic syntax is:
+
+.nf
+main.cf:
+ \fIparameter\fR \fB= .. pcre:{ { \fIrule\-1\fB }, { \fIrule\-2\fB } .. } ..\fR
+
+master.cf:
+ \fB.. \-o { \fIparameter\fR \fB= .. pcre:{ { \fIrule\-1\fB }, { \fIrule\-2\fB } .. } .. } ..\fR
+.fi
+
+Postfix ignores whitespace after '{' and before '}', and
+writes each \fIrule\fR as one text line to an in\-memory
+file:
+
+.nf
+in\-memory file:
+ rule\-1
+ rule\-2
+ ..
+.fi
+
+Postfix parses the result as if it is a file in /etc/postfix.
.SH "EXAMPLE SMTPD ACCESS MAP"
.na
.nf
Note: since negated patterns (those preceded by \fB!\fR) return a
result when the expression does not match, substitutions are not
available for negated patterns.
+.SH "INLINE SPECIFICATION"
+.na
+.nf
+.ad
+.fi
+The contents of a table may be specified in the table name.
+The basic syntax is:
+
+.nf
+main.cf:
+ \fIparameter\fR \fB= .. pcre:{ { \fIrule\-1\fB }, { \fIrule\-2\fB } .. } ..\fR
+
+master.cf:
+ \fB.. \-o { \fIparameter\fR \fB= .. pcre:{ { \fIrule\-1\fB }, { \fIrule\-2\fB } .. } .. } ..\fR
+.fi
+
+Postfix ignores whitespace after '{' and before '}', and
+writes each \fIrule\fR as one text line to an in\-memory
+file:
+
+.nf
+in\-memory file:
+ rule\-1
+ rule\-2
+ ..
+.fi
+
+Postfix parses the result as if it is a file in /etc/postfix.
+
+Note: if a rule contains \fB$\fR, specify \fB$$\fR to keep
+Postfix from trying to do \fI$name\fR expansion as it
+evaluates a parameter value.
.SH "EXAMPLE SMTPD ACCESS MAP"
.na
.nf
Note: since negated patterns (those preceded by \fB!\fR) return a
result when the expression does not match, substitutions are not
available for negated patterns.
+.SH "INLINE SPECIFICATION"
+.na
+.nf
+.ad
+.fi
+The contents of a table may be specified in the table name.
+The basic syntax is:
+
+.nf
+main.cf:
+ \fIparameter\fR \fB= .. pcre:{ { \fIrule\-1\fB }, { \fIrule\-2\fB } .. } ..\fR
+
+master.cf:
+ \fB.. \-o { \fIparameter\fR \fB= .. pcre:{ { \fIrule\-1\fB }, { \fIrule\-2\fB } .. } .. } ..\fR
+.fi
+
+Postfix ignores whitespace after '{' and before '}', and
+writes each \fIrule\fR as one text line to an in\-memory
+file:
+
+.nf
+in\-memory file:
+ rule\-1
+ rule\-2
+ ..
+.fi
+
+Postfix parses the result as if it is a file in /etc/postfix.
+
+Note: if a rule contains \fB$\fR, specify \fB$$\fR to keep
+Postfix from trying to do \fI$name\fR expansion as it
+evaluates a parameter value.
.SH "EXAMPLE SMTPD ACCESS MAP"
.na
.nf
#
# Note: address information may be enclosed inside "[]" but
# this form is not required.
+# INLINE SPECIFICATION
+# .ad
+# .fi
+# The contents of a table may be specified in the table name.
+# The basic syntax is:
+#
+# .nf
+# main.cf:
+# \fIparameter\fR \fB= .. pcre:{ { \fIrule-1\fB }, { \fIrule-2\fB } .. } ..\fR
+#
+# master.cf:
+# \fB.. -o { \fIparameter\fR \fB= .. pcre:{ { \fIrule-1\fB }, { \fIrule-2\fB } .. } .. } ..\fR
+# .fi
+#
+# Postfix ignores whitespace after '{' and before '}', and
+# writes each \fIrule\fR as one text line to an in-memory
+# file:
+#
+# .nf
+# in-memory file:
+# rule-1
+# rule-2
+# ..
+# .fi
+#
+# Postfix parses the result as if it is a file in /etc/postfix.
# EXAMPLE SMTPD ACCESS MAP
# .nf
# /etc/postfix/main.cf:
# Note: since negated patterns (those preceded by \fB!\fR) return a
# result when the expression does not match, substitutions are not
# available for negated patterns.
+# INLINE SPECIFICATION
+# .ad
+# .fi
+# The contents of a table may be specified in the table name.
+# The basic syntax is:
+#
+# .nf
+# main.cf:
+# \fIparameter\fR \fB= .. pcre:{ { \fIrule-1\fB }, { \fIrule-2\fB } .. } ..\fR
+#
+# master.cf:
+# \fB.. -o { \fIparameter\fR \fB= .. pcre:{ { \fIrule-1\fB }, { \fIrule-2\fB } .. } .. } ..\fR
+# .fi
+#
+# Postfix ignores whitespace after '{' and before '}', and
+# writes each \fIrule\fR as one text line to an in-memory
+# file:
+#
+# .nf
+# in-memory file:
+# rule-1
+# rule-2
+# ..
+# .fi
+#
+# Postfix parses the result as if it is a file in /etc/postfix.
+#
+# Note: if a rule contains \fB$\fR, specify \fB$$\fR to keep
+# Postfix from trying to do \fI$name\fR expansion as it
+# evaluates a parameter value.
# EXAMPLE SMTPD ACCESS MAP
# # Protect your outgoing majordomo exploders
# /^(?!owner-)(.*)-outgoing@(.*)/ 550 Use ${1}@${2} instead
# input string against the patterns between \fBif\fR and
# \fBendif\fR. The \fBif\fR..\fBendif\fR can nest.
# .sp
-# Note: do not prepend whitespace to patterns inside
+# Note: do not prepend whitespace to patterns inside
# \fBif\fR..\fBendif\fR.
# .sp
# This feature is available in Postfix 2.1 and later.
# match that input string against the patterns between \fBif\fR
# and \fBendif\fR. The \fBif\fR..\fBendif\fR can nest.
# .sp
-# Note: do not prepend whitespace to patterns inside
+# Note: do not prepend whitespace to patterns inside
# \fBif\fR..\fBendif\fR.
# .sp
# This feature is available in Postfix 2.1 and later.
# Note: since negated patterns (those preceded by \fB!\fR) return a
# result when the expression does not match, substitutions are not
# available for negated patterns.
+# INLINE SPECIFICATION
+# .ad
+# .fi
+# The contents of a table may be specified in the table name.
+# The basic syntax is:
+#
+# .nf
+# main.cf:
+# \fIparameter\fR \fB= .. pcre:{ { \fIrule-1\fB }, { \fIrule-2\fB } .. } ..\fR
+#
+# master.cf:
+# \fB.. -o { \fIparameter\fR \fB= .. pcre:{ { \fIrule-1\fB }, { \fIrule-2\fB } .. } .. } ..\fR
+# .fi
+#
+# Postfix ignores whitespace after '{' and before '}', and
+# writes each \fIrule\fR as one text line to an in-memory
+# file:
+#
+# .nf
+# in-memory file:
+# rule-1
+# rule-2
+# ..
+# .fi
+#
+# Postfix parses the result as if it is a file in /etc/postfix.
+#
+# Note: if a rule contains \fB$\fR, specify \fB$$\fR to keep
+# Postfix from trying to do \fI$name\fR expansion as it
+# evaluates a parameter value.
# EXAMPLE SMTPD ACCESS MAP
# # Disallow sender-specified routing. This is a must if you relay mail
# # for other domains.
extern char *var_smtpd_noop_cmds;
#define VAR_SMTPD_FORBID_CMDS "smtpd_forbidden_commands"
-#define DEF_SMTPD_FORBID_CMDS "CONNECT GET POST"
+#define DEF_SMTPD_FORBID_CMDS "CONNECT GET POST regexp:{{/^[^A-Z]/ Bogus}}"
extern char *var_smtpd_forbid_cmds;
#define VAR_SMTPD_CMD_FILTER "smtpd_command_filter"
* Patches change both the patchlevel and the release date. Snapshots have no
* patchlevel; they change the release date only.
*/
-#define MAIL_RELEASE_DATE "20210529"
+#define MAIL_RELEASE_DATE "20210605"
#define MAIL_VERSION_NUMBER "3.7"
#ifdef SNAPSHOT
../../bin/$(PROG): $(PROG)
cp $(PROG) ../../bin
-tests: test1 test2 fail_test quote_test file_test
+tests: test1 test2 fail_test quote_test file_test lmdb_abb_test lmdb_retry_test
root_tests:
test1: $(PROG) map.in map-abc1.ref map-ghi1.ref map-uABC1.ref
- $(SHLIB_ENV) ./$(PROG) map.in
+ $(SHLIB_ENV) $(VALGRIND) ./$(PROG) map.in
for key in abc ghi; \
do \
- $(SHLIB_ENV) ./$(PROG) -q $${key} map.in | diff map-$${key}1.ref -; \
+ $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -q $${key} map.in | diff map-$${key}1.ref -; \
done
- $(SHLIB_ENV) ./$(PROG) -f map.in
+ $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -f map.in
for key in ABC; \
do \
- $(SHLIB_ENV) ./$(PROG) -fq $${key} map.in | diff map-u$${key}1.ref -; \
+ $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -fq $${key} map.in | diff map-u$${key}1.ref -; \
done
rm -f map.in.db
test2: $(PROG) map.in map-abc2.ref map-ghi2.ref map-uABC2.ref
- $(SHLIB_ENV) ./$(PROG) map.in
+ $(SHLIB_ENV) $(VALGRIND) ./$(PROG) map.in
for key in abc ghi; \
do \
- echo $${key} | $(SHLIB_ENV) ./$(PROG) -q - map.in | diff map-$${key}2.ref -; \
+ echo $${key} | $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -q - map.in | diff map-$${key}2.ref -; \
done
- $(SHLIB_ENV) ./$(PROG) -f map.in
+ $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -f map.in
for key in ABC; \
do \
- echo $${key} | $(SHLIB_ENV) ./$(PROG) -fq - map.in | diff map-u$${key}2.ref -; \
+ echo $${key} | $(SHLIB_ENV) $(VALGRIND) ./$(PROG) -fq - map.in | diff map-u$${key}2.ref -; \
done
rm -f map.in.db
fail_test: $(PROG) aliases fail_test.in fail_test.ref
- -($(SHLIB_ENV) sh fail_test.in || exit 0) 2>&1 | \
- sed -e 's/No error:/Unknown error:/' \
- -e 's/Success/Unknown error: 0/' > fail_test.tmp
+ -($(SHLIB_ENV) sh fail_test.in || exit 0) >fail_test.tmp 2>&1
diff fail_test.ref fail_test.tmp
rm -f fail_test.tmp
diff file_test.ref file_test.tmp
rm -f file_test.tmp file_test_map.* postmap-file-1 postmap-file-2
+lmdb_abb_test: $(PROG) lmdb_abb lmdb_abb.ref
+ rm -f lmdb_abb.lmdb
+ ($(SHLIB_ENV) $(VALGRIND) ./postmap lmdb:lmdb_abb; \
+ $(SHLIB_ENV) $(VALGRIND) ./postmap -s lmdb:lmdb_abb | sort) >lmdb_abb.tmp 2>&1
+ diff lmdb_abb.ref lmdb_abb.tmp
+ rm -f lmdb_abb.tmp lmdb_abb.lmdb
+
+lmdb_retry_test: $(PROG)
+ rm -f lmdb_retry.lmdb main.cf
+ tr A-Z a-z < /usr/share/dict/words| \
+ sed -e 's/.*/& &/' -e 10000q| LANG=C sort -u >lmdb_retry
+ echo lmdb_map_size=1000 >main.cf
+ ($(SHLIB_ENV) $(VALGRIND) ./postmap -c . lmdb:lmdb_retry; \
+ $(SHLIB_ENV) $(VALGRIND) ./postmap -s lmdb:lmdb_retry | \
+ LANG=C sort > lmdb_retry.tmp)
+ cmp lmdb_retry lmdb_retry.tmp
+ rm -f lmdb_retry lmdb_retry.tmp lmdb_retry.lmdb main.cf
+
printfck: $(OBJS) $(PROG)
rm -rf printfck
mkdir printfck
--- /dev/null
+a 1
+b 2
+b 3
--- /dev/null
+postmap: warning: lmdb:lmdb_abb: duplicate entry: "b"
+a 1
+b 2
extpar.c dict_inline.c casefold.c dict_utf8.c strcasecmp_utf8.c \
split_qnameval.c argv_attr_print.c argv_attr_scan.c dict_file.c \
msg_logger.c logwriter.c unix_dgram_connect.c unix_dgram_listen.c \
- byte_mask.c known_tcp_ports.c argv_split_at.c
+ byte_mask.c known_tcp_ports.c argv_split_at.c dict_stream.c
OBJS = alldig.o allprint.o argv.o argv_split.o attr_clnt.o attr_print0.o \
attr_print64.o attr_print_plain.o attr_scan0.o attr_scan64.o \
attr_scan_plain.o auto_clnt.o base64_code.o basename.o binhash.o \
extpar.o dict_inline.o casefold.o dict_utf8.o strcasecmp_utf8.o \
split_qnameval.o argv_attr_print.o argv_attr_scan.o dict_file.o \
msg_logger.o logwriter.o unix_dgram_connect.o unix_dgram_listen.o \
- byte_mask.o known_tcp_ports.o argv_split_at.o
+ byte_mask.o known_tcp_ports.o argv_split_at.o dict_stream.o
# MAP_OBJ is for maps that may be dynamically loaded with dynamicmaps.cf.
# When hard-linking these, makedefs sets NON_PLUGIN_MAP_OBJ=$(MAP_OBJ),
# otherwise it sets the PLUGIN_* macros.
valid_utf8_string ip_match base32_code msg_rate_delay netstring \
vstream timecmp dict_cache midna_domain casefold strcasecmp_utf8 \
vbuf_print split_qnameval vstream msg_logger byte_mask \
- known_tcp_ports
+ known_tcp_ports dict_stream
PLUGIN_MAP_SO = $(LIB_PREFIX)pcre$(LIB_SUFFIX)
LIB_DIR = ../../lib
$(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS)
mv junk $@.o
+dict_stream: $(LIB)
+ mv $@.o junk
+ $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS)
+ mv junk $@.o
+
tests: all valid_hostname_test mac_expand_test dict_test unescape_test \
hex_quote_test ctable_test inet_addr_list_test base64_code_test \
attr_scan64_test attr_scan0_test dict_pcre_test host_port_test \
vstring_test vstream_test dict_pcre_file_test dict_regexp_file_test \
dict_cidr_file_test dict_static_file_test dict_random_test \
dict_random_file_test dict_inline_file_test byte_mask_tests \
- mystrtok_test known_tcp_ports_test
+ mystrtok_test known_tcp_ports_test dict_stream_test \
+ dict_inline_pcre_test dict_inline_regexp_test dict_inline_cidr_test
root_tests:
diff known_tcp_ports.ref known_tcp_ports.tmp
rm -f known_tcp_ports.tmp
+dict_stream_test: dict_stream dict_stream.ref
+ $(SHLIB_ENV) ${VALGRIND} ./dict_stream >dict_stream.tmp 2>&1
+ diff dict_stream.ref dict_stream.tmp
+ rm -f dict_stream.tmp
+
+dict_inline_pcre_test: dict_open dict_inline_pcre.ref
+ (echo get foo; echo get bar) | \
+ $(SHLIB_ENV) ${VALGRIND} ./dict_open 'pcre:{{/foo/ got foo}}' \
+ read 2>&1 | grep -v uid= >dict_inline_pcre.tmp
+ diff dict_inline_pcre.ref dict_inline_pcre.tmp
+ rm -f dict_inline_pcre.tmp
+
+dict_inline_regexp_test: dict_open dict_inline_regexp.ref
+ (echo get foo; echo get bar) | \
+ $(SHLIB_ENV) ${VALGRIND} ./dict_open 'regexp:{{/foo/ got foo}}' \
+ read 2>&1 | grep -v uid= >dict_inline_regexp.tmp
+ diff dict_inline_regexp.ref dict_inline_regexp.tmp
+ rm -f dict_inline_regexp.tmp
+
+dict_inline_cidr_test: dict_open dict_inline_cidr.ref
+ (echo get 1.2.3.4; echo get 4.3.2.1) | \
+ $(SHLIB_ENV) ${VALGRIND} ./dict_open \
+ 'cidr:{{1.2.3.4 got 1.2.3.4}}' \
+ read 2>&1 | grep -v uid= >dict_inline_cidr.tmp
+ diff dict_inline_cidr.ref dict_inline_cidr.tmp
+ rm -f dict_inline_cidr.tmp
+
depend: $(MAKES)
(sed '1,/^# do not edit/!d' Makefile.in; \
set -e; for i in [a-z][a-z0-9]*.c; do \
dict_static.o: vbuf.h
dict_static.o: vstream.h
dict_static.o: vstring.h
+dict_stream.o: argv.h
+dict_stream.o: check_arg.h
+dict_stream.o: dict.h
+dict_stream.o: dict_stream.c
+dict_stream.o: msg.h
+dict_stream.o: myflock.h
+dict_stream.o: mymalloc.h
+dict_stream.o: stringops.h
+dict_stream.o: sys_defs.h
+dict_stream.o: vbuf.h
+dict_stream.o: vstream.h
+dict_stream.o: vstring.h
dict_surrogate.o: argv.h
dict_surrogate.o: check_arg.h
dict_surrogate.o: compat_va_copy.h
exec_command.o: sys_defs.h
extpar.o: check_arg.h
extpar.o: extpar.c
-extpar.o: msg.h
extpar.o: stringops.h
extpar.o: sys_defs.h
extpar.o: vbuf.h
/*
* System library.
*/
+#include <sys/stat.h>
#include <fcntl.h>
#include <setjmp.h>
extern void dict_file_purge_buffers(DICT *);
extern const char *dict_file_lookup(DICT *dict, const char *);
+ /*
+ * dict_stream(3)
+ */
+extern VSTREAM *dict_stream_open(const char *dict_type, const char *mapname,
+ int open_flags, int dict_flags, struct stat * st, VSTRING **why);
+
/* LICENSE
/* .ad
/* .fi
/*
* Open the configuration file.
*/
- if ((map_fp = vstream_fopen(mapname, O_RDONLY, 0)) == 0)
+ if ((map_fp = dict_stream_open(DICT_TYPE_CIDR, mapname, O_RDONLY,
+ dict_flags, &st, &why)) == 0)
DICT_CIDR_OPEN_RETURN(dict_surrogate(DICT_TYPE_CIDR, mapname,
open_flags, dict_flags,
- "open %s: %m", mapname));
- if (fstat(vstream_fileno(map_fp), &st) < 0)
- msg_fatal("fstat %s: %m", mapname);
-
+ "%s", vstring_str(why)));
line_buffer = vstring_alloc(100);
why = vstring_alloc(100);
/* System library. */
#include <sys_defs.h>
+#include <errno.h>
/* Utility library. */
{
DICT_FAIL *dp = (DICT_FAIL *) dict;
+ errno = 0;
DICT_ERR_VAL_RETURN(dict, dp->dict_errno, DICT_STAT_ERROR);
}
{
DICT_FAIL *dp = (DICT_FAIL *) dict;
+ errno = 0;
DICT_ERR_VAL_RETURN(dict, dp->dict_errno, DICT_STAT_ERROR);
}
{
DICT_FAIL *dp = (DICT_FAIL *) dict;
+ errno = 0;
DICT_ERR_VAL_RETURN(dict, dp->dict_errno, (char *) 0);
}
{
DICT_FAIL *dp = (DICT_FAIL *) dict;
+ errno = 0;
DICT_ERR_VAL_RETURN(dict, dp->dict_errno, DICT_STAT_ERROR);
}
--- /dev/null
+> get 1.2.3.4
+1.2.3.4=got 1.2.3.4
+> get 4.3.2.1
+4.3.2.1: not found
--- /dev/null
+> get foo
+foo=got foo
+> get bar
+bar: not found
--- /dev/null
+> get foo
+foo=got foo
+> get bar
+bar: not found
/* DIAGNOSTICS
/* Fatal errors: cannot open file, file write error, out of
/* memory.
+/*
+/* If a jump buffer is specified with dict_setjmp(), then the LMDB
+/* client will call dict_longjmp() to return to that execution
+/* context after a recoverable error.
/* BUGS
/* The on-the-fly map resize operations require no concurrent
/* activity in the same database by other threads in the same
DICT_PCRE *dict_pcre;
VSTREAM *map_fp = 0;
struct stat st;
+ VSTRING *why = 0;
VSTRING *line_buffer = 0;
DICT_PCRE_RULE *last_rule = 0;
DICT_PCRE_RULE *rule;
vstream_fclose(map_fp); \
if (line_buffer != 0) \
vstring_free(line_buffer); \
+ if (why != 0) \
+ vstring_free(why); \
return (__d); \
} while (0)
/*
* Open the configuration file.
*/
- if ((map_fp = vstream_fopen(mapname, O_RDONLY, 0)) == 0)
+ if ((map_fp = dict_stream_open(DICT_TYPE_PCRE, mapname, O_RDONLY,
+ dict_flags, &st, &why)) == 0)
DICT_PCRE_OPEN_RETURN(dict_surrogate(DICT_TYPE_PCRE, mapname,
open_flags, dict_flags,
- "open %s: %m", mapname));
- if (fstat(vstream_fileno(map_fp), &st) < 0)
- msg_fatal("fstat %s: %m", mapname);
-
+ "%s", vstring_str(why)));
line_buffer = vstring_alloc(100);
dict_pcre = (DICT_PCRE *) dict_alloc(DICT_TYPE_PCRE, mapname,
DICT_REGEXP *dict_regexp;
VSTREAM *map_fp = 0;
struct stat st;
+ VSTRING *why = 0;
VSTRING *line_buffer = 0;
DICT_REGEXP_RULE *rule;
DICT_REGEXP_RULE *last_rule = 0;
vstring_free(line_buffer); \
if (map_fp != 0) \
vstream_fclose(map_fp); \
+ if (why != 0) \
+ vstring_free(why); \
return (__d); \
} while (0)
/*
* Open the configuration file.
*/
- if ((map_fp = vstream_fopen(mapname, O_RDONLY, 0)) == 0)
+ if ((map_fp = dict_stream_open(DICT_TYPE_REGEXP, mapname, O_RDONLY,
+ dict_flags, &st, &why)) == 0)
DICT_REGEXP_OPEN_RETURN(dict_surrogate(DICT_TYPE_REGEXP, mapname,
open_flags, dict_flags,
- "open %s: %m", mapname));
- if (fstat(vstream_fileno(map_fp), &st) < 0)
- msg_fatal("fstat %s: %m", mapname);
-
+ "%s", vstring_str(why)));
line_buffer = vstring_alloc(100);
dict_regexp = (DICT_REGEXP *) dict_alloc(DICT_TYPE_REGEXP, mapname,
--- /dev/null
+/*++
+/* NAME
+/* dict_stream 3
+/* SUMMARY
+/*
+/* SYNOPSIS
+/* #include <dict.h>
+/*
+/* VSTREAM *dict_stream_open(
+/* const char *dict_type,
+/* const char *mapname,
+/* int open_flags,
+/* int dict_flags,
+/* struct stat * st,
+/* VSTRING **why)
+/* DESCRIPTION
+/* dict_stream_open() opens a dictionary, which can be specified
+/* as a file name, or as inline text enclosed with {}. If successful,
+/* dict_stream_open() returns a non-null VSTREAM pointer. Otherwise,
+/* it returns an error text through the why argument, allocating
+/* storage for the error text if the why argument points to a
+/* null pointer.
+/*
+/* When the dictionary file is specified inline, dict_stream_open()
+/* removes the outer {} from the mapname value, and removes leading
+/* or trailing comma or whitespace from the result. It then expects
+/* to find zero or more rules enclosed in {}, separated by comma
+/* and/or whitespace. dict_stream() writes each rule as one text
+/* line to an in-memory stream, without its enclosing {} and without
+/* leading or trailing whitespace. The result value is a VSTREAM
+/* pointer for the in-memory stream that can be read as a regular
+/* file.
+/* .sp
+/* inline-file = "{" 0*(0*wsp-comma rule-spec) 0*wsp-comma "}"
+/* .sp
+/* rule-spec = "{" 0*wsp rule-text 0*wsp "}"
+/* .sp
+/* rule-text = any text containing zero or more balanced {}
+/* .sp
+/* wsp-comma = wsp | ","
+/* .sp
+/* wsp = whitespace
+/*
+/* Arguments:
+/* .IP dict_type
+/* .IP open_flags
+/* .IP dict_flags
+/* The same as with dict_open(3).
+/* .IP mapname
+/* Pathname of a file with dictionary content, or inline dictionary
+/* content as specified above.
+/* .IP st
+/* File metadata with the file owner, or fake metadata with the
+/* real UID and GID of the dict_stream_open() caller. This is
+/* used for "taint" tracking (zero=trusted, non-zero=untrusted).
+/* IP why
+/* Pointer to pointer to error message storage. dict_stream_open()
+/* updates this storage when reporting an error, and allocates
+/* memory if why points to a null pointer.
+/* LICENSE
+/* .ad
+/* .fi
+/* The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/* Wietse Venema
+/* Google, Inc.
+/* 111 8th Avenue
+/* New York, NY 10011, USA
+/*--*/
+
+ /*
+ * System library.
+ */
+#include <sys_defs.h>
+
+ /*
+ * Utility library.
+ */
+#include <dict.h>
+#include <msg.h>
+#include <mymalloc.h>
+#include <stringops.h>
+#include <vstring.h>
+
+#define STR(x) vstring_str(x)
+#define LEN(x) VSTRING_LEN(x)
+
+/* dict_inline_to_multiline - convert inline map spec to multiline text */
+
+static char *dict_inline_to_multiline(VSTRING *vp, const char *mapname)
+{
+ char *saved_name = mystrdup(mapname);
+ char *bp = saved_name;
+ char *cp;
+ char *err = 0;
+
+ VSTRING_RESET(vp);
+ /* Strip the {} from the map "name". */
+ err = extpar(&bp, CHARS_BRACE, EXTPAR_FLAG_NONE);
+ /* Extract zero or more rules inside {}. */
+ while (err == 0 && (cp = mystrtokq(&bp, CHARS_COMMA_SP, CHARS_BRACE)) != 0)
+ if ((err = extpar(&cp, CHARS_BRACE, EXTPAR_FLAG_STRIP)) == 0)
+ /* Write rule to in-memory file. */
+ vstring_sprintf_append(vp, "%s\n", cp);
+ VSTRING_TERMINATE(vp);
+ myfree(saved_name);
+ return (err);
+}
+
+/* dict_stream_open - open inline configuration or configuration file */
+
+VSTREAM *dict_stream_open(const char *dict_type, const char *mapname,
+ int open_flags, int dict_flags,
+ struct stat * st, VSTRING **why)
+{
+ VSTRING *inline_buf = 0;
+ VSTREAM *map_fp;
+ char *err = 0;
+
+#define RETURN_0_WITH_REASON(...) do { \
+ if (*why == 0) \
+ *why = vstring_alloc(100); \
+ vstring_sprintf(*why, __VA_ARGS__); \
+ if (inline_buf != 0) \
+ vstring_free(inline_buf); \
+ if (err != 0) \
+ myfree(err); \
+ return (0); \
+ } while (0)
+
+ if (mapname[0] == CHARS_BRACE[0]) {
+ inline_buf = vstring_alloc(100);
+ if ((err = dict_inline_to_multiline(inline_buf, mapname)) != 0)
+ RETURN_0_WITH_REASON("%s map: %s", dict_type, err);
+ map_fp = vstream_memopen(inline_buf, O_RDONLY);
+ vstream_control(map_fp, VSTREAM_CTL_OWN_VSTRING, VSTREAM_CTL_END);
+ st->st_uid = getuid(); /* geteuid()? */
+ st->st_gid = getgid(); /* getegid()? */
+ return (map_fp);
+ } else {
+ if ((map_fp = vstream_fopen(mapname, open_flags, 0)) == 0)
+ RETURN_0_WITH_REASON("open %s: %m", mapname);
+ if (fstat(vstream_fileno(map_fp), st) < 0)
+ msg_fatal("fstat %s: %m", mapname);
+ return (map_fp);
+ }
+}
+
+#ifdef TEST
+
+#include <string.h>
+
+int main(int argc, char **argv)
+{
+ struct testcase {
+ const char *title;
+ const char *mapname; /* starts with brace */
+ const char *expect_err; /* null or message */
+ const char *expect_cont; /* null or content */
+ };
+
+#define EXP_NOERR 0
+#define EXP_NOCONT 0
+
+#define STRING_OR(s, text_if_null) ((s) ? (s) : (text_if_null))
+#define DICT_TYPE_TEST "test"
+
+ const char rule_spec_error[] = DICT_TYPE_TEST " map: "
+ "syntax error after '}' in \"{blah blah}x\"";
+ const char inline_config_error[] = DICT_TYPE_TEST " map: "
+ "syntax error after '}' in \"{{foo bar}, {blah blah}}x\"";
+ struct testcase testcases[] = {
+ {"normal",
+ "{{foo bar}, {blah blah}}", EXP_NOERR, "foo bar\nblah blah\n"
+ },
+ {"trims leading/trailing wsp around rule-text",
+ "{{ foo bar }, { blah blah }}", EXP_NOERR, "foo bar\nblah blah\n"
+ },
+ {"trims leading/trailing comma-wsp around rule-spec",
+ "{, ,{foo bar}, {blah blah}, ,}", EXP_NOERR, "foo bar\nblah blah\n"
+ },
+ {"empty inline-file",
+ "{, }", EXP_NOERR, ""
+ },
+ {"propagates extpar error for inline-file",
+ "{{foo bar}, {blah blah}}x", inline_config_error, EXP_NOCONT
+ },
+ {"propagates extpar error for rule-spec",
+ "{{foo bar}, {blah blah}x}", rule_spec_error, EXP_NOCONT
+ },
+ 0,
+ };
+ struct testcase *tp;
+ VSTRING *act_err = 0;
+ VSTRING *act_cont = vstring_alloc(100);
+ VSTREAM *fp;
+ struct stat st;
+ ssize_t exp_len;
+ ssize_t act_len;
+ int pass;
+ int fail;
+
+ for (pass = fail = 0, tp = testcases; tp->title; tp++) {
+ int test_passed = 0;
+
+ msg_info("RUN test case %ld %s", (long) (tp - testcases), tp->title);
+
+#if 0
+ msg_info("title=%s", tp->title);
+ msg_info("mapname=%s", tp->mapname);
+ msg_info("expect_err=%s", STRING_OR_NULL(tp->expect_err));
+ msg_info("expect_cont=%s", STRINGOR_NULL(tp->expect_cont));
+#endif
+
+ if (act_err)
+ VSTRING_RESET(act_err);
+ fp = dict_stream_open(DICT_TYPE_TEST, tp->mapname, O_RDONLY,
+ 0, &st, &act_err);
+ if (fp) {
+ if (tp->expect_err) {
+ msg_warn("test case %s: got stream, expected error", tp->title);
+ } else if (!tp->expect_err && act_err && LEN(act_err) > 0) {
+ msg_warn("test case %s: got error '%s', expected noerror",
+ tp->title, STR(act_err));
+ } else if (!tp->expect_cont) {
+ msg_warn("test case %s: got stream, expected nostream",
+ tp->title);
+ } else {
+ exp_len = strlen(tp->expect_cont);
+ if ((act_len = vstream_fread_buf(fp, act_cont, 2 * exp_len)) < 0) {
+ msg_warn("test case %s: content read error", tp->title);
+ } else {
+ VSTRING_TERMINATE(act_cont);
+ if (strcmp(tp->expect_cont, STR(act_cont)) != 0) {
+ msg_warn("test case %s: got content '%s', expected '%s'",
+ tp->title, STR(act_cont), tp->expect_cont);
+ } else {
+ test_passed = 1;
+ }
+ }
+ }
+ } else {
+ if (!tp->expect_err) {
+ msg_warn("test case %s: got nostream, expected noerror",
+ tp->title);
+ } else if (tp->expect_cont) {
+ msg_warn("test case %s: got nostream, expected stream",
+ tp->title);
+ } else if (strcmp(STR(act_err), tp->expect_err) != 0) {
+ msg_warn("test case %s: got error '%s', expected '%s'",
+ tp->title, STR(act_err), tp->expect_err);
+ } else {
+ test_passed = 1;
+ }
+
+ }
+ if (test_passed) {
+ msg_info("PASS test %ld", (long) (tp - testcases));
+ pass++;
+ } else {
+ msg_info("FAIL test %ld", (long) (tp - testcases));
+ fail++;
+ }
+ if (fp)
+ vstream_fclose(fp);
+ }
+ if (act_err)
+ vstring_free(act_err);
+ vstring_free(act_cont);
+ msg_info("PASS=%d FAIL=%d", pass, fail);
+ return (fail > 0);
+}
+
+#endif /* TEST */
--- /dev/null
+unknown: RUN test case 0 normal
+unknown: PASS test 0
+unknown: RUN test case 1 trims leading/trailing wsp around rule-text
+unknown: PASS test 1
+unknown: RUN test case 2 trims leading/trailing comma-wsp around rule-spec
+unknown: PASS test 2
+unknown: RUN test case 3 empty inline-file
+unknown: PASS test 3
+unknown: RUN test case 4 propagates extpar error for inline-file
+unknown: PASS test 4
+unknown: RUN test case 5 propagates extpar error for rule-spec
+unknown: PASS test 5
+unknown: PASS=6 FAIL=0
/* whitespace before the closing parenthesis.
/* .RE
/* DIAGNOSTICS
-/* panic: the input string does not start with the opening
-/* parenthesis.
-/*
/* In case of error the result value is a dynamically-allocated
/* string with a description of the problem that includes a
/* copy of the offending input. A non-null result value should
/* be destroyed with myfree(). The following describes the errors
/* and the state of the buffer and buffer pointer.
+/* .IP "no opening parenthesis at start of text"
+/* The buffer pointer points to the input text.
/* .IP "missing closing parenthesis"
/* The buffer pointer points to text as if a closing parenthesis
/* were present at the end of the input.
/* IBM T.J. Watson Research
/* P.O. Box 704
/* Yorktown Heights, NY 10598, USA
+/*
+/* Wietse Venema
+/* Google, Inc.
+/* 111 8th Avenue
+/* New York, NY 10011, USA
/*--*/
/*
/*
* Utility library.
*/
-#include <msg.h>
+#include <vstring.h>
#include <stringops.h>
/* extpar - extract text from parentheses */
char *err = 0;
size_t len;
- if (cp[0] != parens[0])
- msg_panic("extpar: no '%c' at start of text: \"%s\"", parens[0], cp);
- if ((len = balpar(cp, parens)) == 0) {
+ if (cp[0] != parens[0]) {
+ err = vstring_export(vstring_sprintf(vstring_alloc(100),
+ "no '%c' at start of text in \"%s\"", parens[0], cp));
+ len = 0;
+ } else if ((len = balpar(cp, parens)) == 0) {
err = concatenate("missing '", parens + 1, "' in \"",
cp, "\"", (char *) 0);
cp += 1;
* - With a bulk-mode transaction we commit when the database is closed.
*/
if (slmdb->open_flags & O_TRUNC) {
- if ((status = mdb_drop(slmdb->txn, slmdb->dbi, 0)) != 0)
+ if ((status = mdb_drop(slmdb->txn, slmdb->dbi, 0)) != 0) {
+ mdb_txn_abort(slmdb->txn);
+ slmdb->txn = 0;
return (status);
+ }
if ((slmdb->slmdb_flags & SLMDB_FLAG_BULK) == 0) {
- if ((status = mdb_txn_commit(slmdb->txn)) != 0)
- return (status);
+ status = mdb_txn_commit(slmdb->txn);
slmdb->txn = 0;
+ if (status != 0)
+ return (status);
}
- } else if ((slmdb->lmdb_flags & MDB_RDONLY) != 0
- || (slmdb->slmdb_flags & SLMDB_FLAG_BULK) == 0) {
+ } else if ((slmdb->slmdb_flags & SLMDB_FLAG_BULK) == 0) {
mdb_txn_abort(slmdb->txn);
slmdb->txn = 0;
}
static int slmdb_recover(SLMDB *slmdb, int status)
{
MDB_envinfo info;
+ int recover_bulk_transaction = 0;
/*
* This may be needed in non-MDB_NOLOCK mode. Recovery is rare enough
/*
* Recover bulk transactions only if they can be restarted. Limit the
* number of recovery attempts per slmdb(3) API request.
+ *
+ * slmdb->txn must be either null (non-bulk transaction error), or an
+ * aborted bulk-mode transaction.
*/
if ((slmdb->txn != 0 && slmdb->longjmp_fn == 0)
|| ((slmdb->api_retry_count += 1) >= slmdb->api_retry_limit))
return (status);
+ /*
+ * Limit the number of recovery attempts per bulk transaction failure.
+ */
+ if (slmdb->txn != 0 && slmdb->longjmp_fn != 0) {
+ if ((slmdb->bulk_retry_count += 1) > slmdb->bulk_retry_limit)
+ return (status);
+ recover_bulk_transaction = 1;
+ }
+
/*
* If we can recover from the error, we clear the error condition and the
* caller should retry the failed operation immediately. Otherwise, the
* caller should terminate with a fatal run-time error and the program
* should be re-run later.
- *
- * slmdb->txn must be either null (non-bulk transaction error), or an
- * aborted bulk-mode transaction.
*/
switch (status) {
* upgrade the lock to "exclusive", because the failed write transaction
* has no side effects.
*/
- if (slmdb->txn != 0 && status == 0 && slmdb->longjmp_fn != 0
- && (slmdb->bulk_retry_count += 1) <= slmdb->bulk_retry_limit) {
- if ((status = mdb_txn_begin(slmdb->env, (MDB_txn *) 0,
- slmdb->lmdb_flags & MDB_RDONLY,
- &slmdb->txn)) == 0
- && (status = slmdb_prepare(slmdb)) == 0)
- slmdb->longjmp_fn(slmdb->cb_context, 1);
- }
+ if (status == 0 && recover_bulk_transaction != 0
+ && (status = mdb_txn_begin(slmdb->env, (MDB_txn *) 0,
+ slmdb->lmdb_flags & MDB_RDONLY,
+ &slmdb->txn)) == 0
+ && (status = slmdb_prepare(slmdb)) == 0)
+ slmdb->longjmp_fn(slmdb->cb_context, 1);
return (status);
}
status = slmdb_put(slmdb, mdb_key, mdb_value, flags);
SLMDB_API_RETURN(slmdb, status);
} else {
- /* Key exists, abort non-bulk transaction only. */
+ /* Abort non-bulk transaction only. */
if (slmdb->txn == 0)
mdb_txn_abort(txn);
}
* Do the update.
*/
if ((status = mdb_del(txn, slmdb->dbi, mdb_key, (MDB_val *) 0)) != 0) {
- mdb_txn_abort(txn);
if (status != MDB_NOTFOUND) {
+ mdb_txn_abort(txn);
if ((status = slmdb_recover(slmdb, status)) == 0)
status = slmdb_del(slmdb, mdb_key);
SLMDB_API_RETURN(slmdb, status);
+ } else {
+ /* Abort non-bulk transaction only. */
+ if (slmdb->txn == 0)
+ mdb_txn_abort(txn);
}
}
/* Revert VSTREAM_CTL_TIMEOUT behavior to the default, i.e.
/* a time limit for individual file descriptor read or write
/* operations.
+/* .IP CA_VSTREAM_CTL_OWN_VSTRING (no arguments)
+/* Transfer ownership of the VSTRING that was opened with
+/* vstream_memopen() etc. to the stream, so that the VSTRING
+/* is automatically destroyed when the stream is closed.
/* .PP
/* vstream_fileno() gives access to the file handle associated with
/* a buffered stream. With streams that have separate read/write
/* The double-buffering feature is activated.
/* .IP VSTREAM_FLAG_MEMORY
/* The stream is connected to a VSTRING buffer.
+/* .IP VSTREAM_FLAG_OWN_VSTRING
+/* The stream 'owns' the VSTRING buffer, and is responsible
+/* for cleaning up when the stream is closed.
/* DIAGNOSTICS
/* Panics: interface violations. Fatal errors: out of memory.
/* SEE ALSO
myfree(stream->path);
if (stream->jbuf)
myfree((void *) stream->jbuf);
+ if (stream->vstring && (stream->buf.flags & VSTREAM_FLAG_OWN_VSTRING))
+ vstring_free(stream->vstring);
if (!VSTREAM_STATIC(stream))
myfree((void *) stream);
return (err ? VSTREAM_EOF : 0);
*/
int memory_ops =
((1 << VSTREAM_CTL_END) | (1 << VSTREAM_CTL_CONTEXT)
- | (1 << VSTREAM_CTL_PATH) | (1 << VSTREAM_CTL_EXCEPT));
+ | (1 << VSTREAM_CTL_PATH) | (1 << VSTREAM_CTL_EXCEPT)
+ | (1 << VSTREAM_CTL_OWN_VSTRING));
for (va_start(ap, name); name != VSTREAM_CTL_END; name = va_arg(ap, int)) {
if ((stream->buf.flags & VSTREAM_FLAG_MEMORY)
stream->time_limit.tv_sec = stream->timeout;
stream->time_limit.tv_usec = 0;
break;
+ case VSTREAM_CTL_OWN_VSTRING:
+ if ((stream->buf.flags |= VSTREAM_FLAG_MEMORY) == 0)
+ msg_panic("%s: operation on non-VSTRING stream", myname);
+ stream->buf.flags |= VSTREAM_FLAG_OWN_VSTRING;
+ break;
default:
msg_panic("%s: bad name %d", myname, name);
}
#define VSTREAM_FLAG_DOUBLE (1<<12) /* double buffer */
#define VSTREAM_FLAG_DEADLINE (1<<13) /* deadline active */
#define VSTREAM_FLAG_MEMORY (1<<14) /* internal stream */
+#define VSTREAM_FLAG_OWN_VSTRING (1<<15)/* owns VSTRING resource */
#define VSTREAM_PURGE_READ (1<<0) /* flush unread data */
#define VSTREAM_PURGE_WRITE (1<<1) /* flush unwritten data */
#define VSTREAM_CTL_SWAP_FD 13
#define VSTREAM_CTL_START_DEADLINE 14
#define VSTREAM_CTL_STOP_DEADLINE 15
+#define VSTREAM_CTL_OWN_VSTRING 16
/* Safer API: type-checked arguments, external use. */
#define CA_VSTREAM_CTL_END VSTREAM_CTL_END
#define CA_VSTREAM_CTL_SWAP_FD(v) VSTREAM_CTL_SWAP_FD, CHECK_PTR(VSTREAM_CTL, VSTREAM, (v))
#define CA_VSTREAM_CTL_START_DEADLINE VSTREAM_CTL_START_DEADLINE
#define CA_VSTREAM_CTL_STOP_DEADLINE VSTREAM_CTL_STOP_DEADLINE
+#define CA_VSTREAM_CTL_OWN_VSTRING VSTREAM_CTL_OWN_VSTRING
CHECK_VAL_HELPER_DCL(VSTREAM_CTL, ssize_t);
CHECK_VAL_HELPER_DCL(VSTREAM_CTL, int);