about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2009-08-28 20:47:43 -0700
committerEric Wong <normalperson@yhbt.net>2009-08-28 20:48:10 -0700
commita70468036d9b780bc7ec921f7feb6e1275778169 (patch)
tree302c8b4f4a30203c9549dbd7579006a729c1830f
-rw-r--r--.gitignore13
-rw-r--r--COPYING165
-rw-r--r--GNUmakefile37
-rw-r--r--History.txt5
-rw-r--r--Manifest.txt19
-rw-r--r--README.txt114
-rw-r--r--Rakefile30
-rw-r--r--ext/clogger_ext/clogger.c800
-rw-r--r--ext/clogger_ext/extconf.rb12
-rw-r--r--ext/clogger_ext/ruby_1_9_compat.h23
-rw-r--r--lib/clogger.rb133
-rw-r--r--lib/clogger/format.rb25
-rw-r--r--lib/clogger/pure.rb126
-rw-r--r--setup.rb1585
-rw-r--r--test/test_clogger.rb349
15 files changed, 3436 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6d1222b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+Makefile
+*.log
+*.rbc
+*.so
+*.o
+*.a
+*.bundle
+.DS_Store
+/.config
+/InstalledFiles
+/doc
+/local.mk
+/pkg
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..cca7fc2
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,165 @@
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/GNUmakefile b/GNUmakefile
new file mode 100644
index 0000000..3481564
--- /dev/null
+++ b/GNUmakefile
@@ -0,0 +1,37 @@
+all:: test
+ruby = ruby
+
+-include local.mk
+
+ifeq ($(DLEXT),) # "so" for Linux
+  DLEXT := $(shell $(ruby) -rrbconfig -e 'puts Config::CONFIG["DLEXT"]')
+endif
+
+ifeq ($(RUBY_VERSION),)
+  RUBY_VERSION := $(shell $(ruby) -e 'puts RUBY_VERSION')
+endif
+
+ext/clogger_ext/Makefile: ext/clogger_ext/clogger.c ext/clogger_ext/extconf.rb
+        cd ext/clogger_ext && $(ruby) extconf.rb
+
+ext/clogger_ext/clogger.$(DLEXT): ext/clogger_ext/Makefile
+        $(MAKE) -C ext/clogger_ext
+
+clean:
+        -$(MAKE) -C ext/clogger_ext clean
+        $(RM) ext/clogger_ext/Makefile lib/clogger_ext.$(DLEXT)
+
+test-ext: ext/clogger_ext/clogger.$(DLEXT)
+        $(ruby) -Iext/clogger_ext:lib test/test_clogger.rb
+
+test-pure:
+        $(ruby) -Ilib test/test_clogger.rb
+
+test: test-ext test-pure
+
+Manifest.txt:
+        git ls-files > $@+
+        cmp $@+ $@ || mv $@+ $@
+        $(RM) -f $@+
+
+.PHONY: test doc Manifest.txt
diff --git a/History.txt b/History.txt
new file mode 100644
index 0000000..51bd07d
--- /dev/null
+++ b/History.txt
@@ -0,0 +1,5 @@
+=== 0.0.1 / 2009-08-28
+
+* 1 major enhancement
+
+  * initial release
diff --git a/Manifest.txt b/Manifest.txt
new file mode 100644
index 0000000..ec8915c
--- /dev/null
+++ b/Manifest.txt
@@ -0,0 +1,19 @@
+.gitignore
+COPYING
+GNUmakefile
+History.txt
+Manifest.txt
+README.txt
+Rakefile
+benchmarks/rack_common_logger_0_9_1.rb
+benchmarks/rack_common_logger_1_0_0.rb
+benchmarks/rack_common_logger_ew.rb
+benchmarks/test_run.rb
+ext/clogger_ext/clogger.c
+ext/clogger_ext/extconf.rb
+ext/clogger_ext/ruby_1_9_compat.h
+lib/clogger.rb
+lib/clogger/format.rb
+lib/clogger/pure.rb
+setup.rb
+test/test_clogger.rb
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..b57b97f
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,114 @@
+= Clogger - configurable request logging for Rack
+
+* http://clogger.rubyforge.org/
+* mailto:clogger@librelist.com
+* git://rubyforge.org/clogger.git
+* http://clogger.rubyforge.org/git?p=clogger.git
+
+== DESCRIPTION
+
+Clogger is Rack middleware for logging HTTP requests.  The log format
+is customizable so you can specify exactly which fields to log.
+
+== FEATURES
+
+* pre-defines Apache Common Log Format, Apache Combined Log Format and
+  Rack::CommonLogger (as distributed by Rack 1.0) formats.
+
+* highly customizable with easy-to-read nginx-like log formatting variables.
+
+* Untrusted values are escaped (all HTTP headers, request URI components)
+  to make life easier for HTTP log parsers. The following bytes are escaped:
+
+    ' (single quote)
+    " (double quote)
+    all bytes in the range of \x00-\x1f
+
+== SYNOPSIS
+
+Clogger may be loaded as Rack middleware in your config.ru:
+
+  require "clogger"
+  use Clogger,
+      :format => Clogger::Format::Combined,
+      :logger => File.open("/path/to/log", "ab")
+  run YourApplication.new
+
+If you're using Rails 2.3.x or later, in your config/environment.rb
+somewhere inside the "Rails::Initializer.run do |config|" block:
+
+  config.middleware.use 'Clogger',
+      :format => Clogger::Format::Combined,
+      :logger => File.open("/path/to/log", "ab")
+
+== VARIABLES
+
+* $http_* - HTTP request headers (e.g. $http_user_agent)
+* $sent_http_* - HTTP response headers (e.g. $sent_http_content_length)
+* $cookie_* - HTTP request cookie (e.g. $cookie_session_id)
+  Rack::Request#cookies must have been used by the underlying application
+  to parse the cookies into a hash.
+* $request_method - the HTTP request method (e.g. GET, POST, HEAD, ...)
+* $path_info - path component requested (e.g. /index.html)
+* $query_string - request query string (not including leading "?")
+* $request_uri - the URI requested ($path_info?$query_string)
+* $request - the first line of the HTTP request
+  ($request_method $request_uri $http_version)
+* $request_time, $request_time{PRECISION} - time taken for request
+  (including response body iteration).  PRECISION defaults to 3
+  (milliseconds) if not specified but may be specified 0(seconds) to
+  6(microseconds).
+* $time_local, $time_local{FORMAT} - current local time, FORMAT defaults to
+  "%d/%b/%Y:%H:%M:%S %z" but accepts any strftime(3)-compatible format
+* $time_utc, $time_utc{FORMAT} - like $time_local, except with UTC
+* $usec - current time in seconds.microseconds since the Epoch
+* $msec - current time in seconds.milliseconds since the Epoch
+* $body_bytes_sent - bytes in the response body (Apache: %B)
+* $response_length - body_bytes_sent, except "-" instead of "0" (Apache: %b)
+* $remote_user - HTTP-authenticated user
+* $remote_addr - IP of the requesting client socket
+* $ip - X-Forwarded-For request header if available, $remote_addr if not
+* $pid - process ID of the current process
+* $e{Thread.current} - Thread processing the request
+* $e{Actor.current} - Actor processing the request (Revactor or Rubinius)
+
+== REQUIREMENTS
+
+* Ruby, Rack
+
+== CONTACT
+
+All feedback (bug reports, user/development dicussion, patches, pull
+requests) should go to the mailing list.  Patches should be sent inline
+(git format-patch -M + git send-email) so we can reply to them inline.
+
+* mailto:clogger@librelist.com
+
+== INSTALL:
+
+For Rubygems users:
+
+  gem install clogger
+
+If you're using MRI 1.8 or 1.9 and have a build environment, you can also try:
+
+  gem install clogger_ext
+
+A setup.rb file is also included if you do not use Rubygems.
+
+== LICENSE
+
+Copyright (C) 2009 Eric Wong <normalperson@yhbt.net> and contributors.
+
+Clogger is free software; you can redistribute it and/or modify it under
+the terms of the GNU Lesser General Public License as published by the
+Free Software Foundation, version 3.0.
+
+Clogger is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Clogger; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..d1dcf85
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,30 @@
+require 'hoe'
+$LOAD_PATH << 'lib'
+require 'clogger'
+begin
+  require 'rake/extensiontask'
+  Rake::ExtensionTask.new('clogger_ext')
+rescue LoadError
+  warn "rake-compiler not available, cross compiling disabled"
+end
+
+common = lambda do |hoe|
+  title = hoe.paragraphs_of("README.txt", 0).first.sub(/^= /, '')
+  hoe.version = Clogger::VERSION
+  hoe.summary = title.split(/\s*-\s*/, 2).last
+  hoe.description = hoe.paragraphs_of("README.txt", 3)
+  hoe.rubyforge_name = 'clogger'
+  hoe.author = 'Eric Wong'
+  hoe.email = 'clogger@librelist.com'
+  hoe.spec_extras.merge!('rdoc_options' => [ "--title", title ])
+  hoe.remote_rdoc_dir = ''
+end
+
+if ENV['CLOGGER_EXT']
+  Hoe.spec('clogger_ext') do
+    common.call(self)
+    self.spec_extras.merge!(:extensions => Dir.glob('ext/*/extconf.rb'))
+  end
+else
+  Hoe.spec('clogger') { common.call(self) }
+end
diff --git a/ext/clogger_ext/clogger.c b/ext/clogger_ext/clogger.c
new file mode 100644
index 0000000..34927d3
--- /dev/null
+++ b/ext/clogger_ext/clogger.c
@@ -0,0 +1,800 @@
+#define _BSD_SOURCE
+#include <ruby.h>
+#include <assert.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/time.h>
+#include <time.h>
+#include <errno.h>
+#ifdef HAVE_FCNTL_H
+#  include <fcntl.h>
+#endif
+#include "ruby_1_9_compat.h"
+
+/* in case _BSD_SOURCE doesn't give us this macro */
+#ifndef timersub
+#  define timersub(a, b, result) \
+do { \
+        (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
+        (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
+        if ((result)->tv_usec < 0) { \
+                --(result)->tv_sec; \
+                (result)->tv_usec += 1000000; \
+        } \
+} while (0)
+#endif
+
+/* give GCC hints for better branch prediction
+ * (we layout branches so that ASCII characters are handled faster) */
+#if defined(__GNUC__) && (__GNUC__ >= 3)
+#  define likely(x)                __builtin_expect (!!(x), 1)
+#  define unlikely(x)                __builtin_expect (!!(x), 0)
+#else
+#  define unlikely(x)                (x)
+#  define likely(x)                (x)
+#endif
+
+enum clogger_opcode {
+        CL_OP_LITERAL = 0,
+        CL_OP_REQUEST,
+        CL_OP_RESPONSE,
+        CL_OP_SPECIAL,
+        CL_OP_EVAL,
+        CL_OP_TIME_LOCAL,
+        CL_OP_TIME_UTC,
+        CL_OP_REQUEST_TIME,
+        CL_OP_TIME,
+        CL_OP_COOKIE
+};
+
+enum clogger_special {
+        CL_SP_body_bytes_sent = 0,
+        CL_SP_status,
+        CL_SP_request,
+        CL_SP_request_length,
+        CL_SP_response_length,
+        CL_SP_ip,
+        CL_SP_pid
+};
+
+struct clogger {
+        VALUE app;
+
+        VALUE fmt_ops;
+        VALUE logger;
+        VALUE log_buf;
+
+        VALUE env;
+        VALUE cookies;
+        VALUE response;
+
+        off_t body_bytes_sent;
+        struct timeval tv_start;
+
+        int fd;
+        int wrap_body;
+        int need_resp;
+        int reentrant; /* tri-state, -1:auto, 1/0 true/false */
+};
+
+static ID ltlt_id;
+static ID call_id;
+static ID each_id;
+static ID close_id;
+static ID to_i_id;
+static ID to_s_id;
+static ID size_id;
+static VALUE cClogger;
+static VALUE mFormat;
+
+/* common hash lookup keys */
+static VALUE g_HTTP_X_FORWARDED_FOR;
+static VALUE g_REMOTE_ADDR;
+static VALUE g_REQUEST_METHOD;
+static VALUE g_PATH_INFO;
+static VALUE g_QUERY_STRING;
+static VALUE g_HTTP_VERSION;
+static VALUE g_rack_errors;
+static VALUE g_rack_input;
+static VALUE g_rack_multithread;
+static VALUE g_dash;
+static VALUE g_empty;
+static VALUE g_space;
+static VALUE g_question_mark;
+static VALUE g_rack_request_cookie_hash;
+
+#define LOG_BUF_INIT_SIZE 128
+
+static void init_buffers(struct clogger *c)
+{
+        c->log_buf = rb_str_buf_new(LOG_BUF_INIT_SIZE);
+}
+
+static inline int need_escape(unsigned c)
+{
+        assert(c <= 0xff);
+        return !!(c == '\'' || c == '"' || (c >= 0 && c <= 0x1f));
+}
+
+/* we are encoding-agnostic, clients can send us all sorts of junk */
+static VALUE byte_xs(VALUE from)
+{
+        static const char esc[] = "0123456789ABCDEF";
+        unsigned char *new_ptr;
+        unsigned char *ptr = (unsigned char *)RSTRING_PTR(from);
+        long len = RSTRING_LEN(from);
+        long new_len = len;
+        VALUE rv;
+
+        for (; --len >= 0; ptr++) {
+                unsigned c = *ptr;
+
+                if (unlikely(need_escape(c)))
+                        new_len += 3; /* { '\', 'x', 'X', 'X' } */
+        }
+
+        len = RSTRING_LEN(from);
+        if (new_len == len)
+                return from;
+
+        rv = rb_str_new(0, new_len);
+        new_ptr = (unsigned char *)RSTRING_PTR(rv);
+        ptr = (unsigned char *)RSTRING_PTR(from);
+        for (; --len >= 0; ptr++) {
+                unsigned c = *ptr;
+
+                if (unlikely(need_escape(c))) {
+                        *new_ptr++ = '\\';
+                        *new_ptr++ = 'x';
+                        *new_ptr++ = esc[c >> 4];
+                        *new_ptr++ = esc[c & 0xf];
+                } else {
+                        *new_ptr++ = c;
+                }
+        }
+        assert(RSTRING_PTR(rv)[RSTRING_LEN(rv)] == '\0');
+
+        return rv;
+}
+
+/* strcasecmp isn't locale independent, so we roll our own */
+static int str_case_eq(VALUE a, VALUE b)
+{
+        long alen = RSTRING_LEN(a);
+        long blen = RSTRING_LEN(b);
+
+        if (alen == blen) {
+                const char *aptr = RSTRING_PTR(a);
+                const char *bptr = RSTRING_PTR(b);
+
+                for (; alen--; ++aptr, ++bptr) {
+                        if ((*bptr == *aptr)
+                            || (*aptr >= 'A' && *aptr <= 'Z' &&
+                                (*aptr | 0x20) == *bptr))
+                                continue;
+                        return 0;
+                }
+                return 1;
+        }
+        return 0;
+}
+
+struct response_ops { long nr; VALUE ops; };
+
+/* this can be worse than O(M*N) :<... but C loops are fast ... */
+static VALUE swap_sent_headers(VALUE kv, VALUE memo)
+{
+        struct response_ops *tmp = (struct response_ops *)memo;
+        VALUE key = RARRAY_PTR(kv)[0];
+        long i = RARRAY_LEN(tmp->ops);
+        VALUE *ary = RARRAY_PTR(tmp->ops);
+        VALUE value;
+
+        for (; --i >= 0; ary++) {
+                VALUE *op = RARRAY_PTR(*ary);
+                enum clogger_opcode opcode = NUM2INT(op[0]);
+
+                if (opcode != CL_OP_RESPONSE)
+                        continue;
+                assert(RARRAY_LEN(*ary) == 2);
+                if (!str_case_eq(key, op[1]))
+                        continue;
+
+                value = RARRAY_PTR(kv)[1];
+                op[0] = INT2NUM(CL_OP_LITERAL);
+                op[1] = byte_xs(rb_obj_as_string(value));
+
+                if (!--tmp->nr)
+                        rb_iter_break();
+                return Qnil;
+        }
+        return Qnil;
+}
+
+static VALUE sent_headers_ops(struct clogger *c)
+{
+        struct response_ops tmp;
+        long i, len;
+        VALUE *ary;
+
+        if (!c->need_resp)
+                return c->fmt_ops;
+
+        tmp.nr = 0;
+        tmp.ops = rb_ary_dup(c->fmt_ops);
+        len = RARRAY_LEN(tmp.ops);
+        ary = RARRAY_PTR(tmp.ops);
+
+        for (i = 0; i < len; ++i) {
+                VALUE *op = RARRAY_PTR(ary[i]);
+
+                if (NUM2INT(op[0]) == CL_OP_RESPONSE) {
+                        assert(RARRAY_LEN(ary[i]) == 2);
+                        ary[i] = rb_ary_dup(ary[i]);
+                        ++tmp.nr;
+                }
+        }
+
+        rb_iterate(rb_each, RARRAY_PTR(c->response)[1],
+                   swap_sent_headers, (VALUE)&tmp);
+
+        return tmp.ops;
+}
+
+static void clogger_mark(void *ptr)
+{
+        struct clogger *c = ptr;
+
+        rb_gc_mark_locations(&c->app, &c->response);
+}
+
+static VALUE clogger_alloc(VALUE klass)
+{
+        struct clogger *c;
+
+        return Data_Make_Struct(klass, struct clogger, clogger_mark, 0, c);
+}
+
+static struct clogger *clogger_get(VALUE self)
+{
+        struct clogger *c;
+
+        Data_Get_Struct(self, struct clogger, c);
+        assert(c);
+        return c;
+}
+
+static VALUE obj_fileno(VALUE obj)
+{
+        return rb_funcall(obj, rb_intern("fileno"), 0);
+}
+
+/* only for writing to regular files, not stupid crap like NFS  */
+static void write_full(int fd, const void *buf, size_t count)
+{
+        ssize_t r;
+
+        while (count > 0) {
+                r = write(fd, buf, count);
+
+                if (r == count) { /* overwhelmingly likely */
+                        return;
+                } else if (r > 0) {
+                        count -= r;
+                        buf += r;
+                } else {
+                        if (errno == EINTR || errno == EAGAIN)
+                                continue; /* poor souls on NFS and like: */
+                        if (!errno)
+                                errno = ENOSPC;
+                        rb_sys_fail("write");
+                }
+        }
+}
+
+/*
+ * allow us to use write_full() iff we detect a blocking file
+ * descriptor that wouldn't play nicely with Ruby threading/fibers
+ */
+static int raw_fd(VALUE fileno)
+{
+#if defined(HAVE_FCNTL) && defined(F_GETFL) && defined(O_NONBLOCK)
+        int fd;
+        int flags;
+
+        if (NIL_P(fileno))
+                return -1;
+        fd = NUM2INT(fileno);
+
+        flags = fcntl(fd, F_GETFL);
+        if (flags < 0)
+                rb_sys_fail("fcntl");
+
+        return (flags & O_NONBLOCK) ? -1 : fd;
+#else /* platforms w/o fcntl/F_GETFL/O_NONBLOCK */
+        return -1;
+#endif /* platforms w/o fcntl/F_GETFL/O_NONBLOCK */
+}
+
+/* :nodoc: */
+static VALUE clogger_reentrant(VALUE self)
+{
+        return clogger_get(self)->reentrant == 0 ? Qfalse : Qtrue;
+}
+
+/* :nodoc: */
+static VALUE clogger_wrap_body(VALUE self)
+{
+        return clogger_get(self)->wrap_body == 0 ? Qfalse : Qtrue;
+}
+
+static void append_status(struct clogger *c, VALUE status)
+{
+        char buf[sizeof("999")];
+        int nr;
+
+        if (TYPE(status) != T_FIXNUM) {
+                status = rb_funcall(status, to_i_id, 0);
+                /* no way it's a valid status code (at least not HTTP/1.1) */
+                if (TYPE(status) != T_FIXNUM) {
+                        rb_str_buf_append(c->log_buf, g_dash);
+                        return;
+                }
+        }
+
+        nr = NUM2INT(status);
+        if (nr >= 100 && nr <= 999) {
+                nr = snprintf(buf, sizeof(buf), "%03d", nr);
+                assert(nr == 3);
+                rb_str_buf_cat(c->log_buf, buf, nr);
+        } else {
+                /* raise?, swap for 500? */
+                rb_str_buf_append(c->log_buf, g_dash);
+        }
+}
+
+/* this is Rack 1.0.0-compatible, won't try to parse commas in XFF */
+static void append_ip(struct clogger *c)
+{
+        VALUE env = c->env;
+        VALUE tmp = rb_hash_aref(env, g_HTTP_X_FORWARDED_FOR);
+
+        if (NIL_P(tmp)) {
+                /* can't be faked on any real server, so no escape */
+                tmp = rb_hash_aref(env, g_REMOTE_ADDR);
+                if (NIL_P(tmp))
+                        tmp = g_dash;
+        } else {
+                tmp = byte_xs(tmp);
+        }
+        rb_str_buf_append(c->log_buf, tmp);
+}
+
+static void append_body_bytes_sent(struct clogger *c)
+{
+        char buf[(sizeof(off_t) * 8) / 3 + 1];
+        const char *fmt = sizeof(off_t) == sizeof(long) ? "%ld" : "%lld";
+        int nr = snprintf(buf, sizeof(buf), fmt, c->body_bytes_sent);
+
+        assert(nr > 0 && nr < sizeof(buf));
+        rb_str_buf_cat(c->log_buf, buf, nr);
+}
+
+static void append_tv(struct clogger *c, const VALUE *op, struct timeval *tv)
+{
+        char buf[sizeof(".000000") + ((sizeof(tv->tv_sec) * 8) / 3)];
+        int nr;
+        char *fmt = RSTRING_PTR(op[1]);
+        int div = NUM2INT(op[2]);
+
+        nr = snprintf(buf, sizeof(buf), fmt,
+                      (int)tv->tv_sec, (int)(tv->tv_usec / div));
+        assert(nr > 0 && nr < sizeof(buf));
+        rb_str_buf_cat(c->log_buf, buf, nr);
+}
+
+static void append_request_time_fmt(struct clogger *c, const VALUE *op)
+{
+        struct timeval now, d;
+
+        gettimeofday(&now, NULL);
+        timersub(&now, &c->tv_start, &d);
+        append_tv(c, op, &d);
+}
+
+static void append_time_fmt(struct clogger *c, const VALUE *op)
+{
+        struct timeval now;
+
+        gettimeofday(&now, NULL);
+        append_tv(c, op, &now);
+}
+
+static void append_request(struct clogger *c)
+{
+        VALUE tmp;
+        VALUE env = c->env;
+
+        /* REQUEST_METHOD doesn't need escaping, Rack::Lint governs it */
+        tmp = rb_hash_aref(env, g_REQUEST_METHOD);
+        rb_str_buf_append(c->log_buf, NIL_P(tmp) ? g_empty : tmp);
+        rb_str_buf_append(c->log_buf, g_space);
+
+        /* broken clients can send " and other questionable URIs */
+        tmp = rb_hash_aref(env, g_PATH_INFO);
+        rb_str_buf_append(c->log_buf, NIL_P(tmp) ? g_empty : byte_xs(tmp));
+
+        tmp = rb_hash_aref(env, g_QUERY_STRING);
+        if (RSTRING_LEN(tmp) != 0) {
+                rb_str_buf_append(c->log_buf, g_question_mark);
+                rb_str_buf_append(c->log_buf, byte_xs(tmp));
+        }
+        rb_str_buf_append(c->log_buf, g_space);
+
+        /* HTTP_VERSION can be injected by malicious clients */
+        tmp = rb_hash_aref(env, g_HTTP_VERSION);
+        rb_str_buf_append(c->log_buf, NIL_P(tmp) ? g_empty : byte_xs(tmp));
+}
+
+static void append_request_length(struct clogger *c)
+{
+        VALUE tmp = rb_hash_aref(c->env, g_rack_input);
+        if (NIL_P(tmp)) {
+                rb_str_buf_append(c->log_buf, g_dash);
+        } else {
+                tmp = rb_funcall(tmp, size_id, 0);
+                rb_str_buf_append(c->log_buf, rb_funcall(tmp, to_s_id, 0));
+        }
+}
+
+static void append_time(struct clogger *c, enum clogger_opcode op, VALUE fmt)
+{
+        /* you'd have to be a moron to use formats this big... */
+        char buf[sizeof("Saturday, November 01, 1970, 00:00:00 PM +0000")];
+        size_t nr;
+        struct tm tmp;
+        time_t t = time(NULL);
+
+        if (op == CL_OP_TIME_LOCAL)
+                localtime_r(&t, &tmp);
+        else if (op == CL_OP_TIME_UTC)
+                gmtime_r(&t, &tmp);
+        else
+                assert(0 && "unknown op");
+
+        nr = strftime(buf, sizeof(buf), RSTRING_PTR(fmt), &tmp);
+        if (nr == 0 || nr == sizeof(buf))
+                rb_str_buf_append(c->log_buf, g_dash);
+        else
+                rb_str_buf_cat(c->log_buf, buf, nr);
+}
+
+static void append_pid(struct clogger *c)
+{
+        char buf[(sizeof(pid_t) * 8) / 3 + 1];
+        int nr = snprintf(buf, sizeof(buf), "%d", (int)getpid());
+
+        assert(nr > 0 && nr < sizeof(buf));
+        rb_str_buf_cat(c->log_buf, buf, nr);
+}
+
+static void append_eval(struct clogger *c, VALUE str)
+{
+        int state = -1;
+        VALUE rv = rb_eval_string_protect(RSTRING_PTR(str), &state);
+
+        rv = state == 0 ? rb_obj_as_string(rv) : g_dash;
+        rb_str_buf_append(c->log_buf, rv);
+}
+
+static void append_cookie(struct clogger *c, VALUE key)
+{
+        VALUE cookie;
+
+        if (c->cookies == Qfalse)
+                c->cookies = rb_hash_aref(c->env, g_rack_request_cookie_hash);
+
+        if (NIL_P(c->cookies)) {
+                cookie = g_dash;
+        } else {
+                cookie = rb_hash_aref(c->cookies, key);
+                if (NIL_P(cookie))
+                        cookie = g_dash;
+        }
+        rb_str_buf_append(c->log_buf, cookie);
+}
+
+static void append_request_env(struct clogger *c, VALUE key)
+{
+        VALUE tmp = rb_hash_aref(c->env, key);
+
+        tmp = NIL_P(tmp) ? g_dash : byte_xs(rb_obj_as_string(tmp));
+        rb_str_buf_append(c->log_buf, tmp);
+}
+
+static void special_var(struct clogger *c, enum clogger_special var)
+{
+        switch (var) {
+        case CL_SP_body_bytes_sent:
+                append_body_bytes_sent(c);
+                break;
+        case CL_SP_status:
+                append_status(c, RARRAY_PTR(c->response)[0]);
+                break;
+        case CL_SP_request:
+                append_request(c);
+                break;
+        case CL_SP_request_length:
+                append_request_length(c);
+                break;
+        case CL_SP_response_length:
+                if (c->body_bytes_sent == 0)
+                        rb_str_buf_append(c->log_buf, g_dash);
+                else
+                        append_body_bytes_sent(c);
+                break;
+        case CL_SP_ip:
+                append_ip(c);
+                break;
+        case CL_SP_pid:
+                append_pid(c);
+        }
+}
+
+static VALUE cwrite(struct clogger *c)
+{
+        const VALUE ops = sent_headers_ops(c);
+        const VALUE *ary = RARRAY_PTR(ops);
+        long i = RARRAY_LEN(ops);
+        VALUE dst = c->log_buf;
+
+        rb_str_set_len(dst, 0);
+
+        for (; --i >= 0; ary++) {
+                const VALUE *op = RARRAY_PTR(*ary);
+                enum clogger_opcode opcode = NUM2INT(op[0]);
+
+                switch (opcode) {
+                case CL_OP_LITERAL:
+                        rb_str_buf_append(dst, op[1]);
+                        break;
+                case CL_OP_REQUEST:
+                        append_request_env(c, op[1]);
+                        break;
+                case CL_OP_RESPONSE:
+                        /* headers we found already got swapped for literals */
+                        rb_str_buf_append(dst, g_dash);
+                        break;
+                case CL_OP_SPECIAL:
+                        special_var(c, NUM2INT(op[1]));
+                        break;
+                case CL_OP_EVAL:
+                        append_eval(c, op[1]);
+                        break;
+                case CL_OP_TIME_LOCAL:
+                case CL_OP_TIME_UTC:
+                        append_time(c, opcode, op[1]);
+                        break;
+                case CL_OP_REQUEST_TIME:
+                        append_request_time_fmt(c, op);
+                        break;
+                case CL_OP_TIME:
+                        append_time_fmt(c, op);
+                        break;
+                case CL_OP_COOKIE:
+                        append_cookie(c, op[1]);
+                        break;
+                }
+        }
+
+        if (c->fd >= 0) {
+                write_full(c->fd, RSTRING_PTR(dst), RSTRING_LEN(dst));
+        } else {
+                VALUE logger = c->logger;
+
+                if (NIL_P(logger))
+                        logger = rb_hash_aref(c->env, g_rack_errors);
+                rb_funcall(logger, ltlt_id, 1, dst);
+        }
+
+        return Qnil;
+}
+
+/**
+ * call-seq:
+ *   Clogger.new(app, :logger => $stderr, :format => string) => obj
+ *
+ * Creates a new Clogger object that wraps +app+.  +:logger+ may
+ * be any object that responds to the "<<" method with a string argument.
+ */
+static VALUE clogger_init(int argc, VALUE *argv, VALUE self)
+{
+        struct clogger *c = clogger_get(self);
+        VALUE o = Qnil;
+        VALUE fmt = rb_const_get(mFormat, rb_intern("Common"));
+
+        rb_scan_args(argc, argv, "11", &c->app, &o);
+        c->fd = -1;
+        c->logger = Qnil;
+        c->reentrant = -1; /* auto-detect */
+
+        if (TYPE(o) == T_HASH) {
+                VALUE tmp;
+
+                c->logger = rb_hash_aref(o, ID2SYM(rb_intern("logger")));
+                if (!NIL_P(c->logger))
+                        c->fd = raw_fd(rb_rescue(obj_fileno, c->logger, 0, 0));
+
+                tmp = rb_hash_aref(o, ID2SYM(rb_intern("format")));
+                if (!NIL_P(tmp))
+                        fmt = tmp;
+        }
+
+        init_buffers(c);
+        c->fmt_ops = rb_funcall(self, rb_intern("compile_format"), 1, fmt);
+
+        if (Qtrue == rb_funcall(self, rb_intern("need_response_headers?"),
+                                1, c->fmt_ops))
+                c->need_resp = 1;
+        if (Qtrue == rb_funcall(self, rb_intern("need_wrap_body?"),
+                                1, c->fmt_ops))
+                c->wrap_body = 1;
+
+        return self;
+}
+
+static VALUE body_iter_i(VALUE str, VALUE memop)
+{
+        off_t *len = (off_t *)memop;
+
+        *len += RSTRING_LEN(str);
+
+        return rb_yield(str);
+}
+
+static VALUE wrap_each(struct clogger *c)
+{
+        VALUE body = RARRAY_PTR(c->response)[2];
+
+        rb_need_block();
+        c->body_bytes_sent = 0;
+        rb_iterate(rb_each, body, body_iter_i, (VALUE)&c->body_bytes_sent);
+
+        return body;
+}
+
+/**
+ * call-seq:
+ *   clogger.each { |part| socket.write(part) }
+ *
+ * Delegates the body#each call to the underlying +body+ object
+ * while tracking the number of bytes yielded.  This will log
+ * the request.
+ */
+static VALUE clogger_each(VALUE self)
+{
+        struct clogger *c = clogger_get(self);
+
+        return rb_ensure(wrap_each, (VALUE)c, cwrite, (VALUE)c);
+}
+
+/**
+ * call-seq:
+ *   clogger.close
+ *
+ * Delegates the body#close call to the underlying +body+ object.
+ * This is only used when Clogger is wrapping the +body+ of a Rack
+ * response and should be automatically called by the web server.
+ */
+static VALUE clogger_close(VALUE self)
+{
+        struct clogger *c = clogger_get(self);
+
+        return rb_funcall(RARRAY_PTR(c->response)[2], close_id, 0);
+}
+
+/* :nodoc: */
+static VALUE clogger_fileno(VALUE self)
+{
+        struct clogger *c = clogger_get(self);
+
+        return c->fd < 0 ? Qnil : INT2NUM(c->fd);
+}
+
+static void ccall(struct clogger *c, VALUE env)
+{
+        gettimeofday(&c->tv_start, NULL);
+        c->env = env;
+        c->cookies = Qfalse;
+        c->response = rb_funcall(c->app, call_id, 1, env);
+}
+
+/*
+ * call-seq:
+ *   clogger.call(env) => [ status, headers, body ]
+ *
+ * calls the wrapped Rack application with +env+, returns the
+ * [status, headers, body ] tuplet required by Rack.
+ */
+static VALUE clogger_call(VALUE self, VALUE env)
+{
+        struct clogger *c = clogger_get(self);
+
+        if (c->wrap_body) {
+                VALUE tmp;
+
+                if (c->reentrant < 0) {
+                        tmp = rb_hash_aref(env, g_rack_multithread);
+                        c->reentrant = Qfalse == tmp ? 0 : 1;
+                }
+                if (c->reentrant) {
+                        self = rb_obj_dup(self);
+                        c = clogger_get(self);
+                }
+
+                ccall(c, env);
+                tmp = rb_ary_dup(c->response);
+                rb_ary_store(tmp, 2, self);
+                return tmp;
+        }
+
+        ccall(c, env);
+        cwrite(c);
+
+        return c->response;
+}
+
+/* :nodoc */
+static VALUE clogger_init_copy(VALUE clone, VALUE orig)
+{
+        struct clogger *a = clogger_get(orig);
+        struct clogger *b = clogger_get(clone);
+
+        memcpy(b, a, sizeof(struct clogger));
+        init_buffers(b);
+
+        return clone;
+}
+
+#define CONST_GLOBAL_STR2(var, val) do { \
+        g_##var = rb_obj_freeze(rb_str_new(val, sizeof(val) - 1)); \
+        rb_global_variable(&g_##var); \
+} while (0)
+
+#define CONST_GLOBAL_STR(val) CONST_GLOBAL_STR2(val, #val)
+
+void Init_clogger_ext(void)
+{
+        ltlt_id = rb_intern("<<");
+        call_id = rb_intern("call");
+        each_id = rb_intern("each");
+        close_id = rb_intern("close");
+        to_i_id = rb_intern("to_i");
+        to_s_id = rb_intern("to_s");
+        size_id = rb_intern("size");
+        cClogger = rb_define_class("Clogger", rb_cObject);
+        mFormat = rb_define_module_under(cClogger, "Format");
+        rb_define_alloc_func(cClogger, clogger_alloc);
+        rb_define_method(cClogger, "initialize", clogger_init, -1);
+        rb_define_method(cClogger, "initialize_copy", clogger_init_copy, 1);
+        rb_define_method(cClogger, "call", clogger_call, 1);
+        rb_define_method(cClogger, "each", clogger_each, 0);
+        rb_define_method(cClogger, "close", clogger_close, 0);
+        rb_define_method(cClogger, "fileno", clogger_fileno, 0);
+        rb_define_method(cClogger, "wrap_body?", clogger_wrap_body, 0);
+        rb_define_method(cClogger, "reentrant?", clogger_reentrant, 0);
+        CONST_GLOBAL_STR(REMOTE_ADDR);
+        CONST_GLOBAL_STR(HTTP_X_FORWARDED_FOR);
+        CONST_GLOBAL_STR(REQUEST_METHOD);
+        CONST_GLOBAL_STR(PATH_INFO);
+        CONST_GLOBAL_STR(QUERY_STRING);
+        CONST_GLOBAL_STR(HTTP_VERSION);
+        CONST_GLOBAL_STR2(rack_errors, "rack.errors");
+        CONST_GLOBAL_STR2(rack_input, "rack.input");
+        CONST_GLOBAL_STR2(rack_multithread, "rack.multithread");
+        CONST_GLOBAL_STR2(dash, "-");
+        CONST_GLOBAL_STR2(empty, "");
+        CONST_GLOBAL_STR2(space, " ");
+        CONST_GLOBAL_STR2(question_mark, "?");
+        CONST_GLOBAL_STR2(rack_request_cookie_hash, "rack.request.cookie_hash");
+}
diff --git a/ext/clogger_ext/extconf.rb b/ext/clogger_ext/extconf.rb
new file mode 100644
index 0000000..8cc8b5e
--- /dev/null
+++ b/ext/clogger_ext/extconf.rb
@@ -0,0 +1,12 @@
+require 'mkmf'
+
+if have_header('fcntl.h')
+  have_macro('F_GETFL', %w(fcntl.h))
+  have_macro('O_NONBLOCK', %w(unistd.h fcntl.h))
+end
+
+have_func('localtime_r', 'time.h') or abort "localtime_r needed"
+have_func('gmtime_r', 'time.h') or abort "gmtime_r needed"
+have_func('rb_str_set_len', 'ruby.h')
+dir_config('clogger_ext')
+create_makefile('clogger_ext')
diff --git a/ext/clogger_ext/ruby_1_9_compat.h b/ext/clogger_ext/ruby_1_9_compat.h
new file mode 100644
index 0000000..e0ba4ac
--- /dev/null
+++ b/ext/clogger_ext/ruby_1_9_compat.h
@@ -0,0 +1,23 @@
+/* Ruby 1.8.6+ macros (for compatibility with Ruby 1.9) */
+#ifndef RSTRING_PTR
+#  define RSTRING_PTR(s) (RSTRING(s)->ptr)
+#endif
+#ifndef RSTRING_LEN
+#  define RSTRING_LEN(s) (RSTRING(s)->len)
+#endif
+#ifndef RARRAY_PTR
+#  define RARRAY_PTR(s) (RARRAY(s)->ptr)
+#endif
+#ifndef RARRAY_LEN
+#  define RARRAY_LEN(s) (RARRAY(s)->len)
+#endif
+
+#ifndef HAVE_RB_STR_SET_LEN
+/* this is taken from Ruby 1.8.7, 1.8.6 may not have it */
+static void rb_18_str_set_len(VALUE str, long len)
+{
+        RSTRING(str)->len = len;
+        RSTRING(str)->ptr[len] = '\0';
+}
+#define rb_str_set_len(str,len) rb_18_str_set_len(str,len)
+#endif
diff --git a/lib/clogger.rb b/lib/clogger.rb
new file mode 100644
index 0000000..0e4148e
--- /dev/null
+++ b/lib/clogger.rb
@@ -0,0 +1,133 @@
+# -*- encoding: binary -*-
+class Clogger
+  VERSION = '0.0.1'
+
+  OP_LITERAL = 0
+  OP_REQUEST = 1
+  OP_RESPONSE = 2
+  OP_SPECIAL = 3
+  OP_EVAL = 4
+  OP_TIME_LOCAL = 5
+  OP_TIME_UTC = 6
+  OP_REQUEST_TIME = 7
+  OP_TIME = 8
+  OP_COOKIE = 9
+
+  # support nginx variables that are less customizable than our own
+  ALIASES = {
+    '$request_time' => '$request_time{3}',
+    '$time_local' => '$time_local{%d/%b/%Y:%H:%M:%S %z}',
+    '$msec' => '$time{3}',
+    '$usec' => '$time{6}',
+  }
+
+  SPECIAL_VARS = {
+    :body_bytes_sent => 0,
+    :status => 1,
+    :request => 2, # REQUEST_METHOD PATH_INFO?QUERY_STRING HTTP_VERSION
+    :request_length => 3, # env['rack.input'].size
+    :response_length => 4, # like body_bytes_sent, except "-" instead of "0"
+    :ip => 5, # HTTP_X_FORWARDED_FOR || REMOTE_ADDR || -
+    :pid => 6, # getpid()
+  }
+
+private
+
+  CGI_ENV = Regexp.new('\A\$(' <<
+      %w(remote_addr remote_ident remote_user
+         path_info query_string script_name
+         server_name server_port).join('|') << ')\z').freeze
+
+  SCAN = /([^$]*)(\$+(?:env\{\w+(?:\.[\w\.]+)?\}|
+                        e\{[^\}]+\}|
+                        (?:request_)?time\{\d+\}|
+                        time_(?:utc|local)\{[^\}]+\}|
+                        \w*))?([^$]*)/x
+
+  def compile_format(str)
+    rv = []
+    str.scan(SCAN).each do |pre,tok,post|
+      rv << [ OP_LITERAL, pre ] if pre && pre != ""
+
+      unless tok.nil?
+        if tok.sub!(/\A(\$+)\$/, '$')
+          rv << [ OP_LITERAL, $1.dup ]
+        end
+
+        compat = ALIASES[tok] and tok = compat
+
+        case tok
+        when /\A(\$*)\z/
+          rv << [ OP_LITERAL, $1.dup ]
+        when /\A\$env\{(\w+(?:\.[\w\.]+))\}\z/
+          rv << [ OP_REQUEST, $1.freeze ]
+        when /\A\$e\{([^\}]+)\}\z/
+          rv << [ OP_EVAL, $1.dup ]
+        when /\A\$cookie_(\w+)\z/
+          rv << [ OP_COOKIE, $1.dup.freeze ]
+        when CGI_ENV, /\A\$(http_\w+)\z/
+          rv << [ OP_REQUEST, $1.upcase.freeze ]
+        when /\A\$sent_http_(\w+)\z/
+          rv << [ OP_RESPONSE, $1.downcase.tr('_','-').freeze ]
+        when /\A\$time_local\{([^\}]+)\}\z/
+          rv << [ OP_TIME_LOCAL, $1.dup ]
+        when /\A\$time_utc\{([^\}]+)\}\z/
+          rv << [ OP_TIME_UTC, $1.dup ]
+        when /\A\$time\{(\d+)\}\z/
+          rv << [ OP_TIME, *usec_conv_pair(tok, $1.to_i) ]
+        when /\A\$request_time\{(\d+)\}\z/
+          rv << [ OP_REQUEST_TIME, *usec_conv_pair(tok, $1.to_i) ]
+        else
+          tok_sym = tok[1..-1].to_sym
+          if special_code = SPECIAL_VARS[tok_sym]
+            rv << [ OP_SPECIAL, special_code ]
+          else
+            raise ArgumentError, "unable to make sense of token: #{tok}"
+          end
+        end
+      end
+
+      rv << [ OP_LITERAL, post ] if post && post != ""
+    end
+
+    # auto-append a newline
+    last = rv.last or return rv
+    op = last.first
+    if (op == OP_LITERAL && /\n\z/ !~ last.last) || op != OP_LITERAL
+      rv << [ OP_LITERAL, "\n" ]
+    end
+
+    rv
+  end
+
+  def usec_conv_pair(tok, prec)
+    if prec == 0
+      [ "%d", 1 ] # stupid...
+    elsif prec > 6
+      raise ArgumentError, "#{tok}: too high precision: #{prec} (max=6)"
+    else
+      [ "%d.%0#{prec}d", 10 ** (6 - prec) ]
+    end
+  end
+
+  def need_response_headers?(fmt_ops)
+    fmt_ops.any? { |op| OP_RESPONSE == op[0] }
+  end
+
+  def need_wrap_body?(fmt_ops)
+    fmt_ops.any? do |op|
+      (OP_REQUEST_TIME == op[0]) || (OP_SPECIAL == op[0] &&
+        (SPECIAL_VARS[:body_bytes_sent] == op[1] ||
+         SPECIAL_VARS[:response_length] == op[1]))
+    end
+  end
+
+end
+
+require 'clogger/format'
+
+begin
+  require 'clogger_ext'
+rescue LoadError
+  require 'clogger/pure'
+end
diff --git a/lib/clogger/format.rb b/lib/clogger/format.rb
new file mode 100644
index 0000000..9e4f59f
--- /dev/null
+++ b/lib/clogger/format.rb
@@ -0,0 +1,25 @@
+# -*- encoding: binary -*-
+
+class Clogger
+
+  # predefined log formats in wide use
+  module Format
+    # common log format used by Apache:
+    # http://httpd.apache.org/docs/2.2/logs.html
+    Common = "$remote_addr - $remote_user [$time_local] " \
+             '"$request" $status $response_length'.freeze
+
+    # combined log format used by Apache:
+    # http://httpd.apache.org/docs/2.2/logs.html
+    Combined = %Q|#{Common} "$http_referer" "$http_user_agent"|.freeze
+
+    # combined log format used by nginx:
+    # http://wiki.nginx.org/NginxHttpLogModule
+    NginxCombined = Combined.gsub(/response_length/, 'body_bytes_sent').freeze
+
+    # log format used by Rack 1.0
+    Rack_1_0 = "$ip - $remote_user [$time_local{%d/%b/%Y %H:%M:%S}] " \
+               '"$request" $status $response_length $request_time{4}'.freeze
+  end
+
+end
diff --git a/lib/clogger/pure.rb b/lib/clogger/pure.rb
new file mode 100644
index 0000000..11c03f4
--- /dev/null
+++ b/lib/clogger/pure.rb
@@ -0,0 +1,126 @@
+# -*- encoding: binary -*-
+# :stopdoc:
+#
+# Not at all optimized for performance, this was written based on
+# the original C extension code so it's not very Ruby-ish...
+class Clogger
+
+  def initialize(app, opts = {})
+    @app = app
+    @logger = opts[:logger]
+    @fmt_ops = compile_format(opts[:format] || Format::Common)
+    @wrap_body = need_wrap_body?(@fmt_ops)
+    @reentrant = nil
+    @body_bytes_sent = 0
+  end
+
+  def call(env)
+    @start = Time.now
+    status, headers, body = @app.call(env)
+    if wrap_body?
+      @reentrant = env['rack.multithread']
+      @env, @status, @headers, @body = env, status, headers, body
+      return [ status, headers, reentrant? ? self.dup : self ]
+    end
+    log(env, status, headers)
+    [ status, headers, body ]
+  end
+
+  def each
+    @body_bytes_sent = 0
+    @body.each do |part|
+      @body_bytes_sent += part.size
+      yield part
+    end
+    ensure
+      log(@env, @status, @headers)
+  end
+
+  def close
+    @body.close
+  end
+
+  def reentrant?
+    @reentrant
+  end
+
+  def wrap_body?
+    @wrap_body
+  end
+
+  def fileno
+    @logger.fileno rescue nil
+  end
+
+private
+
+  def byte_xs(s)
+    s = s.dup
+    s.force_encoding(Encoding::BINARY) if defined?(Encoding::BINARY)
+    s.gsub!(/(['"\x00-\x1f])/) { |x| "\\x#{$1.unpack('H2').first}" }
+    s
+  end
+
+  SPECIAL_RMAP = SPECIAL_VARS.inject([]) { |ary, (k,v)| ary[v] = k; ary }
+
+  def special_var(special_nr, env, status, headers)
+    case SPECIAL_RMAP[special_nr]
+    when :body_bytes_sent
+      @body_bytes_sent.to_s
+    when :status
+      status = status.to_i
+      status >= 100 && status <= 999 ? ('%03d' % status) : '-'
+    when :request
+      qs = env['QUERY_STRING']
+      qs.empty? or qs = "?#{byte_xs(qs)}"
+      "#{env['REQUEST_METHOD']} " \
+        "#{byte_xs(env['PATH_INFO'])}#{qs} " \
+        "#{byte_xs(env['HTTP_VERSION'])}"
+    when :request_length
+      env['rack.input'].size.to_s
+    when :response_length
+      @body_bytes_sent == 0 ? '-' : @body_bytes_sent.to_s
+    when :ip
+      xff = env['HTTP_X_FORWARDED_FOR'] and return byte_xs(xff)
+      env['REMOTE_ADDR'] || '-'
+    when :pid
+      $$.to_s
+    else
+      raise "EDOOFUS #{special_nr}"
+    end
+  end
+
+  def time_format(sec, usec, format, div)
+    format % [ sec, usec / div ]
+  end
+
+  def log(env, status, headers)
+    (@logger || env['rack.errors']) << @fmt_ops.map { |op|
+      case op[0]
+      when OP_LITERAL; op[1]
+      when OP_REQUEST; byte_xs(env[op[1]] || "-")
+      when OP_RESPONSE; byte_xs(get_sent_header(headers, op[1]))
+      when OP_SPECIAL; special_var(op[1], env, status, headers)
+      when OP_EVAL; eval(op[1]).to_s rescue "-"
+      when OP_TIME_LOCAL; Time.now.strftime(op[1])
+      when OP_TIME_UTC; Time.now.utc.strftime(op[1])
+      when OP_REQUEST_TIME
+        t = Time.now - @start
+        time_format(t.to_i, (t - t.to_i) * 1000000, op[1], op[2])
+      when OP_TIME
+        t = Time.now
+        time_format(t.sec, t.usec, op[1], op[2])
+      when OP_COOKIE
+        (env['rack.request.cookie_hash'][op[1]] rescue "-") || "-"
+      else
+        raise "EDOOFUS #{op.inspect}"
+      end
+    }.join('')
+  end
+
+  def get_sent_header(headers, match)
+    headers.each { |key, value| match == key.downcase and return value }
+    "-"
+  end
+
+end
diff --git a/setup.rb b/setup.rb
new file mode 100644
index 0000000..9f0c826
--- /dev/null
+++ b/setup.rb
@@ -0,0 +1,1585 @@
+#
+# setup.rb
+#
+# Copyright (c) 2000-2005 Minero Aoki
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU LGPL, Lesser General Public License version 2.1.
+#
+
+unless Enumerable.method_defined?(:map)   # Ruby 1.4.6
+  module Enumerable
+    alias map collect
+  end
+end
+
+unless File.respond_to?(:read)   # Ruby 1.6
+  def File.read(fname)
+    open(fname) {|f|
+      return f.read
+    }
+  end
+end
+
+unless Errno.const_defined?(:ENOTEMPTY)   # Windows?
+  module Errno
+    class ENOTEMPTY
+      # We do not raise this exception, implementation is not needed.
+    end
+  end
+end
+
+def File.binread(fname)
+  open(fname, 'rb') {|f|
+    return f.read
+  }
+end
+
+# for corrupted Windows' stat(2)
+def File.dir?(path)
+  File.directory?((path[-1,1] == '/') ? path : path + '/')
+end
+
+
+class ConfigTable
+
+  include Enumerable
+
+  def initialize(rbconfig)
+    @rbconfig = rbconfig
+    @items = []
+    @table = {}
+    # options
+    @install_prefix = nil
+    @config_opt = nil
+    @verbose = true
+    @no_harm = false
+  end
+
+  attr_accessor :install_prefix
+  attr_accessor :config_opt
+
+  attr_writer :verbose
+
+  def verbose?
+    @verbose
+  end
+
+  attr_writer :no_harm
+
+  def no_harm?
+    @no_harm
+  end
+
+  def [](key)
+    lookup(key).resolve(self)
+  end
+
+  def []=(key, val)
+    lookup(key).set val
+  end
+
+  def names
+    @items.map {|i| i.name }
+  end
+
+  def each(&block)
+    @items.each(&block)
+  end
+
+  def key?(name)
+    @table.key?(name)
+  end
+
+  def lookup(name)
+    @table[name] or setup_rb_error "no such config item: #{name}"
+  end
+
+  def add(item)
+    @items.push item
+    @table[item.name] = item
+  end
+
+  def remove(name)
+    item = lookup(name)
+    @items.delete_if {|i| i.name == name }
+    @table.delete_if {|name, i| i.name == name }
+    item
+  end
+
+  def load_script(path, inst = nil)
+    if File.file?(path)
+      MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path
+    end
+  end
+
+  def savefile
+    '.config'
+  end
+
+  def load_savefile
+    begin
+      File.foreach(savefile()) do |line|
+        k, v = *line.split(/=/, 2)
+        self[k] = v.strip
+      end
+    rescue Errno::ENOENT
+      setup_rb_error $!.message + "\n#{File.basename($0)} config first"
+    end
+  end
+
+  def save
+    @items.each {|i| i.value }
+    File.open(savefile(), 'w') {|f|
+      @items.each do |i|
+        f.printf "%s=%s\n", i.name, i.value if i.value? and i.value
+      end
+    }
+  end
+
+  def load_standard_entries
+    standard_entries(@rbconfig).each do |ent|
+      add ent
+    end
+  end
+
+  def standard_entries(rbconfig)
+    c = rbconfig
+
+    rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT'])
+
+    major = c['MAJOR'].to_i
+    minor = c['MINOR'].to_i
+    teeny = c['TEENY'].to_i
+    version = "#{major}.#{minor}"
+
+    # ruby ver. >= 1.4.4?
+    newpath_p = ((major >= 2) or
+                 ((major == 1) and
+                  ((minor >= 5) or
+                   ((minor == 4) and (teeny >= 4)))))
+
+    if c['rubylibdir']
+      # V > 1.6.3
+      libruby         = "#{c['prefix']}/lib/ruby"
+      librubyver      = c['rubylibdir']
+      librubyverarch  = c['archdir']
+      siteruby        = c['sitedir']
+      siterubyver     = c['sitelibdir']
+      siterubyverarch = c['sitearchdir']
+    elsif newpath_p
+      # 1.4.4 <= V <= 1.6.3
+      libruby         = "#{c['prefix']}/lib/ruby"
+      librubyver      = "#{c['prefix']}/lib/ruby/#{version}"
+      librubyverarch  = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
+      siteruby        = c['sitedir']
+      siterubyver     = "$siteruby/#{version}"
+      siterubyverarch = "$siterubyver/#{c['arch']}"
+    else
+      # V < 1.4.4
+      libruby         = "#{c['prefix']}/lib/ruby"
+      librubyver      = "#{c['prefix']}/lib/ruby/#{version}"
+      librubyverarch  = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
+      siteruby        = "#{c['prefix']}/lib/ruby/#{version}/site_ruby"
+      siterubyver     = siteruby
+      siterubyverarch = "$siterubyver/#{c['arch']}"
+    end
+    parameterize = lambda {|path|
+      path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')
+    }
+
+    if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg }
+      makeprog = arg.sub(/'/, '').split(/=/, 2)[1]
+    else
+      makeprog = 'make'
+    end
+
+    [
+      ExecItem.new('installdirs', 'std/site/home',
+                   'std: install under libruby; site: install under site_ruby; home: install under $HOME')\
+          {|val, table|
+            case val
+            when 'std'
+              table['rbdir'] = '$librubyver'
+              table['sodir'] = '$librubyverarch'
+            when 'site'
+              table['rbdir'] = '$siterubyver'
+              table['sodir'] = '$siterubyverarch'
+            when 'home'
+              setup_rb_error '$HOME was not set' unless ENV['HOME']
+              table['prefix'] = ENV['HOME']
+              table['rbdir'] = '$libdir/ruby'
+              table['sodir'] = '$libdir/ruby'
+            end
+          },
+      PathItem.new('prefix', 'path', c['prefix'],
+                   'path prefix of target environment'),
+      PathItem.new('bindir', 'path', parameterize.call(c['bindir']),
+                   'the directory for commands'),
+      PathItem.new('libdir', 'path', parameterize.call(c['libdir']),
+                   'the directory for libraries'),
+      PathItem.new('datadir', 'path', parameterize.call(c['datadir']),
+                   'the directory for shared data'),
+      PathItem.new('mandir', 'path', parameterize.call(c['mandir']),
+                   'the directory for man pages'),
+      PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']),
+                   'the directory for system configuration files'),
+      PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']),
+                   'the directory for local state data'),
+      PathItem.new('libruby', 'path', libruby,
+                   'the directory for ruby libraries'),
+      PathItem.new('librubyver', 'path', librubyver,
+                   'the directory for standard ruby libraries'),
+      PathItem.new('librubyverarch', 'path', librubyverarch,
+                   'the directory for standard ruby extensions'),
+      PathItem.new('siteruby', 'path', siteruby,
+          'the directory for version-independent aux ruby libraries'),
+      PathItem.new('siterubyver', 'path', siterubyver,
+                   'the directory for aux ruby libraries'),
+      PathItem.new('siterubyverarch', 'path', siterubyverarch,
+                   'the directory for aux ruby binaries'),
+      PathItem.new('rbdir', 'path', '$siterubyver',
+                   'the directory for ruby scripts'),
+      PathItem.new('sodir', 'path', '$siterubyverarch',
+                   'the directory for ruby extentions'),
+      PathItem.new('rubypath', 'path', rubypath,
+                   'the path to set to #! line'),
+      ProgramItem.new('rubyprog', 'name', rubypath,
+                      'the ruby program using for installation'),
+      ProgramItem.new('makeprog', 'name', makeprog,
+                      'the make program to compile ruby extentions'),
+      SelectItem.new('shebang', 'all/ruby/never', 'ruby',
+                     'shebang line (#!) editing mode'),
+      BoolItem.new('without-ext', 'yes/no', 'no',
+                   'does not compile/install ruby extentions')
+    ]
+  end
+  private :standard_entries
+
+  def load_multipackage_entries
+    multipackage_entries().each do |ent|
+      add ent
+    end
+  end
+
+  def multipackage_entries
+    [
+      PackageSelectionItem.new('with', 'name,name...', '', 'ALL',
+                               'package names that you want to install'),
+      PackageSelectionItem.new('without', 'name,name...', '', 'NONE',
+                               'package names that you do not want to install')
+    ]
+  end
+  private :multipackage_entries
+
+  ALIASES = {
+    'std-ruby'         => 'librubyver',
+    'stdruby'          => 'librubyver',
+    'rubylibdir'       => 'librubyver',
+    'archdir'          => 'librubyverarch',
+    'site-ruby-common' => 'siteruby',     # For backward compatibility
+    'site-ruby'        => 'siterubyver',  # For backward compatibility
+    'bin-dir'          => 'bindir',
+    'bin-dir'          => 'bindir',
+    'rb-dir'           => 'rbdir',
+    'so-dir'           => 'sodir',
+    'data-dir'         => 'datadir',
+    'ruby-path'        => 'rubypath',
+    'ruby-prog'        => 'rubyprog',
+    'ruby'             => 'rubyprog',
+    'make-prog'        => 'makeprog',
+    'make'             => 'makeprog'
+  }
+
+  def fixup
+    ALIASES.each do |ali, name|
+      @table[ali] = @table[name]
+    end
+    @items.freeze
+    @table.freeze
+    @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/
+  end
+
+  def parse_opt(opt)
+    m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}"
+    m.to_a[1,2]
+  end
+
+  def dllext
+    @rbconfig['DLEXT']
+  end
+
+  def value_config?(name)
+    lookup(name).value?
+  end
+
+  class Item
+    def initialize(name, template, default, desc)
+      @name = name.freeze
+      @template = template
+      @value = default
+      @default = default
+      @description = desc
+    end
+
+    attr_reader :name
+    attr_reader :description
+
+    attr_accessor :default
+    alias help_default default
+
+    def help_opt
+      "--#{@name}=#{@template}"
+    end
+
+    def value?
+      true
+    end
+
+    def value
+      @value
+    end
+
+    def resolve(table)
+      @value.gsub(%r<\$([^/]+)>) { table[$1] }
+    end
+
+    def set(val)
+      @value = check(val)
+    end
+
+    private
+
+    def check(val)
+      setup_rb_error "config: --#{name} requires argument" unless val
+      val
+    end
+  end
+
+  class BoolItem < Item
+    def config_type
+      'bool'
+    end
+
+    def help_opt
+      "--#{@name}"
+    end
+
+    private
+
+    def check(val)
+      return 'yes' unless val
+      case val
+      when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes'
+      when /\An(o)?\z/i, /\Af(alse)\z/i  then 'no'
+      else
+        setup_rb_error "config: --#{@name} accepts only yes/no for argument"
+      end
+    end
+  end
+
+  class PathItem < Item
+    def config_type
+      'path'
+    end
+
+    private
+
+    def check(path)
+      setup_rb_error "config: --#{@name} requires argument"  unless path
+      path[0,1] == '$' ? path : File.expand_path(path)
+    end
+  end
+
+  class ProgramItem < Item
+    def config_type
+      'program'
+    end
+  end
+
+  class SelectItem < Item
+    def initialize(name, selection, default, desc)
+      super
+      @ok = selection.split('/')
+    end
+
+    def config_type
+      'select'
+    end
+
+    private
+
+    def check(val)
+      unless @ok.include?(val.strip)
+        setup_rb_error "config: use --#{@name}=#{@template} (#{val})"
+      end
+      val.strip
+    end
+  end
+
+  class ExecItem < Item
+    def initialize(name, selection, desc, &block)
+      super name, selection, nil, desc
+      @ok = selection.split('/')
+      @action = block
+    end
+
+    def config_type
+      'exec'
+    end
+
+    def value?
+      false
+    end
+
+    def resolve(table)
+      setup_rb_error "$#{name()} wrongly used as option value"
+    end
+
+    undef set
+
+    def evaluate(val, table)
+      v = val.strip.downcase
+      unless @ok.include?(v)
+        setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})"
+      end
+      @action.call v, table
+    end
+  end
+
+  class PackageSelectionItem < Item
+    def initialize(name, template, default, help_default, desc)
+      super name, template, default, desc
+      @help_default = help_default
+    end
+
+    attr_reader :help_default
+
+    def config_type
+      'package'
+    end
+
+    private
+
+    def check(val)
+      unless File.dir?("packages/#{val}")
+        setup_rb_error "config: no such package: #{val}"
+      end
+      val
+    end
+  end
+
+  class MetaConfigEnvironment
+    def initialize(config, installer)
+      @config = config
+      @installer = installer
+    end
+
+    def config_names
+      @config.names
+    end
+
+    def config?(name)
+      @config.key?(name)
+    end
+
+    def bool_config?(name)
+      @config.lookup(name).config_type == 'bool'
+    end
+
+    def path_config?(name)
+      @config.lookup(name).config_type == 'path'
+    end
+
+    def value_config?(name)
+      @config.lookup(name).config_type != 'exec'
+    end
+
+    def add_config(item)
+      @config.add item
+    end
+
+    def add_bool_config(name, default, desc)
+      @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
+    end
+
+    def add_path_config(name, default, desc)
+      @config.add PathItem.new(name, 'path', default, desc)
+    end
+
+    def set_config_default(name, default)
+      @config.lookup(name).default = default
+    end
+
+    def remove_config(name)
+      @config.remove(name)
+    end
+
+    # For only multipackage
+    def packages
+      raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer
+      @installer.packages
+    end
+
+    # For only multipackage
+    def declare_packages(list)
+      raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer
+      @installer.packages = list
+    end
+  end
+
+end   # class ConfigTable
+
+
+# This module requires: #verbose?, #no_harm?
+module FileOperations
+
+  def mkdir_p(dirname, prefix = nil)
+    dirname = prefix + File.expand_path(dirname) if prefix
+    $stderr.puts "mkdir -p #{dirname}" if verbose?
+    return if no_harm?
+
+    # Does not check '/', it's too abnormal.
+    dirs = File.expand_path(dirname).split(%r<(?=/)>)
+    if /\A[a-z]:\z/i =~ dirs[0]
+      disk = dirs.shift
+      dirs[0] = disk + dirs[0]
+    end
+    dirs.each_index do |idx|
+      path = dirs[0..idx].join('')
+      Dir.mkdir path unless File.dir?(path)
+    end
+  end
+
+  def rm_f(path)
+    $stderr.puts "rm -f #{path}" if verbose?
+    return if no_harm?
+    force_remove_file path
+  end
+
+  def rm_rf(path)
+    $stderr.puts "rm -rf #{path}" if verbose?
+    return if no_harm?
+    remove_tree path
+  end
+
+  def remove_tree(path)
+    if File.symlink?(path)
+      remove_file path
+    elsif File.dir?(path)
+      remove_tree0 path
+    else
+      force_remove_file path
+    end
+  end
+
+  def remove_tree0(path)
+    Dir.foreach(path) do |ent|
+      next if ent == '.'
+      next if ent == '..'
+      entpath = "#{path}/#{ent}"
+      if File.symlink?(entpath)
+        remove_file entpath
+      elsif File.dir?(entpath)
+        remove_tree0 entpath
+      else
+        force_remove_file entpath
+      end
+    end
+    begin
+      Dir.rmdir path
+    rescue Errno::ENOTEMPTY
+      # directory may not be empty
+    end
+  end
+
+  def move_file(src, dest)
+    force_remove_file dest
+    begin
+      File.rename src, dest
+    rescue
+      File.open(dest, 'wb') {|f|
+        f.write File.binread(src)
+      }
+      File.chmod File.stat(src).mode, dest
+      File.unlink src
+    end
+  end
+
+  def force_remove_file(path)
+    begin
+      remove_file path
+    rescue
+    end
+  end
+
+  def remove_file(path)
+    File.chmod 0777, path
+    File.unlink path
+  end
+
+  def install(from, dest, mode, prefix = nil)
+    $stderr.puts "install #{from} #{dest}" if verbose?
+    return if no_harm?
+
+    realdest = prefix ? prefix + File.expand_path(dest) : dest
+    realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest)
+    str = File.binread(from)
+    if diff?(str, realdest)
+      verbose_off {
+        rm_f realdest if File.exist?(realdest)
+      }
+      File.open(realdest, 'wb') {|f|
+        f.write str
+      }
+      File.chmod mode, realdest
+
+      File.open("#{objdir_root()}/InstalledFiles", 'a') {|f|
+        if prefix
+          f.puts realdest.sub(prefix, '')
+        else
+          f.puts realdest
+        end
+      }
+    end
+  end
+
+  def diff?(new_content, path)
+    return true unless File.exist?(path)
+    new_content != File.binread(path)
+  end
+
+  def command(*args)
+    $stderr.puts args.join(' ') if verbose?
+    system(*args) or raise RuntimeError,
+        "system(#{args.map{|a| a.inspect }.join(' ')}) failed"
+  end
+
+  def ruby(*args)
+    command config('rubyprog'), *args
+  end
+
+  def make(task = nil)
+    command(*[config('makeprog'), task].compact)
+  end
+
+  def extdir?(dir)
+    File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb")
+  end
+
+  def files_of(dir)
+    Dir.open(dir) {|d|
+      return d.select {|ent| File.file?("#{dir}/#{ent}") }
+    }
+  end
+
+  DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn )
+
+  def directories_of(dir)
+    Dir.open(dir) {|d|
+      return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT
+    }
+  end
+
+end
+
+
+# This module requires: #srcdir_root, #objdir_root, #relpath
+module HookScriptAPI
+
+  def get_config(key)
+    @config[key]
+  end
+
+  alias config get_config
+
+  # obsolete: use metaconfig to change configuration
+  def set_config(key, val)
+    @config[key] = val
+  end
+
+  #
+  # srcdir/objdir (works only in the package directory)
+  #
+
+  def curr_srcdir
+    "#{srcdir_root()}/#{relpath()}"
+  end
+
+  def curr_objdir
+    "#{objdir_root()}/#{relpath()}"
+  end
+
+  def srcfile(path)
+    "#{curr_srcdir()}/#{path}"
+  end
+
+  def srcexist?(path)
+    File.exist?(srcfile(path))
+  end
+
+  def srcdirectory?(path)
+    File.dir?(srcfile(path))
+  end
+
+  def srcfile?(path)
+    File.file?(srcfile(path))
+  end
+
+  def srcentries(path = '.')
+    Dir.open("#{curr_srcdir()}/#{path}") {|d|
+      return d.to_a - %w(. ..)
+    }
+  end
+
+  def srcfiles(path = '.')
+    srcentries(path).select {|fname|
+      File.file?(File.join(curr_srcdir(), path, fname))
+    }
+  end
+
+  def srcdirectories(path = '.')
+    srcentries(path).select {|fname|
+      File.dir?(File.join(curr_srcdir(), path, fname))
+    }
+  end
+
+end
+
+
+class ToplevelInstaller
+
+  Version   = '3.4.1'
+  Copyright = 'Copyright (c) 2000-2005 Minero Aoki'
+
+  TASKS = [
+    [ 'all',      'do config, setup, then install' ],
+    [ 'config',   'saves your configurations' ],
+    [ 'show',     'shows current configuration' ],
+    [ 'setup',    'compiles ruby extentions and others' ],
+    [ 'install',  'installs files' ],
+    [ 'test',     'run all tests in test/' ],
+    [ 'clean',    "does `make clean' for each extention" ],
+    [ 'distclean',"does `make distclean' for each extention" ]
+  ]
+
+  def ToplevelInstaller.invoke
+    config = ConfigTable.new(load_rbconfig())
+    config.load_standard_entries
+    config.load_multipackage_entries if multipackage?
+    config.fixup
+    klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller)
+    klass.new(File.dirname($0), config).invoke
+  end
+
+  def ToplevelInstaller.multipackage?
+    File.dir?(File.dirname($0) + '/packages')
+  end
+
+  def ToplevelInstaller.load_rbconfig
+    if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg }
+      ARGV.delete(arg)
+      load File.expand_path(arg.split(/=/, 2)[1])
+      $".push 'rbconfig.rb'
+    else
+      require 'rbconfig'
+    end
+    ::Config::CONFIG
+  end
+
+  def initialize(ardir_root, config)
+    @ardir = File.expand_path(ardir_root)
+    @config = config
+    # cache
+    @valid_task_re = nil
+  end
+
+  def config(key)
+    @config[key]
+  end
+
+  def inspect
+    "#<#{self.class} #{__id__()}>"
+  end
+
+  def invoke
+    run_metaconfigs
+    case task = parsearg_global()
+    when nil, 'all'
+      parsearg_config
+      init_installers
+      exec_config
+      exec_setup
+      exec_install
+    else
+      case task
+      when 'config', 'test'
+        ;
+      when 'clean', 'distclean'
+        @config.load_savefile if File.exist?(@config.savefile)
+      else
+        @config.load_savefile
+      end
+      __send__ "parsearg_#{task}"
+      init_installers
+      __send__ "exec_#{task}"
+    end
+  end
+
+  def run_metaconfigs
+    @config.load_script "#{@ardir}/metaconfig"
+  end
+
+  def init_installers
+    @installer = Installer.new(@config, @ardir, File.expand_path('.'))
+  end
+
+  #
+  # Hook Script API bases
+  #
+
+  def srcdir_root
+    @ardir
+  end
+
+  def objdir_root
+    '.'
+  end
+
+  def relpath
+    '.'
+  end
+
+  #
+  # Option Parsing
+  #
+
+  def parsearg_global
+    while arg = ARGV.shift
+      case arg
+      when /\A\w+\z/
+        setup_rb_error "invalid task: #{arg}" unless valid_task?(arg)
+        return arg
+      when '-q', '--quiet'
+        @config.verbose = false
+      when '--verbose'
+        @config.verbose = true
+      when '--help'
+        print_usage $stdout
+        exit 0
+      when '--version'
+        puts "#{File.basename($0)} version #{Version}"
+        exit 0
+      when '--copyright'
+        puts Copyright
+        exit 0
+      else
+        setup_rb_error "unknown global option '#{arg}'"
+      end
+    end
+    nil
+  end
+
+  def valid_task?(t)
+    valid_task_re() =~ t
+  end
+
+  def valid_task_re
+    @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/
+  end
+
+  def parsearg_no_options
+    unless ARGV.empty?
+      task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1)
+      setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}"
+    end
+  end
+
+  alias parsearg_show       parsearg_no_options
+  alias parsearg_setup      parsearg_no_options
+  alias parsearg_test       parsearg_no_options
+  alias parsearg_clean      parsearg_no_options
+  alias parsearg_distclean  parsearg_no_options
+
+  def parsearg_config
+    evalopt = []
+    set = []
+    @config.config_opt = []
+    while i = ARGV.shift
+      if /\A--?\z/ =~ i
+        @config.config_opt = ARGV.dup
+        break
+      end
+      name, value = *@config.parse_opt(i)
+      if @config.value_config?(name)
+        @config[name] = value
+      else
+        evalopt.push [name, value]
+      end
+      set.push name
+    end
+    evalopt.each do |name, value|
+      @config.lookup(name).evaluate value, @config
+    end
+    # Check if configuration is valid
+    set.each do |n|
+      @config[n] if @config.value_config?(n)
+    end
+  end
+
+  def parsearg_install
+    @config.no_harm = false
+    @config.install_prefix = ''
+    while a = ARGV.shift
+      case a
+      when '--no-harm'
+        @config.no_harm = true
+      when /\A--prefix=/
+        path = a.split(/=/, 2)[1]
+        path = File.expand_path(path) unless path[0,1] == '/'
+        @config.install_prefix = path
+      else
+        setup_rb_error "install: unknown option #{a}"
+      end
+    end
+  end
+
+  def print_usage(out)
+    out.puts 'Typical Installation Procedure:'
+    out.puts "  $ ruby #{File.basename $0} config"
+    out.puts "  $ ruby #{File.basename $0} setup"
+    out.puts "  # ruby #{File.basename $0} install (may require root privilege)"
+    out.puts
+    out.puts 'Detailed Usage:'
+    out.puts "  ruby #{File.basename $0} <global option>"
+    out.puts "  ruby #{File.basename $0} [<global options>] <task> [<task options>]"
+
+    fmt = "  %-24s %s\n"
+    out.puts
+    out.puts 'Global options:'
+    out.printf fmt, '-q,--quiet',   'suppress message outputs'
+    out.printf fmt, '   --verbose', 'output messages verbosely'
+    out.printf fmt, '   --help',    'print this message'
+    out.printf fmt, '   --version', 'print version and quit'
+    out.printf fmt, '   --copyright',  'print copyright and quit'
+    out.puts
+    out.puts 'Tasks:'
+    TASKS.each do |name, desc|
+      out.printf fmt, name, desc
+    end
+
+    fmt = "  %-24s %s [%s]\n"
+    out.puts
+    out.puts 'Options for CONFIG or ALL:'
+    @config.each do |item|
+      out.printf fmt, item.help_opt, item.description, item.help_default
+    end
+    out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's"
+    out.puts
+    out.puts 'Options for INSTALL:'
+    out.printf fmt, '--no-harm', 'only display what to do if given', 'off'
+    out.printf fmt, '--prefix=path',  'install path prefix', ''
+    out.puts
+  end
+
+  #
+  # Task Handlers
+  #
+
+  def exec_config
+    @installer.exec_config
+    @config.save   # must be final
+  end
+
+  def exec_setup
+    @installer.exec_setup
+  end
+
+  def exec_install
+    @installer.exec_install
+  end
+
+  def exec_test
+    @installer.exec_test
+  end
+
+  def exec_show
+    @config.each do |i|
+      printf "%-20s %s\n", i.name, i.value if i.value?
+    end
+  end
+
+  def exec_clean
+    @installer.exec_clean
+  end
+
+  def exec_distclean
+    @installer.exec_distclean
+  end
+
+end   # class ToplevelInstaller
+
+
+class ToplevelInstallerMulti < ToplevelInstaller
+
+  include FileOperations
+
+  def initialize(ardir_root, config)
+    super
+    @packages = directories_of("#{@ardir}/packages")
+    raise 'no package exists' if @packages.empty?
+    @root_installer = Installer.new(@config, @ardir, File.expand_path('.'))
+  end
+
+  def run_metaconfigs
+    @config.load_script "#{@ardir}/metaconfig", self
+    @packages.each do |name|
+      @config.load_script "#{@ardir}/packages/#{name}/metaconfig"
+    end
+  end
+
+  attr_reader :packages
+
+  def packages=(list)
+    raise 'package list is empty' if list.empty?
+    list.each do |name|
+      raise "directory packages/#{name} does not exist"\
+              unless File.dir?("#{@ardir}/packages/#{name}")
+    end
+    @packages = list
+  end
+
+  def init_installers
+    @installers = {}
+    @packages.each do |pack|
+      @installers[pack] = Installer.new(@config,
+                                       "#{@ardir}/packages/#{pack}",
+                                       "packages/#{pack}")
+    end
+    with    = extract_selection(config('with'))
+    without = extract_selection(config('without'))
+    @selected = @installers.keys.select {|name|
+                  (with.empty? or with.include?(name)) \
+                      and not without.include?(name)
+                }
+  end
+
+  def extract_selection(list)
+    a = list.split(/,/)
+    a.each do |name|
+      setup_rb_error "no such package: #{name}"  unless @installers.key?(name)
+    end
+    a
+  end
+
+  def print_usage(f)
+    super
+    f.puts 'Inluded packages:'
+    f.puts '  ' + @packages.sort.join(' ')
+    f.puts
+  end
+
+  #
+  # Task Handlers
+  #
+
+  def exec_config
+    run_hook 'pre-config'
+    each_selected_installers {|inst| inst.exec_config }
+    run_hook 'post-config'
+    @config.save   # must be final
+  end
+
+  def exec_setup
+    run_hook 'pre-setup'
+    each_selected_installers {|inst| inst.exec_setup }
+    run_hook 'post-setup'
+  end
+
+  def exec_install
+    run_hook 'pre-install'
+    each_selected_installers {|inst| inst.exec_install }
+    run_hook 'post-install'
+  end
+
+  def exec_test
+    run_hook 'pre-test'
+    each_selected_installers {|inst| inst.exec_test }
+    run_hook 'post-test'
+  end
+
+  def exec_clean
+    rm_f @config.savefile
+    run_hook 'pre-clean'
+    each_selected_installers {|inst| inst.exec_clean }
+    run_hook 'post-clean'
+  end
+
+  def exec_distclean
+    rm_f @config.savefile
+    run_hook 'pre-distclean'
+    each_selected_installers {|inst| inst.exec_distclean }
+    run_hook 'post-distclean'
+  end
+
+  #
+  # lib
+  #
+
+  def each_selected_installers
+    Dir.mkdir 'packages' unless File.dir?('packages')
+    @selected.each do |pack|
+      $stderr.puts "Processing the package `#{pack}' ..." if verbose?
+      Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}")
+      Dir.chdir "packages/#{pack}"
+      yield @installers[pack]
+      Dir.chdir '../..'
+    end
+  end
+
+  def run_hook(id)
+    @root_installer.run_hook id
+  end
+
+  # module FileOperations requires this
+  def verbose?
+    @config.verbose?
+  end
+
+  # module FileOperations requires this
+  def no_harm?
+    @config.no_harm?
+  end
+
+end   # class ToplevelInstallerMulti
+
+
+class Installer
+
+  FILETYPES = %w( bin lib ext data conf man )
+
+  include FileOperations
+  include HookScriptAPI
+
+  def initialize(config, srcroot, objroot)
+    @config = config
+    @srcdir = File.expand_path(srcroot)
+    @objdir = File.expand_path(objroot)
+    @currdir = '.'
+  end
+
+  def inspect
+    "#<#{self.class} #{File.basename(@srcdir)}>"
+  end
+
+  def noop(rel)
+  end
+
+  #
+  # Hook Script API base methods
+  #
+
+  def srcdir_root
+    @srcdir
+  end
+
+  def objdir_root
+    @objdir
+  end
+
+  def relpath
+    @currdir
+  end
+
+  #
+  # Config Access
+  #
+
+  # module FileOperations requires this
+  def verbose?
+    @config.verbose?
+  end
+
+  # module FileOperations requires this
+  def no_harm?
+    @config.no_harm?
+  end
+
+  def verbose_off
+    begin
+      save, @config.verbose = @config.verbose?, false
+      yield
+    ensure
+      @config.verbose = save
+    end
+  end
+
+  #
+  # TASK config
+  #
+
+  def exec_config
+    exec_task_traverse 'config'
+  end
+
+  alias config_dir_bin noop
+  alias config_dir_lib noop
+
+  def config_dir_ext(rel)
+    extconf if extdir?(curr_srcdir())
+  end
+
+  alias config_dir_data noop
+  alias config_dir_conf noop
+  alias config_dir_man noop
+
+  def extconf
+    ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt
+  end
+
+  #
+  # TASK setup
+  #
+
+  def exec_setup
+    exec_task_traverse 'setup'
+  end
+
+  def setup_dir_bin(rel)
+    files_of(curr_srcdir()).each do |fname|
+      update_shebang_line "#{curr_srcdir()}/#{fname}"
+    end
+  end
+
+  alias setup_dir_lib noop
+
+  def setup_dir_ext(rel)
+    make if extdir?(curr_srcdir())
+  end
+
+  alias setup_dir_data noop
+  alias setup_dir_conf noop
+  alias setup_dir_man noop
+
+  def update_shebang_line(path)
+    return if no_harm?
+    return if config('shebang') == 'never'
+    old = Shebang.load(path)
+    if old
+      $stderr.puts "warning: #{path}: Shebang line includes too many args.  It is not portable and your program may not work." if old.args.size > 1
+      new = new_shebang(old)
+      return if new.to_s == old.to_s
+    else
+      return unless config('shebang') == 'all'
+      new = Shebang.new(config('rubypath'))
+    end
+    $stderr.puts "updating shebang: #{File.basename(path)}" if verbose?
+    open_atomic_writer(path) {|output|
+      File.open(path, 'rb') {|f|
+        f.gets if old   # discard
+        output.puts new.to_s
+        output.print f.read
+      }
+    }
+  end
+
+  def new_shebang(old)
+    if /\Aruby/ =~ File.basename(old.cmd)
+      Shebang.new(config('rubypath'), old.args)
+    elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby'
+      Shebang.new(config('rubypath'), old.args[1..-1])
+    else
+      return old unless config('shebang') == 'all'
+      Shebang.new(config('rubypath'))
+    end
+  end
+
+  def open_atomic_writer(path, &block)
+    tmpfile = File.basename(path) + '.tmp'
+    begin
+      File.open(tmpfile, 'wb', &block)
+      File.rename tmpfile, File.basename(path)
+    ensure
+      File.unlink tmpfile if File.exist?(tmpfile)
+    end
+  end
+
+  class Shebang
+    def Shebang.load(path)
+      line = nil
+      File.open(path) {|f|
+        line = f.gets
+      }
+      return nil unless /\A#!/ =~ line
+      parse(line)
+    end
+
+    def Shebang.parse(line)
+      cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ')
+      new(cmd, args)
+    end
+
+    def initialize(cmd, args = [])
+      @cmd = cmd
+      @args = args
+    end
+
+    attr_reader :cmd
+    attr_reader :args
+
+    def to_s
+      "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}")
+    end
+  end
+
+  #
+  # TASK install
+  #
+
+  def exec_install
+    rm_f 'InstalledFiles'
+    exec_task_traverse 'install'
+  end
+
+  def install_dir_bin(rel)
+    install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755
+  end
+
+  def install_dir_lib(rel)
+    install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644
+  end
+
+  def install_dir_ext(rel)
+    return unless extdir?(curr_srcdir())
+    install_files rubyextentions('.'),
+                  "#{config('sodir')}/#{File.dirname(rel)}",
+                  0555
+  end
+
+  def install_dir_data(rel)
+    install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644
+  end
+
+  def install_dir_conf(rel)
+    # FIXME: should not remove current config files
+    # (rename previous file to .old/.org)
+    install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644
+  end
+
+  def install_dir_man(rel)
+    install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644
+  end
+
+  def install_files(list, dest, mode)
+    mkdir_p dest, @config.install_prefix
+    list.each do |fname|
+      install fname, dest, mode, @config.install_prefix
+    end
+  end
+
+  def libfiles
+    glob_reject(%w(*.y *.output), targetfiles())
+  end
+
+  def rubyextentions(dir)
+    ents = glob_select("*.#{@config.dllext}", targetfiles())
+    if ents.empty?
+      setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first"
+    end
+    ents
+  end
+
+  def targetfiles
+    mapdir(existfiles() - hookfiles())
+  end
+
+  def mapdir(ents)
+    ents.map {|ent|
+      if File.exist?(ent)
+      then ent                         # objdir
+      else "#{curr_srcdir()}/#{ent}"   # srcdir
+      end
+    }
+  end
+
+  # picked up many entries from cvs-1.11.1/src/ignore.c
+  JUNK_FILES = %w(
+    core RCSLOG tags TAGS .make.state
+    .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
+    *~ *.old *.bak *.BAK *.orig *.rej _$* *$
+
+    *.org *.in .*
+  )
+
+  def existfiles
+    glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.')))
+  end
+
+  def hookfiles
+    %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt|
+      %w( config setup install clean ).map {|t| sprintf(fmt, t) }
+    }.flatten
+  end
+
+  def glob_select(pat, ents)
+    re = globs2re([pat])
+    ents.select {|ent| re =~ ent }
+  end
+
+  def glob_reject(pats, ents)
+    re = globs2re(pats)
+    ents.reject {|ent| re =~ ent }
+  end
+
+  GLOB2REGEX = {
+    '.' => '\.',
+    '$' => '\$',
+    '#' => '\#',
+    '*' => '.*'
+  }
+
+  def globs2re(pats)
+    /\A(?:#{
+      pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|')
+    })\z/
+  end
+
+  #
+  # TASK test
+  #
+
+  TESTDIR = 'test'
+
+  def exec_test
+    unless File.directory?('test')
+      $stderr.puts 'no test in this package' if verbose?
+      return
+    end
+    $stderr.puts 'Running tests...' if verbose?
+    begin
+      require 'test/unit'
+    rescue LoadError
+      setup_rb_error 'test/unit cannot loaded.  You need Ruby 1.8 or later to invoke this task.'
+    end
+    runner = Test::Unit::AutoRunner.new(true)
+    runner.to_run << TESTDIR
+    runner.run
+  end
+
+  #
+  # TASK clean
+  #
+
+  def exec_clean
+    exec_task_traverse 'clean'
+    rm_f @config.savefile
+    rm_f 'InstalledFiles'
+  end
+
+  alias clean_dir_bin noop
+  alias clean_dir_lib noop
+  alias clean_dir_data noop
+  alias clean_dir_conf noop
+  alias clean_dir_man noop
+
+  def clean_dir_ext(rel)
+    return unless extdir?(curr_srcdir())
+    make 'clean' if File.file?('Makefile')
+  end
+
+  #
+  # TASK distclean
+  #
+
+  def exec_distclean
+    exec_task_traverse 'distclean'
+    rm_f @config.savefile
+    rm_f 'InstalledFiles'
+  end
+
+  alias distclean_dir_bin noop
+  alias distclean_dir_lib noop
+
+  def distclean_dir_ext(rel)
+    return unless extdir?(curr_srcdir())
+    make 'distclean' if File.file?('Makefile')
+  end
+
+  alias distclean_dir_data noop
+  alias distclean_dir_conf noop
+  alias distclean_dir_man noop
+
+  #
+  # Traversing
+  #
+
+  def exec_task_traverse(task)
+    run_hook "pre-#{task}"
+    FILETYPES.each do |type|
+      if type == 'ext' and config('without-ext') == 'yes'
+        $stderr.puts 'skipping ext/* by user option' if verbose?
+        next
+      end
+      traverse task, type, "#{task}_dir_#{type}"
+    end
+    run_hook "post-#{task}"
+  end
+
+  def traverse(task, rel, mid)
+    dive_into(rel) {
+      run_hook "pre-#{task}"
+      __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '')
+      directories_of(curr_srcdir()).each do |d|
+        traverse task, "#{rel}/#{d}", mid
+      end
+      run_hook "post-#{task}"
+    }
+  end
+
+  def dive_into(rel)
+    return unless File.dir?("#{@srcdir}/#{rel}")
+
+    dir = File.basename(rel)
+    Dir.mkdir dir unless File.dir?(dir)
+    prevdir = Dir.pwd
+    Dir.chdir dir
+    $stderr.puts '---> ' + rel if verbose?
+    @currdir = rel
+    yield
+    Dir.chdir prevdir
+    $stderr.puts '<--- ' + rel if verbose?
+    @currdir = File.dirname(rel)
+  end
+
+  def run_hook(id)
+    path = [ "#{curr_srcdir()}/#{id}",
+             "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) }
+    return unless path
+    begin
+      instance_eval File.read(path), path, 1
+    rescue
+      raise if $DEBUG
+      setup_rb_error "hook #{path} failed:\n" + $!.message
+    end
+  end
+
+end   # class Installer
+
+
+class SetupError < StandardError; end
+
+def setup_rb_error(msg)
+  raise SetupError, msg
+end
+
+if $0 == __FILE__
+  begin
+    ToplevelInstaller.invoke
+  rescue SetupError
+    raise if $DEBUG
+    $stderr.puts $!.message
+    $stderr.puts "Try 'ruby #{$0} --help' for detailed usage."
+    exit 1
+  end
+end
diff --git a/test/test_clogger.rb b/test/test_clogger.rb
new file mode 100644
index 0000000..f79017b
--- /dev/null
+++ b/test/test_clogger.rb
@@ -0,0 +1,349 @@
+# -*- encoding: binary -*-
+$stderr.sync = $stdout.sync = true
+require "test/unit"
+require "date"
+require "stringio"
+
+require "rack"
+
+require "clogger"
+class TestClogger < Test::Unit::TestCase
+  include Clogger::Format
+
+  def setup
+    @req = {
+      "REQUEST_METHOD" => "GET",
+      "HTTP_VERSION" => "HTTP/1.0",
+      "HTTP_USER_AGENT" => 'echo and socat \o/',
+      "PATH_INFO" => "/hello",
+      "QUERY_STRING" => "goodbye=true",
+      "rack.errors" => $stderr,
+      "rack.input" => File.open('/dev/null', 'rb'),
+      "REMOTE_ADDR" => 'home',
+    }
+  end
+
+  def test_init_basic
+    Clogger.new(lambda { |env| [ 0, {}, [] ] })
+  end
+
+  def test_init_noargs
+    assert_raise(ArgumentError) { Clogger.new }
+  end
+
+  def test_init_stderr
+    cl = Clogger.new(lambda { |env| [ 0, {}, [] ] }, :logger => $stderr)
+    assert_kind_of(Integer, cl.fileno)
+    assert_equal $stderr.fileno, cl.fileno
+  end
+
+  def test_init_stringio
+    cl = Clogger.new(lambda { |env| [ 0, {}, [] ] }, :logger => StringIO.new)
+    assert_nil cl.fileno
+  end
+
+  def test_write_stringio
+    start = DateTime.now - 1
+    str = StringIO.new
+    cl = Clogger.new(lambda { |env| [ "302 Found", {}, [] ] }, :logger => str)
+    status, headers, body = cl.call(@req)
+    assert_equal("302 Found", status)
+    assert_equal({}, headers)
+    body.each { |part| assert false }
+    str = str.string
+    r = %r{\Ahome - - \[[^\]]+\] "GET /hello\?goodbye=true HTTP/1.0" 302 -\n\z}
+    assert_match r, str
+    %r{\[([^\]]+)\]} =~ str
+    tmp = nil
+    assert_nothing_raised {
+      tmp = DateTime.strptime($1, "%d/%b/%Y:%H:%M:%S %z")
+    }
+    assert tmp >= start
+    assert tmp <= DateTime.now
+  end
+
+  def test_clen_stringio
+    start = DateTime.now - 1
+    str = StringIO.new
+    app = lambda { |env| [ 301, {'Content-Length' => '5'}, ['abcde'] ] }
+    format = Common.dup
+    assert format.gsub!(/response_length/, 'sent_http_content_length')
+    cl = Clogger.new(app, :logger => str, :format => format)
+    status, headers, body = cl.call(@req)
+    assert_equal(301, status)
+    assert_equal({'Content-Length' => '5'}, headers)
+    body.each { |part| assert_equal('abcde', part) }
+    str = str.string
+    r = %r{\Ahome - - \[[^\]]+\] "GET /hello\?goodbye=true HTTP/1.0" 301 5\n\z}
+    assert_match r, str
+    %r{\[([^\]]+)\]} =~ str
+    tmp = nil
+    assert_nothing_raised {
+      tmp = DateTime.strptime($1, "%d/%b/%Y:%H:%M:%S %z")
+    }
+    assert tmp >= start
+    assert tmp <= DateTime.now
+  end
+
+  def test_compile_ambiguous
+    cl = Clogger.new(nil, :logger => $stderr)
+    ary = nil
+    cl.instance_eval {
+      ary = compile_format(
+        '$remote_addr $$$$pid' \
+        "\n")
+    }
+    expect = [
+      [ Clogger::OP_REQUEST, "REMOTE_ADDR" ],
+      [ Clogger::OP_LITERAL, " " ],
+      [ Clogger::OP_LITERAL, "$$$" ],
+      [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:pid] ],
+      [ Clogger::OP_LITERAL, "\n" ],
+      ]
+    assert_equal expect, ary
+  end
+
+  def test_compile_auto_newline
+    cl = Clogger.new(nil, :logger => $stderr)
+    ary = nil
+    cl.instance_eval { ary = compile_format('$remote_addr $request') }
+    expect = [
+      [ Clogger::OP_REQUEST, "REMOTE_ADDR" ],
+      [ Clogger::OP_LITERAL, " " ],
+      [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:request] ],
+      [ Clogger::OP_LITERAL, "\n" ],
+      ]
+    assert_equal expect, ary
+  end
+
+  def test_big_log
+    str = StringIO.new
+    fmt = '$remote_addr $pid $remote_user [$time_local] ' \
+          '"$request" $status $body_bytes_sent "$http_referer" ' \
+         '"$http_user_agent" "$http_cookie" $request_time $http_host'
+    app = lambda { |env| [ 302, {}, [] ] }
+    cl = Clogger.new(app, :logger => str, :format => fmt)
+    cookie = "foo=bar#{'f' * 256}".freeze
+    req = {
+      'HTTP_HOST' => 'example.com:12345',
+      'HTTP_COOKIE' => cookie,
+    }
+    req = @req.merge(req)
+    cl.call(req).last.each { |part| part }
+    str = str.string
+    assert(str.size > 128)
+    assert_match %r["echo and socat \\o/" "#{cookie}" \d+\.\d{3}], str
+    assert_match %r["#{cookie}" \d+\.\d{3} example\.com:12345\n\z], str
+  end
+
+  def test_compile
+    cl = Clogger.new(nil, :logger => $stderr)
+    ary = nil
+    cl.instance_eval {
+      ary = compile_format(
+        '$remote_addr - $remote_user [$time_local] ' \
+        '"$request" $status $body_bytes_sent "$http_referer" ' \
+        '"$http_user_agent" "$http_cookie" $request_time ' \
+        '$env{rack.url_scheme}' \
+        "\n")
+    }
+    expect = [
+      [ Clogger::OP_REQUEST, "REMOTE_ADDR" ],
+      [ Clogger::OP_LITERAL, " - " ],
+      [ Clogger::OP_REQUEST, "REMOTE_USER" ],
+      [ Clogger::OP_LITERAL, " [" ],
+      [ Clogger::OP_TIME_LOCAL, '%d/%b/%Y:%H:%M:%S %z' ],
+      [ Clogger::OP_LITERAL, "] \"" ],
+      [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:request] ],
+      [ Clogger::OP_LITERAL, "\" "],
+      [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:status] ],
+      [ Clogger::OP_LITERAL, " "],
+      [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:body_bytes_sent] ],
+      [ Clogger::OP_LITERAL, " \"" ],
+      [ Clogger::OP_REQUEST, "HTTP_REFERER" ],
+      [ Clogger::OP_LITERAL, "\" \"" ],
+      [ Clogger::OP_REQUEST, "HTTP_USER_AGENT" ],
+      [ Clogger::OP_LITERAL, "\" \"" ],
+      [ Clogger::OP_REQUEST, "HTTP_COOKIE" ],
+      [ Clogger::OP_LITERAL, "\" " ],
+      [ Clogger::OP_REQUEST_TIME, '%d.%03d', 1000 ],
+      [ Clogger::OP_LITERAL, " " ],
+      [ Clogger::OP_REQUEST, "rack.url_scheme" ],
+      [ Clogger::OP_LITERAL, "\n" ],
+    ]
+    assert_equal expect, ary
+  end
+
+  def test_eval
+    current = Thread.current.to_s
+    str = StringIO.new
+    app = lambda { |env| [ 302, {}, [] ] }
+    cl = Clogger.new(app,
+                    :logger => str,
+                    :format => "-$e{Thread.current}-\n")
+    status, headers, body = cl.call(@req)
+    assert_equal "-#{current}-\n", str.string
+  end
+
+  def test_pid
+    str = StringIO.new
+    app = lambda { |env| [ 302, {}, [] ] }
+    cl = Clogger.new(app, :logger => str, :format => "[$pid]\n")
+    status, headers, body = cl.call(@req)
+    assert_equal "[#$$]\n", str.string
+  end
+
+  def test_rack_xff
+    str = StringIO.new
+    app = lambda { |env| [ 302, {}, [] ] }
+    cl = Clogger.new(app, :logger => str, :format => "$ip")
+    req = @req.merge("HTTP_X_FORWARDED_FOR" => '192.168.1.1')
+    status, headers, body = cl.call(req)
+    assert_equal "192.168.1.1\n", str.string
+    str.rewind
+    str.truncate(0)
+    status, headers, body = cl.call(@req)
+    assert_equal "home\n", str.string
+    str.rewind
+    str.truncate(0)
+  end
+
+  def test_rack_1_0
+    start = DateTime.now - 1
+    str = StringIO.new
+    app = lambda { |env| [ 200, {'Content-Length'=>'0'}, %w(a b c)] }
+    cl = Clogger.new(app, :logger => str, :format => Rack_1_0)
+    status, headers, body = cl.call(@req)
+    tmp = []
+    body.each { |s| tmp << s }
+    assert_equal %w(a b c), tmp
+    str = str.string
+    assert_match %r[" 200 3 \d+\.\d{4}\n\z], str
+    tmp = nil
+    %r{\[(\d+/\w+/\d+ \d+:\d+:\d+)\]} =~ str
+    assert $1
+    assert_nothing_raised { tmp = DateTime.strptime($1, "%d/%b/%Y %H:%M:%S") }
+    assert tmp >= start
+    assert tmp <= DateTime.now
+  end
+
+  def test_msec
+    str = StringIO.new
+    app = lambda { |env| [ 200, {}, [] ] }
+    cl = Clogger.new(app, :logger => str, :format => '$msec')
+    status, header, bodies = cl.call(@req)
+    assert_match %r(\A\d+\.\d{3}\n\z), str.string
+  end
+
+  def test_usec
+    str = StringIO.new
+    app = lambda { |env| [ 200, {}, [] ] }
+    cl = Clogger.new(app, :logger => str, :format => '$usec')
+    status, header, bodies = cl.call(@req)
+    assert_match %r(\A\d+\.\d{6}\n\z), str.string
+  end
+
+  def test_time_0
+    str = StringIO.new
+    app = lambda { |env| [ 200, {}, [] ] }
+    cl = Clogger.new(app, :logger => str, :format => '$time{0}')
+    status, header, bodies = cl.call(@req)
+    assert_match %r(\A\d+\n\z), str.string
+  end
+
+  def test_time_1
+    str = StringIO.new
+    app = lambda { |env| [ 200, {}, [] ] }
+    cl = Clogger.new(app, :logger => str, :format => '$time{1}')
+    status, header, bodies = cl.call(@req)
+    assert_match %r(\A\d+\.\d\n\z), str.string
+  end
+
+  def test_request_length
+    str = StringIO.new
+    input = StringIO.new('.....')
+    app = lambda { |env| [ 200, {}, [] ] }
+    cl = Clogger.new(app, :logger => str, :format => '$request_length')
+    status, header, bodies = cl.call(@req.merge('rack.input' => input))
+    assert_equal "5\n", str.string
+  end
+
+  def test_response_length_0
+    str = StringIO.new
+    app = lambda { |env| [ 200, {}, [] ] }
+    cl = Clogger.new(app, :logger => str, :format => '$response_length')
+    status, header, bodies = cl.call(@req)
+    bodies.each { |part| part }
+    assert_equal "-\n", str.string
+  end
+
+  def test_combined
+    start = DateTime.now - 1
+    str = StringIO.new
+    app = lambda { |env| [ 200, {'Content-Length'=>'3'}, %w(a b c)] }
+    cl = Clogger.new(app, :logger => str, :format => Combined)
+    status, headers, body = cl.call(@req)
+    tmp = []
+    body.each { |s| tmp << s }
+    assert_equal %w(a b c), tmp
+    str = str.string
+    assert_match %r[" 200 3 "-" "echo and socat \\o/"\n\z], str
+    tmp = nil
+    %r{\[(\d+/\w+/\d+:\d+:\d+:\d+ .+)\]} =~ str
+    assert $1
+    assert_nothing_raised {
+      tmp = DateTime.strptime($1, "%d/%b/%Y:%H:%M:%S %z")
+    }
+    assert tmp >= start
+    assert tmp <= DateTime.now
+  end
+
+  def test_rack_errors_fallback
+    err = StringIO.new
+    app = lambda { |env| [ 200, {'Content-Length'=>'3'}, %w(a b c)] }
+    cl = Clogger.new(app, :format => '$pid')
+    req = @req.merge('rack.errors' => err)
+    status, headers, body = cl.call(req)
+    assert_equal "#$$\n", err.string
+  end
+
+  def test_body_close
+    s_body = StringIO.new(%w(a b c).join("\n"))
+    app = lambda { |env| [ 200, {'Content-Length'=>'5'}, s_body] }
+    cl = Clogger.new(app, :logger => [], :format => '$pid')
+    status, headers, body = cl.call(@req)
+    assert ! s_body.closed?
+    assert_nothing_raised { body.close }
+    assert s_body.closed?
+  end
+
+  def test_escape
+    str = StringIO.new
+    app = lambda { |env| [ 200, {'Content-Length'=>'5'}, [] ] }
+    cl = Clogger.new(app,
+      :logger => str,
+      :format => '$http_user_agent "$request"')
+    bad = {
+      'HTTP_USER_AGENT' => '"asdf"',
+      'QUERY_STRING' => 'sdf=bar"',
+      'PATH_INFO' => '/"<>"',
+    }
+    status, headers, body = cl.call(@req.merge(bad))
+    expect = '\x22asdf\x22 "GET /\x22<>\x22?sdf=bar\x22 HTTP/1.0"' << "\n"
+    assert_equal expect, str.string
+  end
+
+  def test_cookies
+    str = StringIO.new
+    app = lambda { |env|
+      req = Rack::Request.new(env).cookies
+      [ 302, {}, [] ]
+    }
+    cl = Clogger.new(app,
+        :format => '$cookie_foo $cookie_quux',
+        :logger => str)
+    req = @req.merge('HTTP_COOKIE' => "foo=bar;quux=h&m")
+    status, headers, body = cl.call(req)
+    assert_equal "bar h&m\n", str.string
+  end
+end