{"thread":{"id":"37281","subject":"[PATCH] convert: Stream from fd to required clean filter instead of mmap","startedAt":"2014-08-03T08:56:16Z","lastAt":"2014-08-06T04:32:06Z","messageCount":4,"participants":["Steffen Prohaska","Junio C Hamano","prohaska@zib.de"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"247192","messageId":"1407056176-8231-1-git-send-email-prohaska@zib.de","threadId":"37281","inReplyTo":null,"subject":"[PATCH] convert: Stream from fd to required clean filter instead of mmap","fromName":"Steffen Prohaska","fromEmail":"prohaska@zib.de","sentAt":"2014-08-03T08:56:16Z","receivedAt":"2014-08-03T08:56:16Z","isPatch":true,"sender":{"key":"prohaska@zib.de","avatar":"https://avatars.githubusercontent.com/u/217580?v=4"},"body":"The data is streamed to the filter process anyway.  Better avoid mapping\nthe file if possible.  This is especially useful if a clean filter\nreduces the size, for example if it computes a sha1 for binary data,\nlike git media.  The file size that the previous implementation could\nhandle was limited by the available address space; large files for\nexample could not be handled with (32-bit) msysgit.  The new\nimplementation can filter files of any size as long as the filter output\nis small enough.\n\nThe new code path is only taken if the filter is required.  The filter\nconsumes data directly from the fd.  The original data is not available\nto git, so it must fail if the filter fails.\n\nThe test that exercises required filters is modified to verify that the\ndata actually has been modified on its way from the file system to the\nobject store.\n\nThe expectation on the process size is tested using /usr/bin/time.  An\nalternative would have been tcsh, which could be used to print memory\ninformation as follows:\n\n    tcsh -c 'set time=(0 \"%M\"); <cmd>'\n\nAlthough the logic could perhaps be simplified with tcsh, I chose to use\n'time' to avoid a dependency on tcsh.\n\nSigned-off-by: Steffen Prohaska <prohaska@zib.de>\n---\n convert.c             | 58 ++++++++++++++++++++++++++++++++++++++-----\n convert.h             | 10 +++++---\n sha1_file.c           | 29 ++++++++++++++++++++--\n t/t0021-conversion.sh | 68 +++++++++++++++++++++++++++++++++++++++++++++++----\n 4 files changed, 149 insertions(+), 16 deletions(-)\n\ndiff --git a/convert.c b/convert.c\nindex cb5fbb4..58a516a 100644\n--- a/convert.c\n+++ b/convert.c\n@@ -312,11 +312,12 @@ static int crlf_to_worktree(const char *path, const char *src, size_t len,\n struct filter_params {\n \tconst char *src;\n \tunsigned long size;\n+\tint fd;\n \tconst char *cmd;\n \tconst char *path;\n };\n \n-static int filter_buffer(int in, int out, void *data)\n+static int filter_buffer_or_fd(int in, int out, void *data)\n {\n \t/*\n \t * Spawn cmd and feed the buffer contents through its stdin.\n@@ -325,6 +326,7 @@ static int filter_buffer(int in, int out, void *data)\n \tstruct filter_params *params = (struct filter_params *)data;\n \tint write_err, status;\n \tconst char *argv[] = { NULL, NULL };\n+\tint fd;\n \n \t/* apply % substitution to cmd */\n \tstruct strbuf cmd = STRBUF_INIT;\n@@ -355,7 +357,17 @@ static int filter_buffer(int in, int out, void *data)\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n \n-\twrite_err = (write_in_full(child_process.in, params->src, params->size) < 0);\n+\tif (params->src) {\n+\t    write_err = (write_in_full(child_process.in, params->src, params->size) < 0);\n+\t} else {\n+\t    /* dup(), because copy_fd() closes the input fd. */\n+\t    fd = dup(params->fd);\n+\t    if (fd < 0)\n+\t\twrite_err = error(\"failed to dup file descriptor.\");\n+\t    else\n+\t\twrite_err = copy_fd(fd, child_process.in);\n+\t}\n+\n \tif (close(child_process.in))\n \t\twrite_err = 1;\n \tif (write_err)\n@@ -371,7 +383,7 @@ static int filter_buffer(int in, int out, void *data)\n \treturn (write_err || status);\n }\n \n-static int apply_filter(const char *path, const char *src, size_t len,\n+static int apply_filter(const char *path, const char *src, size_t len, int fd,\n                         struct strbuf *dst, const char *cmd)\n {\n \t/*\n@@ -392,11 +404,12 @@ static int apply_filter(const char *path, const char *src, size_t len,\n \t\treturn 1;\n \n \tmemset(&async, 0, sizeof(async));\n-\tasync.proc = filter_buffer;\n+\tasync.proc = filter_buffer_or_fd;\n \tasync.data = &params;\n \tasync.out = -1;\n \tparams.src = src;\n \tparams.size = len;\n+\tparams.fd = fd;\n \tparams.cmd = cmd;\n \tparams.path = path;\n \n@@ -747,6 +760,22 @@ static void convert_attrs(struct conv_attrs *ca, const char *path)\n \t}\n }\n \n+int would_convert_to_git_filter_fd(const char *path) {\n+\tstruct conv_attrs ca;\n+\tconvert_attrs(&ca, path);\n+\n+\tif (!ca.drv)\n+\t    return 0;\n+\n+\t/* Apply a filter to an fd only if the filter is required to succeed.\n+\t * We must die if the filter fails, because the original data before\n+\t * filtering is not available. */\n+\tif (!ca.drv->required)\n+\t    return 0;\n+\n+\treturn apply_filter(path, 0, 0, -1, 0, ca.drv->clean);\n+}\n+\n int convert_to_git(const char *path, const char *src, size_t len,\n                    struct strbuf *dst, enum safe_crlf checksafe)\n {\n@@ -761,7 +790,7 @@ int convert_to_git(const char *path, const char *src, size_t len,\n \t\trequired = ca.drv->required;\n \t}\n \n-\tret |= apply_filter(path, src, len, dst, filter);\n+\tret |= apply_filter(path, src, len, -1, dst, filter);\n \tif (!ret && required)\n \t\tdie(\"%s: clean filter '%s' failed\", path, ca.drv->name);\n \n@@ -778,6 +807,23 @@ int convert_to_git(const char *path, const char *src, size_t len,\n \treturn ret | ident_to_git(path, src, len, dst, ca.ident);\n }\n \n+void convert_to_git_filter_fd(const char *path, int fd, struct strbuf *dst,\n+\t\t\t      enum safe_crlf checksafe)\n+{\n+\tstruct conv_attrs ca;\n+\tconvert_attrs(&ca, path);\n+\n+\tassert(ca.drv);\n+\tassert(ca.drv->clean);\n+\n+\tif (!apply_filter(path, 0, 0, fd, dst, ca.drv->clean))\n+\t\tdie(\"%s: clean filter '%s' failed\", path, ca.drv->name);\n+\n+\tca.crlf_action = input_crlf_action(ca.crlf_action, ca.eol_attr);\n+\tcrlf_to_git(path, dst->buf, dst->len, dst, ca.crlf_action, checksafe);\n+\tident_to_git(path, dst->buf, dst->len, dst, ca.ident);\n+}\n+\n static int convert_to_working_tree_internal(const char *path, const char *src,\n \t\t\t\t\t    size_t len, struct strbuf *dst,\n \t\t\t\t\t    int normalizing)\n@@ -811,7 +857,7 @@ static int convert_to_working_tree_internal(const char *path, const char *src,\n \t\t}\n \t}\n \n-\tret_filter = apply_filter(path, src, len, dst, filter);\n+\tret_filter = apply_filter(path, src, len, -1, dst, filter);\n \tif (!ret_filter && required)\n \t\tdie(\"%s: smudge filter %s failed\", path, ca.drv->name);\n \ndiff --git a/convert.h b/convert.h\nindex 0c2143c..d9d853c 100644\n--- a/convert.h\n+++ b/convert.h\n@@ -40,11 +40,15 @@ extern int convert_to_working_tree(const char *path, const char *src,\n \t\t\t\t   size_t len, struct strbuf *dst);\n extern int renormalize_buffer(const char *path, const char *src, size_t len,\n \t\t\t      struct strbuf *dst);\n-static inline int would_convert_to_git(const char *path, const char *src,\n-\t\t\t\t       size_t len, enum safe_crlf checksafe)\n+static inline int would_convert_to_git(const char *path)\n {\n-\treturn convert_to_git(path, src, len, NULL, checksafe);\n+\treturn convert_to_git(path, NULL, 0, NULL, 0);\n }\n+/* Precondition: would_convert_to_git_filter_fd(path) == true */\n+extern void convert_to_git_filter_fd(const char *path, int fd,\n+\t\t\t\t     struct strbuf *dst,\n+\t\t\t\t     enum safe_crlf checksafe);\n+extern int would_convert_to_git_filter_fd(const char *path);\n \n /*****************************************************************\n  *\ndiff --git a/sha1_file.c b/sha1_file.c\nindex 3f70b1d..eaf3220 100644\n--- a/sha1_file.c\n+++ b/sha1_file.c\n@@ -3074,6 +3074,29 @@ static int index_mem(unsigned char *sha1, void *buf, size_t size,\n \treturn ret;\n }\n \n+static int index_stream_convert_blob(unsigned char *sha1, int fd,\n+\t\t\t\t     const char *path, unsigned flags)\n+{\n+\tint ret;\n+\tconst int write_object = flags & HASH_WRITE_OBJECT;\n+\tstruct strbuf sbuf = STRBUF_INIT;\n+\n+\tassert(path);\n+\tassert(would_convert_to_git_filter_fd(path));\n+\n+\tconvert_to_git_filter_fd(path, fd, &sbuf,\n+\t\t\t\t write_object ? safe_crlf : SAFE_CRLF_FALSE);\n+\n+\tif (write_object)\n+\t\tret = write_sha1_file(sbuf.buf, sbuf.len, typename(OBJ_BLOB),\n+\t\t\t\t      sha1);\n+\telse\n+\t\tret = hash_sha1_file(sbuf.buf, sbuf.len, typename(OBJ_BLOB),\n+\t\t\t\t     sha1);\n+\tstrbuf_release(&sbuf);\n+\treturn ret;\n+}\n+\n static int index_pipe(unsigned char *sha1, int fd, enum object_type type,\n \t\t      const char *path, unsigned flags)\n {\n@@ -3141,10 +3164,12 @@ int index_fd(unsigned char *sha1, int fd, struct stat *st,\n \tint ret;\n \tsize_t size = xsize_t(st->st_size);\n \n-\tif (!S_ISREG(st->st_mode))\n+\tif (type == OBJ_BLOB && path && would_convert_to_git_filter_fd(path))\n+\t\tret = index_stream_convert_blob(sha1, fd, path, flags);\n+\telse if (!S_ISREG(st->st_mode))\n \t\tret = index_pipe(sha1, fd, type, path, flags);\n \telse if (size <= big_file_threshold || type != OBJ_BLOB ||\n-\t\t (path && would_convert_to_git(path, NULL, 0, 0)))\n+\t\t (path && would_convert_to_git(path)))\n \t\tret = index_core(sha1, fd, size, type, path, flags);\n \telse\n \t\tret = index_stream(sha1, fd, size, type, path, flags);\ndiff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh\nindex f890c54..2cb2414 100755\n--- a/t/t0021-conversion.sh\n+++ b/t/t0021-conversion.sh\n@@ -153,17 +153,23 @@ test_expect_success 'filter shell-escaped filenames' '\n \t:\n '\n \n-test_expect_success 'required filter success' '\n-\tgit config filter.required.smudge cat &&\n-\tgit config filter.required.clean cat &&\n+test_expect_success 'required filter should filter data' '\n+\tgit config filter.required.smudge ./rot13.sh &&\n+\tgit config filter.required.clean ./rot13.sh &&\n \tgit config filter.required.required true &&\n \n \techo \"*.r filter=required\" >.gitattributes &&\n \n-\techo test >test.r &&\n+\tcat test.o >test.r &&\n \tgit add test.r &&\n+\n \trm -f test.r &&\n-\tgit checkout -- test.r\n+\tgit checkout -- test.r &&\n+\tcmp test.o test.r &&\n+\n+\t./rot13.sh <test.o >expected &&\n+\tgit cat-file blob :test.r >actual &&\n+\tcmp expected actual\n '\n \n test_expect_success 'required filter smudge failure' '\n@@ -190,6 +196,58 @@ test_expect_success 'required filter clean failure' '\n \ttest_must_fail git add test.fc\n '\n \n+# Handle differences in /usr/bin/time.\n+#\n+#  - Linux: call with '-v'.\n+#    output: <spaces><description>:<space><value-in-KBytes>\n+#\n+#  - Mac: call with '-l'.\n+#    output: <spaces><value-in-Bytes><spaces><description>\n+#    Strip three digits to get to KB (base 10 is good enough).\n+#\n+case $(uname -s) in\n+Linux)\n+\ttest_set_prereq HAVE_MAX_MEM_USAGE\n+\tmax_mem_usage_KB () {\n+\t    /usr/bin/time -v \"$@\" 2>&1 |\n+\t    grep 'Maximum resident set size' |\n+\t    cut -d ':' -f 2\n+\t}\n+\t;;\n+Darwin)\n+\ttest_set_prereq HAVE_MAX_MEM_USAGE\n+\tmax_mem_usage_KB () {\n+\t\t/usr/bin/time -l \"$@\" 2>&1 |\n+\t\tgrep 'maximum resident set size' |\n+\t\tsed -e 's/  */ /' |\n+\t\tcut -d ' ' -f 2 |\n+\t\tsed -e 's/...$//'\n+\t}\n+\t;;\n+esac\n+\n+max_mem_usage_is_lt_KB () {\n+\tlimit=$1\n+\tshift\n+\tmem_usage=$(max_mem_usage_KB \"$@\")\n+\tif [ $mem_usage -lt $limit ]; then\n+\t\ttrue\n+\telse\n+\t\tprintf 'Command used too much memory (expected limit %dKB, actual usage %dKB).\\n' \\\n+\t\t\t$limit $mem_usage\n+\t\tfalse\n+\tfi\n+}\n+\n+test_expect_success HAVE_MAX_MEM_USAGE \\\n+'filtering large input to small output should use little memory' '\n+\tgit config filter.devnull.clean \"cat >/dev/null\" &&\n+\tgit config filter.devnull.required true &&\n+\tfor i in $(test_seq 1 30); do printf \"%1048576d\" 1; done >30MB &&\n+\techo \"30MB filter=devnull\" >.gitattributes &&\n+\tmax_mem_usage_is_lt_KB 15000 git add 30MB\n+'\n+\n test_expect_success EXPENSIVE 'filter large file' '\n \tgit config filter.largefile.smudge cat &&\n \tgit config filter.largefile.clean cat &&\n-- \n2.1.0.rc0.58.ga8a34bf\n"},{"id":"247238","messageId":"xmqq4mxsrsao.fsf@gitster.dls.corp.google.com","threadId":"37281","inReplyTo":"1407056176-8231-1-git-send-email-prohaska@zib.de","subject":"Re: [PATCH] convert: Stream from fd to required clean filter instead of mmap","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-08-04T19:03:27Z","receivedAt":"2014-08-04T19:03:27Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Steffen Prohaska <prohaska@zib.de> writes:\n\n> The data is streamed to the filter process anyway.  Better avoid mapping\n> the file if possible.  This is especially useful if a clean filter\n> reduces the size, for example if it computes a sha1 for binary data,\n> like git media.  The file size that the previous implementation could\n> handle was limited by the available address space; large files for\n> example could not be handled with (32-bit) msysgit.  The new\n> implementation can filter files of any size as long as the filter output\n> is small enough.\n>\n> The new code path is only taken if the filter is required.  The filter\n> consumes data directly from the fd.  The original data is not available\n> to git, so it must fail if the filter fails.\n\nYay ;-)\n\n>\n> The test that exercises required filters is modified to verify that the\n> data actually has been modified on its way from the file system to the\n> object store.\n>\n> The expectation on the process size is tested using /usr/bin/time.  An\n> alternative would have been tcsh, which could be used to print memory\n> information as follows:\n>\n>     tcsh -c 'set time=(0 \"%M\"); <cmd>'\n>\n> Although the logic could perhaps be simplified with tcsh, I chose to use\n> 'time' to avoid a dependency on tcsh.\n>\n> Signed-off-by: Steffen Prohaska <prohaska@zib.de>\n> ---\n>  convert.c             | 58 ++++++++++++++++++++++++++++++++++++++-----\n>  convert.h             | 10 +++++---\n>  sha1_file.c           | 29 ++++++++++++++++++++--\n>  t/t0021-conversion.sh | 68 +++++++++++++++++++++++++++++++++++++++++++++++----\n>  4 files changed, 149 insertions(+), 16 deletions(-)\n>\n> diff --git a/convert.c b/convert.c\n> index cb5fbb4..58a516a 100644\n> --- a/convert.c\n> +++ b/convert.c\n> @@ -312,11 +312,12 @@ static int crlf_to_worktree(const char *path, const char *src, size_t len,\n>  struct filter_params {\n>  \tconst char *src;\n>  \tunsigned long size;\n> +\tint fd;\n>  \tconst char *cmd;\n>  \tconst char *path;\n>  };\n>  \n> -static int filter_buffer(int in, int out, void *data)\n> +static int filter_buffer_or_fd(int in, int out, void *data)\n>  {\n>  \t/*\n>  \t * Spawn cmd and feed the buffer contents through its stdin.\n> @@ -325,6 +326,7 @@ static int filter_buffer(int in, int out, void *data)\n>  \tstruct filter_params *params = (struct filter_params *)data;\n>  \tint write_err, status;\n>  \tconst char *argv[] = { NULL, NULL };\n> +\tint fd;\n>  \n>  \t/* apply % substitution to cmd */\n>  \tstruct strbuf cmd = STRBUF_INIT;\n> @@ -355,7 +357,17 @@ static int filter_buffer(int in, int out, void *data)\n>  \n>  \tsigchain_push(SIGPIPE, SIG_IGN);\n>  \n> -\twrite_err = (write_in_full(child_process.in, params->src, params->size) < 0);\n> +\tif (params->src) {\n> +\t    write_err = (write_in_full(child_process.in, params->src, params->size) < 0);\n> +\t} else {\n> +\t    /* dup(), because copy_fd() closes the input fd. */\n> +\t    fd = dup(params->fd);\n> +\t    if (fd < 0)\n> +\t\twrite_err = error(\"failed to dup file descriptor.\");\n> +\t    else\n> +\t\twrite_err = copy_fd(fd, child_process.in);\n> +\t}\n> +\n>  \tif (close(child_process.in))\n>  \t\twrite_err = 1;\n>  \tif (write_err)\n> @@ -371,7 +383,7 @@ static int filter_buffer(int in, int out, void *data)\n>  \treturn (write_err || status);\n>  }\n>  \n> -static int apply_filter(const char *path, const char *src, size_t len,\n> +static int apply_filter(const char *path, const char *src, size_t len, int fd,\n>                          struct strbuf *dst, const char *cmd)\n>  {\n>  \t/*\n> @@ -392,11 +404,12 @@ static int apply_filter(const char *path, const char *src, size_t len,\n>  \t\treturn 1;\n>  \n>  \tmemset(&async, 0, sizeof(async));\n> -\tasync.proc = filter_buffer;\n> +\tasync.proc = filter_buffer_or_fd;\n>  \tasync.data = &params;\n>  \tasync.out = -1;\n>  \tparams.src = src;\n>  \tparams.size = len;\n> +\tparams.fd = fd;\n>  \tparams.cmd = cmd;\n>  \tparams.path = path;\n>  \n> @@ -747,6 +760,22 @@ static void convert_attrs(struct conv_attrs *ca, const char *path)\n>  \t}\n>  }\n>  \n> +int would_convert_to_git_filter_fd(const char *path) {\n\nStyle; opening brace on its own line:\n\n\tint would_convert_to_git_filter_fd(const char *path)\n        {\n\n> +\tstruct conv_attrs ca;\n> +\tconvert_attrs(&ca, path);\n> +\n> +\tif (!ca.drv)\n\nAs we do not allow decl-after-stmt, it is easier to read if the\nfirst blank line in the function was between the local variable\ndefinition and the first statement.  I.e.\n\n\tint would_convert_to_git_filter_fd(const char *path)\n        {\n\t\tstruct ...;\n\n\t\tconvert_attrs(...);\n                if (!ca.drv)\n\n\n> +\t    return 0;\n> +\n> +\t/* Apply a filter to an fd only if the filter is required to succeed.\n> +\t * We must die if the filter fails, because the original data before\n> +\t * filtering is not available. */\n\n /*\n  * multi-line\n  * comment style.\n  */\n\n> +\tif (!ca.drv->required)\n> +\t    return 0;\n> +\n> +\treturn apply_filter(path, 0, 0, -1, 0, ca.drv->clean);\n\nWhat's the significance of \"-1\" here?  Does somebody in the\ncallchain from apply_filter() check if fd < 0 and act differently\n(not a complaint nor rhetoric question)?\n\nSpell out the first \"0\" as \"NULL\" if you meant src == NULL\n(similarly for dst).\n\n> diff --git a/convert.h b/convert.h\n> index 0c2143c..d9d853c 100644\n> --- a/convert.h\n> +++ b/convert.h\n> @@ -40,11 +40,15 @@ extern int convert_to_working_tree(const char *path, const char *src,\n>  \t\t\t\t   size_t len, struct strbuf *dst);\n>  extern int renormalize_buffer(const char *path, const char *src, size_t len,\n>  \t\t\t      struct strbuf *dst);\n> -static inline int would_convert_to_git(const char *path, const char *src,\n> -\t\t\t\t       size_t len, enum safe_crlf checksafe)\n> +static inline int would_convert_to_git(const char *path)\n>  {\n> -\treturn convert_to_git(path, src, len, NULL, checksafe);\n> +\treturn convert_to_git(path, NULL, 0, NULL, 0);\n>  }\n\nIs this change because there was only one caller that passed nothing\nmeaningful but \"path\"?  It would have been nicer to see such a change\nas a clean-up _before_ the real patch, if that was the case.\n"},{"id":"247305","messageId":"36DA9A8E-2F61-4DD3-9F82-427976C710A6@zib.de","threadId":"37281","inReplyTo":"xmqq4mxsrsao.fsf@gitster.dls.corp.google.com","subject":"Re: [PATCH] convert: Stream from fd to required clean filter instead of mmap","fromName":"Steffen Prohaska","fromEmail":"prohaska@zib.de","sentAt":"2014-08-06T04:22:58Z","receivedAt":"2014-08-06T04:22:58Z","isPatch":true,"sender":{"key":"prohaska@zib.de","avatar":"https://avatars.githubusercontent.com/u/217580?v=4"},"body":"I'll address the comments about style in a revised patch.\n\n\nOn Aug 4, 2014, at 9:03 PM, Junio C Hamano <gitster@pobox.com> wrote:\n\n>> +\treturn apply_filter(path, 0, 0, -1, 0, ca.drv->clean);\n> \n> What's the significance of \"-1\" here?  Does somebody in the\n> callchain from apply_filter() check if fd < 0 and act differently\n> (not a complaint nor rhetoric question)?\n\nThe decision in apply_filter() to return before the real work is\ntaken on the first 0.  Any value for fd would be ok.  The -1 is\nonly a reminder that the fd is invalid.\n\n\tSteffen\n"},{"id":"247306","messageId":"683E5A36-1D64-4185-8C81-E7D8EDB72DE9@zib.de","threadId":"37281","inReplyTo":"36DA9A8E-2F61-4DD3-9F82-427976C710A6@zib.de","subject":"Re: [PATCH] convert: Stream from fd to required clean filter instead of mmap","fromName":"","fromEmail":"prohaska@zib.de","sentAt":"2014-08-06T04:32:06Z","receivedAt":"2014-08-06T04:32:06Z","isPatch":true,"sender":{"key":"prohaska@zib.de","avatar":"https://avatars.githubusercontent.com/u/217580?v=4"},"body":"\nOn Aug 6, 2014, at 6:22 AM, Steffen Prohaska <prohaska@zib.de> wrote:\n\n> On Aug 4, 2014, at 9:03 PM, Junio C Hamano <gitster@pobox.com> wrote:\n> \n>>> +\treturn apply_filter(path, 0, 0, -1, 0, ca.drv->clean);\n>> \n>> What's the significance of \"-1\" here?  Does somebody in the\n>> callchain from apply_filter() check if fd < 0 and act differently\n>> (not a complaint nor rhetoric question)?\n> \n> The decision in apply_filter() to return before the real work is\n> taken on the first 0.\n\nHmm..., actually it's the last 0 that matters.  If no destination buffer\nis given, apply_filter() returns whether it would filter or not.\n\n\n>  Any value for fd would be ok.  The -1 is\n> only a reminder that the fd is invalid.\n\n\n\tSteffen\n"}]}