Initial import (again)
authorEntrope <entrope@clan-dk.org>
Wed, 11 Feb 2004 04:12:26 +0000 (04:12 +0000)
committerEntrope <entrope@clan-dk.org>
Wed, 11 Feb 2004 04:12:26 +0000 (04:12 +0000)
Initial import of srvx-1.3 code.
git-archimport-id: srvx@srvx.net--2004-srvx/srvx--devo--1.3--base-0

162 files changed:
+srvx.supp [new file with mode: 0644]
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
INSTALL [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
NEWS [new file with mode: 0644]
README [new file with mode: 0644]
TODO [new file with mode: 0644]
autogen.sh [new file with mode: 0755]
compile [new file with mode: 0755]
config.guess [new file with mode: 0755]
config.sub [new file with mode: 0755]
configure.in [new file with mode: 0644]
depcomp [new file with mode: 0755]
docs/access-levels.txt [new file with mode: 0644]
docs/coding-style.txt [new file with mode: 0644]
docs/cookies.txt [new file with mode: 0644]
docs/helpserv.txt [new file with mode: 0644]
docs/ircd-modes.txt [new file with mode: 0644]
docs/malloc-compare.txt [new file with mode: 0644]
install-sh [new file with mode: 0755]
ltmain.sh [new file with mode: 0644]
missing [new file with mode: 0755]
mkinstalldirs [new file with mode: 0755]
patches/asuka-sethost.diff [new file with mode: 0644]
patches/helpserv-pgsql.diff [new file with mode: 0644]
patches/helpserv-pgsql.txt [new file with mode: 0644]
patches/log-pgsql.diff [new file with mode: 0644]
patches/log-pgsql.txt [new file with mode: 0644]
patches/ns_reclaim-flag102403.diff [new file with mode: 0644]
patches/ns_tried2reg102403.diff [new file with mode: 0644]
patches/srvx-bantypes.diff [new file with mode: 0644]
patches/srvx-successor.diff [new file with mode: 0644]
rx/COPYING.LIB [new file with mode: 0644]
rx/ChangeLog [new file with mode: 0644]
rx/Makefile.am [new file with mode: 0644]
rx/Makefile.in [new file with mode: 0644]
rx/_rx.h [new file with mode: 0644]
rx/compile [new file with mode: 0755]
rx/depcomp [new file with mode: 0755]
rx/hashrexp.c [new file with mode: 0644]
rx/inst-rxposix.h [new file with mode: 0644]
rx/rx.c [new file with mode: 0644]
rx/rx.h [new file with mode: 0644]
rx/rxall.h [new file with mode: 0644]
rx/rxanal.c [new file with mode: 0644]
rx/rxanal.h [new file with mode: 0644]
rx/rxbasic.c [new file with mode: 0644]
rx/rxbasic.h [new file with mode: 0644]
rx/rxbitset.c [new file with mode: 0644]
rx/rxbitset.h [new file with mode: 0644]
rx/rxcontext.h [new file with mode: 0644]
rx/rxcset.c [new file with mode: 0644]
rx/rxcset.h [new file with mode: 0644]
rx/rxdbug.c [new file with mode: 0644]
rx/rxgnucomp.c [new file with mode: 0644]
rx/rxgnucomp.h [new file with mode: 0644]
rx/rxhash.c [new file with mode: 0644]
rx/rxhash.h [new file with mode: 0644]
rx/rxnfa.c [new file with mode: 0644]
rx/rxnfa.h [new file with mode: 0644]
rx/rxnode.c [new file with mode: 0644]
rx/rxnode.h [new file with mode: 0644]
rx/rxposix.c [new file with mode: 0644]
rx/rxposix.h [new file with mode: 0644]
rx/rxproto.h [new file with mode: 0644]
rx/rxsimp.c [new file with mode: 0644]
rx/rxsimp.h [new file with mode: 0644]
rx/rxspencer.c [new file with mode: 0644]
rx/rxspencer.h [new file with mode: 0644]
rx/rxstr.c [new file with mode: 0644]
rx/rxstr.h [new file with mode: 0644]
rx/rxsuper.c [new file with mode: 0644]
rx/rxsuper.h [new file with mode: 0644]
rx/rxunfa.c [new file with mode: 0644]
rx/rxunfa.h [new file with mode: 0644]
sockcheck.conf.example [new file with mode: 0644]
src/Makefile.am [new file with mode: 0644]
src/chanserv.c [new file with mode: 0644]
src/chanserv.h [new file with mode: 0644]
src/chanserv.help [new file with mode: 0644]
src/checkdb.c [new file with mode: 0644]
src/common.h [new file with mode: 0644]
src/compat.c [new file with mode: 0644]
src/compat.h [new file with mode: 0644]
src/conf.c [new file with mode: 0644]
src/conf.h [new file with mode: 0644]
src/dict-splay.c [new file with mode: 0644]
src/dict.h [new file with mode: 0644]
src/expnhelp.c [new file with mode: 0644]
src/getopt.c [new file with mode: 0644]
src/getopt.h [new file with mode: 0644]
src/getopt1.c [new file with mode: 0644]
src/gline.c [new file with mode: 0644]
src/gline.h [new file with mode: 0644]
src/global.c [new file with mode: 0644]
src/global.h [new file with mode: 0644]
src/global.help [new file with mode: 0644]
src/globtest.c [new file with mode: 0644]
src/hash.c [new file with mode: 0644]
src/hash.h [new file with mode: 0644]
src/heap.c [new file with mode: 0644]
src/heap.h [new file with mode: 0644]
src/helpfile.c [new file with mode: 0644]
src/helpfile.h [new file with mode: 0644]
src/ioset.c [new file with mode: 0644]
src/ioset.h [new file with mode: 0644]
src/log.c [new file with mode: 0644]
src/log.h [new file with mode: 0644]
src/main.c [new file with mode: 0644]
src/md5.c [new file with mode: 0644]
src/md5.h [new file with mode: 0644]
src/mod-helpserv.c [new file with mode: 0644]
src/mod-helpserv.help [new file with mode: 0644]
src/mod-memoserv.c [new file with mode: 0644]
src/mod-memoserv.help [new file with mode: 0644]
src/mod-snoop.c [new file with mode: 0644]
src/mod-sockcheck.c [new file with mode: 0644]
src/mod-sockcheck.help [new file with mode: 0644]
src/modcmd.c [new file with mode: 0644]
src/modcmd.h [new file with mode: 0644]
src/modcmd.help [new file with mode: 0644]
src/modules.c [new file with mode: 0644]
src/modules.h [new file with mode: 0644]
src/nickserv.c [new file with mode: 0644]
src/nickserv.h [new file with mode: 0644]
src/nickserv.help.m4 [new file with mode: 0644]
src/opserv.c [new file with mode: 0644]
src/opserv.h [new file with mode: 0644]
src/opserv.help [new file with mode: 0644]
src/policer.c [new file with mode: 0644]
src/policer.h [new file with mode: 0644]
src/proto-bahamut.c [new file with mode: 0644]
src/proto-common.c [new file with mode: 0644]
src/proto-p10.c [new file with mode: 0644]
src/proto.h [new file with mode: 0644]
src/recdb.c [new file with mode: 0644]
src/recdb.h [new file with mode: 0644]
src/saxdb.c [new file with mode: 0644]
src/saxdb.h [new file with mode: 0644]
src/saxdb.help [new file with mode: 0644]
src/sendmail.c [new file with mode: 0644]
src/sendmail.h [new file with mode: 0644]
src/sendmail.help [new file with mode: 0644]
src/stamp-h.in [new file with mode: 0644]
src/stamp-h1.in [new file with mode: 0644]
src/timeq.c [new file with mode: 0644]
src/timeq.h [new file with mode: 0644]
src/tools.c [new file with mode: 0644]
srvx.conf.example [new file with mode: 0644]
stamp-h2.in [new file with mode: 0644]
tests/coverage-2.cmd [new file with mode: 0644]
tests/coverage.cmd [new file with mode: 0644]
tests/coverage.txt [new file with mode: 0644]
tests/ircd.conf [new file with mode: 0644]
tests/ircd.motd [new file with mode: 0644]
tests/nickserv.cmd [new file with mode: 0644]
tests/p10.cmd [new file with mode: 0644]
tests/srvx.conf [new file with mode: 0644]
tests/test-driver.pl [new file with mode: 0755]
tests/test.cmd [new file with mode: 0644]

diff --git a/+srvx.supp b/+srvx.supp
new file mode 100644 (file)
index 0000000..ed85600
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   ld.so-SUX/Conditional jump or move depends on uninitialized value
+   Memcheck:Cond
+   obj:*/ld-2.3.6.so
+   obj:*/ld-2.3.6.so
+   obj:*/ld-2.3.6.so
+}
+
+{
+   ld.so-SUX-MORE/Conditional jump or move depends on uninitialized value
+   Memcheck:Cond
+   obj:*/ld-2.3.6.so
+   obj:*/ld-2.3.6.so
+   obj:*/libc-2.3.6.so
+}
+
+{
+   ld.so-SUX/Invalid read of size 8
+   Memcheck:Addr8
+   obj:*/ld-2.3.6.so
+   obj:*/ld-2.3.6.so
+   obj:*/ld-2.3.6.so
+}
+
+{
+   libresolv.so-SUX/Syscall param socketcall.sendto(msg) points to uninitialised byte(s)
+   Memcheck:Param
+   socketcall.sendto(msg)
+   fun:__sendto_nocancel
+   fun:__check_pf
+   fun:getaddrinfo
+}
diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..1bb68dd
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,23 @@
+                            srvx Development Team
+
+Project Admins:
+def        <def@vt.edu>
+Entrope    <entrope@users.sourceforge.net>
+
+Coders:
+def        <def@vt.edu>
+Entrope    <entrope@users.sourceforge.net>
+Jedi       <jedi@turboflux.net>
+SailorFrag <sailorfrag@users.sourceforge.net>
+Zoot       <zoot@gamesnet.net>
+
+Code Contributors:
+Phooeybane <phooeybane@glined.org>
+Portal     <portal@vt.edu>
+
+Documentation:
+Phooeybane <phooeybane@glined.org>
+Seldon
+
+You can find the team on irc.gamesnet.net, in #srvx. Bug reports,
+feature requests, criticism or praise are welcome. We love feedback.
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..89d816e
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,347 @@
+NOTE: If you are Donald Wasylyna, or you intend to use this software
+in connection with any Internet chat network organized or operated by
+Donald Wasylyna or any company where Donald Wasylyna holds at least a
+5% control or ownership stake, you may not use or redistribute this
+software under the terms of this license.  You MUST negotiate a
+license with the developers of this software.
+
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the 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 Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year  name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..0106b23
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,134 @@
+             srvx Installation Instructions
+
+Introduction:
+----------
+If you encounter any problems compiling/running srvx, please make sure
+you have followed the directions in this file correctly and that all
+of the requirements listed below are met.
+
+If the problem persists, report it to one (or all) of the coders
+listed in the AUTHORS file. Please try to include ALL relevant
+information about the error/bug, including anything out of the
+ordinary reported from make and the appropriate entries from the log
+files.
+
+Requirements:
+----------
+This release of srvx (1.2) only supports the Undernet P10 and Bahamut
+protocols and is known to link with ircu/Universal (u2.10.09),
+ircu/Undernet (u2.10.10, u2.10.11), ircu-lain, and Bahamut 1.4.32. It
+does not support hybrid, Unreal, or any other ircd not listed. Should
+you find other compatible ircds, please let us know.
+
+srvx is known to compile on the following systems as long as you
+are using GNU make (make on Linux, gmake on many other systems):
+
+  * Linux (libc5 or glibc2); glibc2.1 recommended+    (SPARC, ALPHA, x86, PPC)
+  * FreeBSD 4.x; tested on 4.2-RELEASE and 4.2-STABLE (SPARC, x86)
+  * FreeBSD 3.x; tested on 3.3-RELEASE and 3.4-STABLE (SPARC, x86)
+  * FreeBSD 2.x; tested on 2.2.8-RELEASE             (SPARC, x86)
+  * NetBSD 1.6+;                                     (ALPHA, MIPS, x86)
+  * SunOS 5.x; tested on 5.8                         (SPARC, x86)
+  * OpenBSD 2.x; tested on 2.8                       (x86)
+  * BSDi 4.x; tested on 4.0.1                        (x86)
+  * CYGWIN 1.1.x and 1.3.x; tested on 1.1.8          (x86)
+
+For the Linux kernel, srvx has been tested on Debian 2.x - 3.x, and
+Redhat 5.x - 8.x.
+
+srvx should compile on other system types also.  If you have success
+on other platforms/archs or problems on any platforms/archs, please
+contact the authors to let us know.
+
+gcc 2.96 tends to emit spurious warnings; before reporting any
+compiler warnings from it, make sure you are using the most recent
+version of it or try using an official release of gcc.
+
+You may also have trouble unless your compiler's C preprocessor
+supports ISO C99 varadic macros.  gcc is the compiler we use for
+almost all our testing, and we recommend it for use with srvx.
+
+Quick Install:
+----------
+$ ./configure
+    NOTE: The protocol the resulting srvx binary will support is
+    determined by the configure script. The P10 protocol is the
+    default; if you would like to link to Bahamut, you must pass
+    the --with-protocol=bahamut flag to the configure script:
+    $ ./configure --with-protocol=bahamut
+$ make
+$ ${EDITOR} srvx.conf
+    NOTE: You may want to copy srvx.conf.example to srvx.conf and
+    edit that.
+$ ./srvx
+
+Compiling:
+----------
+  1) Enter the root directory of the srvx tree.  If installation is done
+     from outside of it, it may cause problems during compile, or during
+     runtime.
+
+  2) Run the configure script (sh configure), it will verify that your
+     system will have the resources needed for srvx to compile.  If you
+     would like to change the path where srvx will be installed to,
+     execute configure with the --prefix=/path option.  The default path
+     is ~/srvx-X.X.X/, with the X's representing the version.  See the
+     note in "Quick Install" if you are linking to Bahamut.
+
+  3) On some systems you may need to edit the Makefile in order for
+     it to compile correctly.  Includes, and other such things may
+     reside in other directories.  Most likely the Makefile won't require
+     any modifications.
+
+  4) You may optionally edit config.h in case the configure script made a
+     mistake.
+
+  5) Execute the "make" command to begin compiling.  If you encounter any
+     uncorrectable errors/warnings, please scroll up to the introduction
+     section and follow the instructions.
+
+  6) You may now either type "make install" to install it to your
+     installation path, or work from your build directory, either is fine.
+
+  7) Copy sockcheck.conf.example to sockcheck.conf (and edit to add
+     new proxy types, if you wish).
+
+  8) Copy srvx.conf.example to srvx.conf and edit to suit your
+     needs. Errors in the configuration file will be logged to
+     main.log (and if srvx is running in the foreground, printed to
+     stdout) when you start the daemon. If you disable nick ownership
+     or enable the email features, you should copy srvx.conf to the
+     src directory and re-run make; this will regenerate the help
+     files (especially for NickServ) to include the appropriate
+     entries.
+
+     You can also do this by hand using the expnhelp program in src:
+       ./src/expnhelp < nickserv.help.m4 > nickserv.help
+
+  9) You can now begin using your service bots.  You can debug by
+     running it with '-fV', it will not background itself, and it
+     will be fairly verbose if you gave the configure script the
+     --enable-debug flag. If you would like to run in the foreground
+     with no verbosity, use the '-f' flag. If you just want to run it,
+     execute srvx without any flags.
+
+ 10) Once you have srvx started, you'll need to register a NickServ
+     account:
+       /msg NickServ@services.irc.com register <account> <password>
+     Make sure that you register the first account -- it is
+     automatically granted certain privileges and gives you root-level
+     access to OpServ once you are opered up.
+
+ 11) New operators can be given access to OpServ through NickServ's
+     (or whatever you've named the nick/authentication service) oset
+     command:
+       /msg NickServ oset <nick>|*<account> level <level>
+     Levels are generally beween 0 and 1000 by convention; higher
+     numbers correspond to more access. You can also add helpers
+     (users with extra privileges such as security override in
+     traditional configurations) through NickServ:
+       /msg NickServ oset <nick>|*<account> flags +H
+
+End of file, INSTALL.
+
+-Jedi (jedi@turboflux.net)
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..63f84d9
--- /dev/null
@@ -0,0 +1,20 @@
+EXTRA_DIST = srvx.conf.example sockcheck.conf.example
+SUBDIRS = @MY_SUBDIRS@
+DIST_SUBDIRS = src rx
+
+all: srvx
+
+srvx: src/srvx
+       cp ./src/srvx $(srcdir)/src/*.help ./src/nickserv.help .
+
+install-exec-local:
+       $(INSTALL) -d -m 755 $(prefix)
+       $(INSTALL) -m 744 ./src/srvx $(prefix)
+       $(INSTALL) -m 644 $(srcdir)/src/*.help $(prefix)
+       $(INSTALL) -m 600 $(srcdir)/srvx.conf.example $(prefix)
+       $(INSTALL) -m 644 $(srcdir)/sockcheck.conf.example $(prefix)
+       @echo
+       @echo srvx-$(VERSION) has been installed to $(prefix)
+       @echo Remember to edit srvx.conf.example and sockcheck.conf.example
+       @echo And of course, ./srvx --help before starting.
+       @echo
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..a71d318
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,41 @@
+News for srvx release 0.9.5
+[March 21, 2001 - Present]
+---------------------------
+- 0.9.5 released March 21, 2001.
+- This is the LAST release before 1.0 goes out.
+- We came across a (large) memory leak in FreeBSD's libc and worked
+  around it.  We also submitted a bug report to the FreeBSD team.
+- As far as we know, no more serious bugs are present in srvx. Most
+  development before 1.0 will be tweaking and minor fixups.
+- Helpfiles should be up to date before the release of 1.0.
+- Just showing off srvx's speed:
+    <@Jedi> ?timecmd trace count host *
+    -OpServX(OpServX@Services.GamesNET.net)- Found 4593 matches.
+    -OpServX(OpServX@Services.GamesNET.net)- Command `trace' finished in 0.003037 seconds.
+
+
+News for everything prior to 0.9.5
+[April 16, 2000 - March 20, 2001]
+----------------------------------
+- Coding began about 1 year prior to the 0.9.5 release.
+- Documentation up to 0.9.5 is virtually non-existant.
+- PRE01 released April 16, 2000.
+- PRE02 released April 18, 2000.
+- After the main parts were completed, it all came down to bug fixing.
+- 0.1 released June 13, 2000.
+- srvx was setup on a machine and linked to GamesNET for its initial beta
+  tests.  Many many many bugs were fixed in the first week, fewer as time
+  went along.
+- 0.2 released July 5, 2000.
+- Between 0.2 and 0.7 code for the services themselves was put in.  Prior
+  to this was mainly core coding.
+- Using dmalloc, bounds-checking and new log replaying code, we were able
+  to hammer out alot bugs, some of which were severe enough to cause a
+  segfault.
+- 0.7 released December 15, 2000.  [0.3 through 0.6 never released]
+- 0.9 released January 17, 2001.   [0.8 never released]
+- Just prior to the 0.9.5 release, numerous memory leaks were fixed,
+  prompting the release of 0.9.5.
+- srvx is still running smoothly on GamesNET.  If we didn't take it down
+  every 2 days to be updated with new patches, its uptime would be quite
+  high.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..4e5562e
--- /dev/null
+++ b/README
@@ -0,0 +1,52 @@
+                              srvx Readme File
+
+Description:
+----------
+srvx is a complete set of services for ircu/Universal or ircu/Undernet
+P10 protocol networks. It aims to be highly efficient, flexible, and
+extensible. Features include advanced proxy detection and all the
+standard services users expect e.g. ChanServ, NickServ, OpServ,
+HelpServ, and Global.
+
+srvx is open source software; the source is available for download
+under the terms and conditions of the GNU General Public License,
+commonly known as the GNU GPL.
+
+The official srvx web site is available at http://www.srvx.net/.
+
+Installation:
+----------
+Please read the INSTALL file for installation instructions.
+
+Support:
+----------
+There are several conditions under which we WILL NOT support your use
+of these services. (The license may permit such uses, but we do not
+wish to encourage them.)
+
+Specifically:
+
+ - Use under Windows. srvx can work to some extent under the Cygwin
+ (http://www.cygwin.com/) environment, but Windows as a whole departs
+ significantly from standard APIs and is difficult to support.
+
+ - Alteration of credits. If you change the credits in your version of
+ the code so as to obscure the original authorship of the program, we
+ will not support you.  If you want to use our code -- especially if
+ you want us to answer your questions about it -- do the decent thing
+ and preserve the credits that are in the code we distribute.
+
+In addition, please read the documentation included in the srvx
+distribution before you ask us for support. The answers to many common
+questions related to installation are in the INSTALL file, for
+example. It saves both our time and your time, so please read the
+documentation.
+
+Contacts:
+----------
+The AUTHORS file contains contact information for the srvx Development
+Team.
+
+End of file, README.
+
+-Jedi (jedi@turboflux.net)
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..b6f432e
--- /dev/null
+++ b/TODO
@@ -0,0 +1,38 @@
+ServerSpy.net
+[FEATREQ] finish ServerSpy.net support
+
+Core
+[FEATREQ] per-service "set privmsg" option
+
+---------
+
+<def|office> featreq: is_authed criteria for trace
+<Zoot> featreq: disallow the use of the channel criteria in addalert
+<myriad> [featreq] something to turn off security override after a set period of time
+<word-up_Wwv> [FEATREQ] how about adding a suspended symbol to the authserv channelaccesslist, would be handy and informative
+<adam> [featreq] an account flag that disallows users from using /ns kill (or some other way to prevent/deter nick holding on an account-level)
+<Byte> [featreq] people cant !invite over a timeban
+<Commander> [featreq] allow srvx to listen on a port for an ircd to connect to it
+[adam(m@myriad.lackofdiscipline.com)] yeah, and possibly another flag or some way to stop a hostmask range from registering any new nicks
+[adam(m@myriad.lackofdiscipline.com)] we're having people grab nicks of other users who don't understand how the system works
+[msg(adam)] yeah, blocking account or nick registrations for an address (or block of addresses) was another featreq on the web page
+<Incutio> put notes into the /msg o chaninfo output
+
+---------
+
+[FEATREQ] Confirmation Of Channel Registration
+  <ChanServ> #Rowdy is now registered under handle Rowdy
+  <ChanServ> Please note, If you do not actively use this channel for 14 days, it will be deregistered
+  <ChanServ> Also note: Advertising and mass Inviting are not allowed on Servername. If you or any of your channel members/ops do any of these activities, your channel will be suspended
+  <ChanServ> To acknowledge you understand these things, /msg ChanServ CONFIRM #Rowdy and this registration process will be finalised.
+  - done on GamesNET by helpers with their channel reg scripts
+
+[FEATREQ] Track which account added a user to a channel userlist
+
+[FEATREQ] Suspension durations and/or comments when suspending user access to a channel
+
+[FEATREQ] Timestamps for channel notes and DNRs
+
+[FEATREQ] support "unregistered": ?ctrace print mode +s unregistered
+
+[FEATREQ] move "version" command into modcmd core; register module versions with modcmd
diff --git a/autogen.sh b/autogen.sh
new file mode 100755 (executable)
index 0000000..f5a8d76
--- /dev/null
@@ -0,0 +1,16 @@
+#! /bin/sh
+
+aclocal
+autoheader -Wall
+automake -a --gnu Makefile rx/Makefile src/Makefile
+autoconf -Wall
+if [ -d ./src/srvx ] ; then
+  echo "WARNING: It looks like you still have the obsolete src/srvx directory."
+  echo "Since we try to compile the binary into that place, this will break the"
+  echo "compile.  This is probably because you did not \"cvs update\" properly"
+  echo "(i.e. with the -P flag).  Since you almost always want to do \"cvs update -P\","
+  echo "we suggest you add a line similar to \"update -P\" to your ~/.cvsrc file."
+  echo "For example:"
+  echo "  echo update -P >> ~/.cvsrc"
+  echo "At the very least, \"rm -r src/srvx\" before you try to compile."
+fi
diff --git a/compile b/compile
new file mode 100755 (executable)
index 0000000..9bb997a
--- /dev/null
+++ b/compile
@@ -0,0 +1,99 @@
+#! /bin/sh
+
+# Wrapper for compilers which do not understand `-c -o'.
+
+# Copyright 1999, 2000 Free Software Foundation, Inc.
+# Written by Tom Tromey <tromey@cygnus.com>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# Usage:
+# compile PROGRAM [ARGS]...
+# `-o FOO.o' is removed from the args passed to the actual compile.
+
+prog=$1
+shift
+
+ofile=
+cfile=
+args=
+while test $# -gt 0; do
+   case "$1" in
+    -o)
+       # configure might choose to run compile as `compile cc -o foo foo.c'.
+       # So we do something ugly here.
+       ofile=$2
+       shift
+       case "$ofile" in
+       *.o | *.obj)
+          ;;
+       *)
+          args="$args -o $ofile"
+          ofile=
+          ;;
+       esac
+       ;;
+    *.c)
+       cfile=$1
+       args="$args $1"
+       ;;
+    *)
+       args="$args $1"
+       ;;
+   esac
+   shift
+done
+
+if test -z "$ofile" || test -z "$cfile"; then
+   # If no `-o' option was seen then we might have been invoked from a
+   # pattern rule where we don't need one.  That is ok -- this is a
+   # normal compilation that the losing compiler can handle.  If no
+   # `.c' file was seen then we are probably linking.  That is also
+   # ok.
+   exec "$prog" $args
+fi
+
+# Name of file we expect compiler to create.
+cofile=`echo $cfile | sed -e 's|^.*/||' -e 's/\.c$/.o/'`
+
+# Create the lock directory.
+# Note: use `[/.-]' here to ensure that we don't use the same name
+# that we are using for the .o file.  Also, base the name on the expected
+# object file name, since that is what matters with a parallel build.
+lockdir=`echo $cofile | sed -e 's|[/.-]|_|g'`.d
+while true; do
+   if mkdir $lockdir > /dev/null 2>&1; then
+      break
+   fi
+   sleep 1
+done
+# FIXME: race condition here if user kills between mkdir and trap.
+trap "rmdir $lockdir; exit 1" 1 2 15
+
+# Run the compile.
+"$prog" $args
+status=$?
+
+if test -f "$cofile"; then
+   mv "$cofile" "$ofile"
+fi
+
+rmdir $lockdir
+exit $status
diff --git a/config.guess b/config.guess
new file mode 100755 (executable)
index 0000000..54936b2
--- /dev/null
@@ -0,0 +1,1380 @@
+#! /bin/sh
+# Attempt to guess a canonical system name.
+#   Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000
+#   Free Software Foundation, Inc.
+
+timestamp='2000-12-21'
+
+# This file is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# Written by Per Bothner <bothner@cygnus.com>.
+# Please send patches to <config-patches@gnu.org>.
+#
+# This script attempts to guess a canonical system name similar to
+# config.sub.  If it succeeds, it prints the system name on stdout, and
+# exits with 0.  Otherwise, it exits with 1.
+#
+# The plan is that this can be called by configure scripts if you
+# don't specify an explicit build system type.
+#
+# Only a few systems have been added to this list; please add others
+# (but try to keep the structure clean).
+#
+
+me=`echo "$0" | sed -e 's,.*/,,'`
+
+usage="\
+Usage: $0 [OPTION]
+
+Output the configuration name of the system \`$me' is run on.
+
+Operation modes:
+  -h, --help         print this help, then exit
+  -t, --time-stamp   print date of last modification, then exit
+  -v, --version      print version number, then exit
+
+Report bugs and patches to <config-patches@gnu.org>."
+
+version="\
+GNU config.guess ($timestamp)
+
+Originally written by Per Bothner.
+Copyright (C) 1992, 93, 94, 95, 96, 97, 98, 99, 2000
+Free Software Foundation, Inc.
+
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
+
+help="
+Try \`$me --help' for more information."
+
+# Parse command line
+while test $# -gt 0 ; do
+  case $1 in
+    --time-stamp | --time* | -t )
+       echo "$timestamp" ; exit 0 ;;
+    --version | -v )
+       echo "$version" ; exit 0 ;;
+    --help | --h* | -h )
+       echo "$usage"; exit 0 ;;
+    -- )     # Stop option processing
+       shift; break ;;
+    - )        # Use stdin as input.
+       break ;;
+    -* )
+       echo "$me: invalid option $1$help" >&2
+       exit 1 ;;
+    * )
+       break ;;
+  esac
+done
+
+if test $# != 0; then
+  echo "$me: too many arguments$help" >&2
+  exit 1
+fi
+
+
+dummy=dummy-$$
+trap 'rm -f $dummy.c $dummy.o $dummy; exit 1' 1 2 15
+
+# CC_FOR_BUILD -- compiler used by this script.
+# Historically, `CC_FOR_BUILD' used to be named `HOST_CC'. We still
+# use `HOST_CC' if defined, but it is deprecated.
+
+case $CC_FOR_BUILD,$HOST_CC,$CC in
+ ,,)    echo "int dummy(){}" > $dummy.c
+       for c in cc gcc c89 ; do
+         ($c $dummy.c -c -o $dummy.o) >/dev/null 2>&1
+         if test $? = 0 ; then
+            CC_FOR_BUILD="$c"; break
+         fi
+       done
+       rm -f $dummy.c $dummy.o
+       if test x"$CC_FOR_BUILD" = x ; then
+         CC_FOR_BUILD=no_compiler_found
+       fi
+       ;;
+ ,,*)   CC_FOR_BUILD=$CC ;;
+ ,*,*)  CC_FOR_BUILD=$HOST_CC ;;
+esac
+
+# This is needed to find uname on a Pyramid OSx when run in the BSD universe.
+# (ghazi@noc.rutgers.edu 8/24/94.)
+if (test -f /.attbin/uname) >/dev/null 2>&1 ; then
+       PATH=$PATH:/.attbin ; export PATH
+fi
+
+UNAME_MACHINE=`(uname -m) 2>/dev/null` || UNAME_MACHINE=unknown
+UNAME_RELEASE=`(uname -r) 2>/dev/null` || UNAME_RELEASE=unknown
+UNAME_SYSTEM=`(uname -s) 2>/dev/null`  || UNAME_SYSTEM=unknown
+UNAME_VERSION=`(uname -v) 2>/dev/null` || UNAME_VERSION=unknown
+
+# Note: order is significant - the case branches are not exclusive.
+
+case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in
+    *:NetBSD:*:*)
+       # Netbsd (nbsd) targets should (where applicable) match one or
+       # more of the tupples: *-*-netbsdelf*, *-*-netbsdaout*,
+       # *-*-netbsdecoff* and *-*-netbsd*.  For targets that recently
+       # switched to ELF, *-*-netbsd* would select the old
+       # object file format.  This provides both forward
+       # compatibility and a consistent mechanism for selecting the
+       # object file format.
+       # Determine the machine/vendor (is the vendor relevant).
+       case "${UNAME_MACHINE}" in
+           amiga) machine=m68k-unknown ;;
+           arm32) machine=arm-unknown ;;
+           atari*) machine=m68k-atari ;;
+           sun3*) machine=m68k-sun ;;
+           mac68k) machine=m68k-apple ;;
+           macppc) machine=powerpc-apple ;;
+           hp3[0-9][05]) machine=m68k-hp ;;
+           ibmrt|romp-ibm) machine=romp-ibm ;;
+           *) machine=${UNAME_MACHINE}-unknown ;;
+       esac
+       # The Operating System including object format, if it has switched
+       # to ELF recently, or will in the future.
+       case "${UNAME_MACHINE}" in
+           i386|sparc|amiga|arm*|hp300|mvme68k|vax|atari|luna68k|mac68k|news68k|next68k|pc532|sun3*|x68k)
+               if echo __ELF__ | $CC_FOR_BUILD -E - 2>/dev/null \
+                       | grep __ELF__ >/dev/null
+               then
+                   # Once all utilities can be ECOFF (netbsdecoff) or a.out (netbsdaout).
+                   # Return netbsd for either.  FIX?
+                   os=netbsd
+               else
+                   os=netbsdelf
+               fi
+               ;;
+           *)
+               os=netbsd
+               ;;
+       esac
+       # The OS release
+       release=`echo ${UNAME_RELEASE}|sed -e 's/[-_].*/\./'`
+       # Since CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM:
+       # contains redundant information, the shorter form:
+       # CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM is used.
+       echo "${machine}-${os}${release}"
+       exit 0 ;;
+    alpha:OSF1:*:*)
+       if test $UNAME_RELEASE = "V4.0"; then
+               UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $3}'`
+       fi
+       # A Vn.n version is a released version.
+       # A Tn.n version is a released field test version.
+       # A Xn.n version is an unreleased experimental baselevel.
+       # 1.2 uses "1.2" for uname -r.
+       cat <<EOF >$dummy.s
+       .data
+\$Lformat:
+       .byte 37,100,45,37,120,10,0     # "%d-%x\n"
+
+       .text
+       .globl main
+       .align 4
+       .ent main
+main:
+       .frame \$30,16,\$26,0
+       ldgp \$29,0(\$27)
+       .prologue 1
+       .long 0x47e03d80 # implver \$0
+       lda \$2,-1
+       .long 0x47e20c21 # amask \$2,\$1
+       lda \$16,\$Lformat
+       mov \$0,\$17
+       not \$1,\$18
+       jsr \$26,printf
+       ldgp \$29,0(\$26)
+       mov 0,\$16
+       jsr \$26,exit
+       .end main
+EOF
+       $CC_FOR_BUILD $dummy.s -o $dummy 2>/dev/null
+       if test "$?" = 0 ; then
+               case `./$dummy` in
+                       0-0)
+                               UNAME_MACHINE="alpha"
+                               ;;
+                       1-0)
+                               UNAME_MACHINE="alphaev5"
+                               ;;
+                       1-1)
+                               UNAME_MACHINE="alphaev56"
+                               ;;
+                       1-101)
+                               UNAME_MACHINE="alphapca56"
+                               ;;
+                       2-303)
+                               UNAME_MACHINE="alphaev6"
+                               ;;
+                       2-307)
+                               UNAME_MACHINE="alphaev67"
+                               ;;
+               esac
+       fi
+       rm -f $dummy.s $dummy
+       echo ${UNAME_MACHINE}-dec-osf`echo ${UNAME_RELEASE} | sed -e 's/^[VTX]//' | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'`
+       exit 0 ;;
+    Alpha\ *:Windows_NT*:*)
+       # How do we know it's Interix rather than the generic POSIX subsystem?
+       # Should we change UNAME_MACHINE based on the output of uname instead
+       # of the specific Alpha model?
+       echo alpha-pc-interix
+       exit 0 ;;
+    21064:Windows_NT:50:3)
+       echo alpha-dec-winnt3.5
+       exit 0 ;;
+    Amiga*:UNIX_System_V:4.0:*)
+       echo m68k-unknown-sysv4
+       exit 0;;
+    amiga:OpenBSD:*:*)
+       echo m68k-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    *:[Aa]miga[Oo][Ss]:*:*)
+       echo ${UNAME_MACHINE}-unknown-amigaos
+       exit 0 ;;
+    arc64:OpenBSD:*:*)
+       echo mips64el-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    arc:OpenBSD:*:*)
+       echo mipsel-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    hkmips:OpenBSD:*:*)
+       echo mips-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    pmax:OpenBSD:*:*)
+       echo mipsel-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    sgi:OpenBSD:*:*)
+       echo mips-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    wgrisc:OpenBSD:*:*)
+       echo mipsel-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    *:OS/390:*:*)
+       echo i370-ibm-openedition
+       exit 0 ;;
+    arm:RISC*:1.[012]*:*|arm:riscix:1.[012]*:*)
+       echo arm-acorn-riscix${UNAME_RELEASE}
+       exit 0;;
+    SR2?01:HI-UX/MPP:*:*)
+       echo hppa1.1-hitachi-hiuxmpp
+       exit 0;;
+    Pyramid*:OSx*:*:* | MIS*:OSx*:*:* | MIS*:SMP_DC-OSx*:*:*)
+       # akee@wpdis03.wpafb.af.mil (Earle F. Ake) contributed MIS and NILE.
+       if test "`(/bin/universe) 2>/dev/null`" = att ; then
+               echo pyramid-pyramid-sysv3
+       else
+               echo pyramid-pyramid-bsd
+       fi
+       exit 0 ;;
+    NILE*:*:*:dcosx)
+       echo pyramid-pyramid-svr4
+       exit 0 ;;
+    sun4H:SunOS:5.*:*)
+       echo sparc-hal-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+       exit 0 ;;
+    sun4*:SunOS:5.*:* | tadpole*:SunOS:5.*:*)
+       echo sparc-sun-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+       exit 0 ;;
+    i86pc:SunOS:5.*:*)
+       echo i386-pc-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+       exit 0 ;;
+    sun4*:SunOS:6*:*)
+       # According to config.sub, this is the proper way to canonicalize
+       # SunOS6.  Hard to guess exactly what SunOS6 will be like, but
+       # it's likely to be more like Solaris than SunOS4.
+       echo sparc-sun-solaris3`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+       exit 0 ;;
+    sun4*:SunOS:*:*)
+       case "`/usr/bin/arch -k`" in
+           Series*|S4*)
+               UNAME_RELEASE=`uname -v`
+               ;;
+       esac
+       # Japanese Language versions have a version number like `4.1.3-JL'.
+       echo sparc-sun-sunos`echo ${UNAME_RELEASE}|sed -e 's/-/_/'`
+       exit 0 ;;
+    sun3*:SunOS:*:*)
+       echo m68k-sun-sunos${UNAME_RELEASE}
+       exit 0 ;;
+    sun*:*:4.2BSD:*)
+       UNAME_RELEASE=`(head -1 /etc/motd | awk '{print substr($5,1,3)}') 2>/dev/null`
+       test "x${UNAME_RELEASE}" = "x" && UNAME_RELEASE=3
+       case "`/bin/arch`" in
+           sun3)
+               echo m68k-sun-sunos${UNAME_RELEASE}
+               ;;
+           sun4)
+               echo sparc-sun-sunos${UNAME_RELEASE}
+               ;;
+       esac
+       exit 0 ;;
+    aushp:SunOS:*:*)
+       echo sparc-auspex-sunos${UNAME_RELEASE}
+       exit 0 ;;
+    atari*:OpenBSD:*:*)
+       echo m68k-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    # The situation for MiNT is a little confusing.  The machine name
+    # can be virtually everything (everything which is not
+    # "atarist" or "atariste" at least should have a processor
+    # > m68000).  The system name ranges from "MiNT" over "FreeMiNT"
+    # to the lowercase version "mint" (or "freemint").  Finally
+    # the system name "TOS" denotes a system which is actually not
+    # MiNT.  But MiNT is downward compatible to TOS, so this should
+    # be no problem.
+    atarist[e]:*MiNT:*:* | atarist[e]:*mint:*:* | atarist[e]:*TOS:*:*)
+        echo m68k-atari-mint${UNAME_RELEASE}
+       exit 0 ;;
+    atari*:*MiNT:*:* | atari*:*mint:*:* | atarist[e]:*TOS:*:*)
+       echo m68k-atari-mint${UNAME_RELEASE}
+        exit 0 ;;
+    *falcon*:*MiNT:*:* | *falcon*:*mint:*:* | *falcon*:*TOS:*:*)
+        echo m68k-atari-mint${UNAME_RELEASE}
+       exit 0 ;;
+    milan*:*MiNT:*:* | milan*:*mint:*:* | *milan*:*TOS:*:*)
+        echo m68k-milan-mint${UNAME_RELEASE}
+        exit 0 ;;
+    hades*:*MiNT:*:* | hades*:*mint:*:* | *hades*:*TOS:*:*)
+        echo m68k-hades-mint${UNAME_RELEASE}
+        exit 0 ;;
+    *:*MiNT:*:* | *:*mint:*:* | *:*TOS:*:*)
+        echo m68k-unknown-mint${UNAME_RELEASE}
+        exit 0 ;;
+    sun3*:OpenBSD:*:*)
+       echo m68k-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    mac68k:OpenBSD:*:*)
+       echo m68k-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    mvme68k:OpenBSD:*:*)
+       echo m68k-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    mvme88k:OpenBSD:*:*)
+       echo m88k-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    powerpc:machten:*:*)
+       echo powerpc-apple-machten${UNAME_RELEASE}
+       exit 0 ;;
+    RISC*:Mach:*:*)
+       echo mips-dec-mach_bsd4.3
+       exit 0 ;;
+    RISC*:ULTRIX:*:*)
+       echo mips-dec-ultrix${UNAME_RELEASE}
+       exit 0 ;;
+    VAX*:ULTRIX*:*:*)
+       echo vax-dec-ultrix${UNAME_RELEASE}
+       exit 0 ;;
+    2020:CLIX:*:* | 2430:CLIX:*:*)
+       echo clipper-intergraph-clix${UNAME_RELEASE}
+       exit 0 ;;
+    mips:*:*:UMIPS | mips:*:*:RISCos)
+       sed 's/^        //' << EOF >$dummy.c
+#ifdef __cplusplus
+#include <stdio.h>  /* for printf() prototype */
+       int main (int argc, char *argv[]) {
+#else
+       int main (argc, argv) int argc; char *argv[]; {
+#endif
+       #if defined (host_mips) && defined (MIPSEB)
+       #if defined (SYSTYPE_SYSV)
+         printf ("mips-mips-riscos%ssysv\n", argv[1]); exit (0);
+       #endif
+       #if defined (SYSTYPE_SVR4)
+         printf ("mips-mips-riscos%ssvr4\n", argv[1]); exit (0);
+       #endif
+       #if defined (SYSTYPE_BSD43) || defined(SYSTYPE_BSD)
+         printf ("mips-mips-riscos%sbsd\n", argv[1]); exit (0);
+       #endif
+       #endif
+         exit (-1);
+       }
+EOF
+       $CC_FOR_BUILD $dummy.c -o $dummy \
+         && ./$dummy `echo "${UNAME_RELEASE}" | sed -n 's/\([0-9]*\).*/\1/p'` \
+         && rm $dummy.c $dummy && exit 0
+       rm -f $dummy.c $dummy
+       echo mips-mips-riscos${UNAME_RELEASE}
+       exit 0 ;;
+    Night_Hawk:Power_UNIX:*:*)
+       echo powerpc-harris-powerunix
+       exit 0 ;;
+    m88k:CX/UX:7*:*)
+       echo m88k-harris-cxux7
+       exit 0 ;;
+    m88k:*:4*:R4*)
+       echo m88k-motorola-sysv4
+       exit 0 ;;
+    m88k:*:3*:R3*)
+       echo m88k-motorola-sysv3
+       exit 0 ;;
+    AViiON:dgux:*:*)
+        # DG/UX returns AViiON for all architectures
+        UNAME_PROCESSOR=`/usr/bin/uname -p`
+       if [ $UNAME_PROCESSOR = mc88100 ] || [ $UNAME_PROCESSOR = mc88110 ]
+       then
+           if [ ${TARGET_BINARY_INTERFACE}x = m88kdguxelfx ] || \
+              [ ${TARGET_BINARY_INTERFACE}x = x ]
+           then
+               echo m88k-dg-dgux${UNAME_RELEASE}
+           else
+               echo m88k-dg-dguxbcs${UNAME_RELEASE}
+           fi
+       else
+           echo i586-dg-dgux${UNAME_RELEASE}
+       fi
+       exit 0 ;;
+    M88*:DolphinOS:*:*)        # DolphinOS (SVR3)
+       echo m88k-dolphin-sysv3
+       exit 0 ;;
+    M88*:*:R3*:*)
+       # Delta 88k system running SVR3
+       echo m88k-motorola-sysv3
+       exit 0 ;;
+    XD88*:*:*:*) # Tektronix XD88 system running UTekV (SVR3)
+       echo m88k-tektronix-sysv3
+       exit 0 ;;
+    Tek43[0-9][0-9]:UTek:*:*) # Tektronix 4300 system running UTek (BSD)
+       echo m68k-tektronix-bsd
+       exit 0 ;;
+    *:IRIX*:*:*)
+       echo mips-sgi-irix`echo ${UNAME_RELEASE}|sed -e 's/-/_/g'`
+       exit 0 ;;
+    ????????:AIX?:[12].1:2)   # AIX 2.2.1 or AIX 2.1.1 is RT/PC AIX.
+       echo romp-ibm-aix      # uname -m gives an 8 hex-code CPU id
+       exit 0 ;;              # Note that: echo "'`uname -s`'" gives 'AIX '
+    i?86:AIX:*:*)
+       echo i386-ibm-aix
+       exit 0 ;;
+    *:AIX:2:3)
+       if grep bos325 /usr/include/stdio.h >/dev/null 2>&1; then
+               sed 's/^                //' << EOF >$dummy.c
+               #include <sys/systemcfg.h>
+
+               main()
+                       {
+                       if (!__power_pc())
+                               exit(1);
+                       puts("powerpc-ibm-aix3.2.5");
+                       exit(0);
+                       }
+EOF
+               $CC_FOR_BUILD $dummy.c -o $dummy && ./$dummy && rm $dummy.c $dummy && exit 0
+               rm -f $dummy.c $dummy
+               echo rs6000-ibm-aix3.2.5
+       elif grep bos324 /usr/include/stdio.h >/dev/null 2>&1; then
+               echo rs6000-ibm-aix3.2.4
+       else
+               echo rs6000-ibm-aix3.2
+       fi
+       exit 0 ;;
+    *:AIX:*:4)
+       IBM_CPU_ID=`/usr/sbin/lsdev -C -c processor -S available | head -1 | awk '{ print $1 }'`
+       if /usr/sbin/lsattr -EHl ${IBM_CPU_ID} | grep POWER >/dev/null 2>&1; then
+               IBM_ARCH=rs6000
+       else
+               IBM_ARCH=powerpc
+       fi
+       if [ -x /usr/bin/oslevel ] ; then
+               IBM_REV=`/usr/bin/oslevel`
+       else
+               IBM_REV=4.${UNAME_RELEASE}
+       fi
+       echo ${IBM_ARCH}-ibm-aix${IBM_REV}
+       exit 0 ;;
+    *:AIX:*:5)
+       case "`lsattr -El proc0 -a type -F value`" in
+           PowerPC*) IBM_ARCH=powerpc
+                     IBM_MANUF=ibm ;;
+           Itanium)  IBM_ARCH=ia64
+                     IBM_MANUF=unknown ;;
+           POWER*)   IBM_ARCH=power
+                     IBM_MANUF=ibm ;;
+           *)        IBM_ARCH=powerpc
+                     IBM_MANUF=ibm ;;
+       esac
+       echo ${IBM_ARCH}-${IBM_MANUF}-aix${UNAME_VERSION}.${UNAME_RELEASE}
+       exit 0 ;;
+    *:AIX:*:*)
+       echo rs6000-ibm-aix
+       exit 0 ;;
+    ibmrt:4.4BSD:*|romp-ibm:BSD:*)
+       echo romp-ibm-bsd4.4
+       exit 0 ;;
+    ibmrt:*BSD:*|romp-ibm:BSD:*)            # covers RT/PC BSD and
+       echo romp-ibm-bsd${UNAME_RELEASE}   # 4.3 with uname added to
+       exit 0 ;;                           # report: romp-ibm BSD 4.3
+    *:BOSX:*:*)
+       echo rs6000-bull-bosx
+       exit 0 ;;
+    DPX/2?00:B.O.S.:*:*)
+       echo m68k-bull-sysv3
+       exit 0 ;;
+    9000/[34]??:4.3bsd:1.*:*)
+       echo m68k-hp-bsd
+       exit 0 ;;
+    hp300:4.4BSD:*:* | 9000/[34]??:4.3bsd:2.*:*)
+       echo m68k-hp-bsd4.4
+       exit 0 ;;
+    9000/[34678]??:HP-UX:*:*)
+       HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'`
+       case "${UNAME_MACHINE}" in
+           9000/31? )            HP_ARCH=m68000 ;;
+           9000/[34]?? )         HP_ARCH=m68k ;;
+           9000/[678][0-9][0-9])
+              case "${HPUX_REV}" in
+                11.[0-9][0-9])
+                  if [ -x /usr/bin/getconf ]; then
+                    sc_cpu_version=`/usr/bin/getconf SC_CPU_VERSION 2>/dev/null`
+                    sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null`
+                    case "${sc_cpu_version}" in
+                      523) HP_ARCH="hppa1.0" ;; # CPU_PA_RISC1_0
+                      528) HP_ARCH="hppa1.1" ;; # CPU_PA_RISC1_1
+                      532)                      # CPU_PA_RISC2_0
+                        case "${sc_kernel_bits}" in
+                          32) HP_ARCH="hppa2.0n" ;;
+                          64) HP_ARCH="hppa2.0w" ;;
+                        esac ;;
+                    esac
+                  fi ;;
+              esac
+              if [ "${HP_ARCH}" = "" ]; then
+              sed 's/^              //' << EOF >$dummy.c
+
+              #define _HPUX_SOURCE
+              #include <stdlib.h>
+              #include <unistd.h>
+
+              int main ()
+              {
+              #if defined(_SC_KERNEL_BITS)
+                  long bits = sysconf(_SC_KERNEL_BITS);
+              #endif
+                  long cpu  = sysconf (_SC_CPU_VERSION);
+
+                  switch (cpu)
+               {
+               case CPU_PA_RISC1_0: puts ("hppa1.0"); break;
+               case CPU_PA_RISC1_1: puts ("hppa1.1"); break;
+               case CPU_PA_RISC2_0:
+              #if defined(_SC_KERNEL_BITS)
+                   switch (bits)
+                       {
+                       case 64: puts ("hppa2.0w"); break;
+                       case 32: puts ("hppa2.0n"); break;
+                       default: puts ("hppa2.0"); break;
+                       } break;
+              #else  /* !defined(_SC_KERNEL_BITS) */
+                   puts ("hppa2.0"); break;
+              #endif
+               default: puts ("hppa1.0"); break;
+               }
+                  exit (0);
+              }
+EOF
+       (CCOPTS= $CC_FOR_BUILD $dummy.c -o $dummy 2>/dev/null ) && HP_ARCH=`./$dummy`
+       if test -z "$HP_ARCH"; then HP_ARCH=hppa; fi
+       rm -f $dummy.c $dummy
+       fi ;;
+       esac
+       echo ${HP_ARCH}-hp-hpux${HPUX_REV}
+       exit 0 ;;
+    ia64:HP-UX:*:*)
+       HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'`
+       echo ia64-hp-hpux${HPUX_REV}
+       exit 0 ;;
+    3050*:HI-UX:*:*)
+       sed 's/^        //' << EOF >$dummy.c
+       #include <unistd.h>
+       int
+       main ()
+       {
+         long cpu = sysconf (_SC_CPU_VERSION);
+         /* The order matters, because CPU_IS_HP_MC68K erroneously returns
+            true for CPU_PA_RISC1_0.  CPU_IS_PA_RISC returns correct
+            results, however.  */
+         if (CPU_IS_PA_RISC (cpu))
+           {
+             switch (cpu)
+               {
+                 case CPU_PA_RISC1_0: puts ("hppa1.0-hitachi-hiuxwe2"); break;
+                 case CPU_PA_RISC1_1: puts ("hppa1.1-hitachi-hiuxwe2"); break;
+                 case CPU_PA_RISC2_0: puts ("hppa2.0-hitachi-hiuxwe2"); break;
+                 default: puts ("hppa-hitachi-hiuxwe2"); break;
+               }
+           }
+         else if (CPU_IS_HP_MC68K (cpu))
+           puts ("m68k-hitachi-hiuxwe2");
+         else puts ("unknown-hitachi-hiuxwe2");
+         exit (0);
+       }
+EOF
+       $CC_FOR_BUILD $dummy.c -o $dummy && ./$dummy && rm $dummy.c $dummy && exit 0
+       rm -f $dummy.c $dummy
+       echo unknown-hitachi-hiuxwe2
+       exit 0 ;;
+    9000/7??:4.3bsd:*:* | 9000/8?[79]:4.3bsd:*:* )
+       echo hppa1.1-hp-bsd
+       exit 0 ;;
+    9000/8??:4.3bsd:*:*)
+       echo hppa1.0-hp-bsd
+       exit 0 ;;
+    *9??*:MPE/iX:*:*)
+       echo hppa1.0-hp-mpeix
+       exit 0 ;;
+    hp7??:OSF1:*:* | hp8?[79]:OSF1:*:* )
+       echo hppa1.1-hp-osf
+       exit 0 ;;
+    hp8??:OSF1:*:*)
+       echo hppa1.0-hp-osf
+       exit 0 ;;
+    i?86:OSF1:*:*)
+       if [ -x /usr/sbin/sysversion ] ; then
+           echo ${UNAME_MACHINE}-unknown-osf1mk
+       else
+           echo ${UNAME_MACHINE}-unknown-osf1
+       fi
+       exit 0 ;;
+    parisc*:Lites*:*:*)
+       echo hppa1.1-hp-lites
+       exit 0 ;;
+    hppa*:OpenBSD:*:*)
+       echo hppa-unknown-openbsd
+       exit 0 ;;
+    C1*:ConvexOS:*:* | convex:ConvexOS:C1*:*)
+       echo c1-convex-bsd
+        exit 0 ;;
+    C2*:ConvexOS:*:* | convex:ConvexOS:C2*:*)
+       if getsysinfo -f scalar_acc
+       then echo c32-convex-bsd
+       else echo c2-convex-bsd
+       fi
+        exit 0 ;;
+    C34*:ConvexOS:*:* | convex:ConvexOS:C34*:*)
+       echo c34-convex-bsd
+        exit 0 ;;
+    C38*:ConvexOS:*:* | convex:ConvexOS:C38*:*)
+       echo c38-convex-bsd
+        exit 0 ;;
+    C4*:ConvexOS:*:* | convex:ConvexOS:C4*:*)
+       echo c4-convex-bsd
+        exit 0 ;;
+    CRAY*X-MP:*:*:*)
+       echo xmp-cray-unicos
+        exit 0 ;;
+    CRAY*Y-MP:*:*:*)
+       echo ymp-cray-unicos${UNAME_RELEASE}
+       exit 0 ;;
+    CRAY*[A-Z]90:*:*:*)
+       echo ${UNAME_MACHINE}-cray-unicos${UNAME_RELEASE} \
+       | sed -e 's/CRAY.*\([A-Z]90\)/\1/' \
+             -e y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/
+       exit 0 ;;
+    CRAY*TS:*:*:*)
+       echo t90-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+       exit 0 ;;
+    CRAY*T3D:*:*:*)
+       echo alpha-cray-unicosmk${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+       exit 0 ;;
+    CRAY*T3E:*:*:*)
+       echo alphaev5-cray-unicosmk${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+       exit 0 ;;
+    CRAY*SV1:*:*:*)
+       echo sv1-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+       exit 0 ;;
+    CRAY-2:*:*:*)
+       echo cray2-cray-unicos
+        exit 0 ;;
+    F30[01]:UNIX_System_V:*:* | F700:UNIX_System_V:*:*)
+       FUJITSU_PROC=`uname -m | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'`
+        FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'`
+        FUJITSU_REL=`echo ${UNAME_RELEASE} | sed -e 's/ /_/'`
+        echo "${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}"
+        exit 0 ;;
+    hp300:OpenBSD:*:*)
+       echo m68k-unknown-openbsd${UNAME_RELEASE}
+       exit 0 ;;
+    i?86:BSD/386:*:* | i?86:BSD/OS:*:* | *:Ascend\ Embedded/OS:*:*)
+       echo ${UNAME_MACHINE}-pc-bsdi${UNAME_RELEASE}
+       exit 0 ;;
+    sparc*:BSD/OS:*:*)
+       echo sparc-unknown-bsdi${UNAME_RELEASE}
+       exit 0 ;;
+    *:BSD/OS:*:*)
+       echo ${UNAME_MACHINE}-unknown-bsdi${UNAME_RELEASE}
+       exit 0 ;;
+    *:FreeBSD:*:*)
+       echo ${UNAME_MACHINE}-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'`
+       exit 0 ;;
+    *:OpenBSD:*:*)
+       echo ${UNAME_MACHINE}-unknown-openbsd`echo ${UNAME_RELEASE}|sed -e 's/[-_].*/\./'`
+       exit 0 ;;
+    i*:CYGWIN*:*)
+       echo ${UNAME_MACHINE}-pc-cygwin
+       exit 0 ;;
+    i*:MINGW*:*)
+       echo ${UNAME_MACHINE}-pc-mingw32
+       exit 0 ;;
+    i*:PW*:*)
+       echo ${UNAME_MACHINE}-pc-pw32
+       exit 0 ;;
+    i*:Windows_NT*:* | Pentium*:Windows_NT*:*)
+       # How do we know it's Interix rather than the generic POSIX subsystem?
+       # It also conflicts with pre-2.0 versions of AT&T UWIN. Should we
+       # UNAME_MACHINE based on the output of uname instead of i386?
+       echo i386-pc-interix
+       exit 0 ;;
+    i*:UWIN*:*)
+       echo ${UNAME_MACHINE}-pc-uwin
+       exit 0 ;;
+    p*:CYGWIN*:*)
+       echo powerpcle-unknown-cygwin
+       exit 0 ;;
+    prep*:SunOS:5.*:*)
+       echo powerpcle-unknown-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+       exit 0 ;;
+    *:GNU:*:*)
+       echo `echo ${UNAME_MACHINE}|sed -e 's,[-/].*$,,'`-unknown-gnu`echo ${UNAME_RELEASE}|sed -e 's,/.*$,,'`
+       exit 0 ;;
+    i*86:Minix:*:*)
+       echo ${UNAME_MACHINE}-pc-minix
+       exit 0 ;;
+    *:Linux:*:*)
+
+       # The BFD linker knows what the default object file format is, so
+       # first see if it will tell us. cd to the root directory to prevent
+       # problems with other programs or directories called `ld' in the path.
+       ld_supported_emulations=`cd /; ld --help 2>&1 \
+                        | sed -ne '/supported emulations:/!d
+                                   s/[         ][      ]*/ /g
+                                   s/.*supported emulations: *//
+                                   s/ .*//
+                                   p'`
+        case "$ld_supported_emulations" in
+         *ia64)
+               echo "${UNAME_MACHINE}-unknown-linux"
+               exit 0
+               ;;
+         i?86linux)
+               echo "${UNAME_MACHINE}-pc-linux-gnuaout"
+               exit 0
+               ;;
+         elf_i?86)
+               TENTATIVE="${UNAME_MACHINE}-pc-linux-gnu"
+               ;;
+         i?86coff)
+               echo "${UNAME_MACHINE}-pc-linux-gnucoff"
+               exit 0
+               ;;
+         sparclinux)
+               echo "${UNAME_MACHINE}-unknown-linux-gnuaout"
+               exit 0
+               ;;
+         elf32_sparc)
+               echo "${UNAME_MACHINE}-unknown-linux-gnu"
+               exit 0
+               ;;
+         armlinux)
+               echo "${UNAME_MACHINE}-unknown-linux-gnuaout"
+               exit 0
+               ;;
+         elf32arm*)
+               echo "${UNAME_MACHINE}-unknown-linux-gnuoldld"
+               exit 0
+               ;;
+         armelf_linux*)
+               echo "${UNAME_MACHINE}-unknown-linux-gnu"
+               exit 0
+               ;;
+         m68klinux)
+               echo "${UNAME_MACHINE}-unknown-linux-gnuaout"
+               exit 0
+               ;;
+         elf32ppc | elf32ppclinux)
+               # Determine Lib Version
+               cat >$dummy.c <<EOF
+#include <features.h>
+#if defined(__GLIBC__)
+extern char __libc_version[];
+extern char __libc_release[];
+#endif
+main(argc, argv)
+     int argc;
+     char *argv[];
+{
+#if defined(__GLIBC__)
+  printf("%s %s\n", __libc_version, __libc_release);
+#else
+  printf("unkown\n");
+#endif
+  return 0;
+}
+EOF
+               LIBC=""
+               $CC_FOR_BUILD $dummy.c -o $dummy 2>/dev/null
+               if test "$?" = 0 ; then
+                       ./$dummy | grep 1\.99 > /dev/null
+                       if test "$?" = 0 ; then
+                               LIBC="libc1"
+                       fi
+               fi
+               rm -f $dummy.c $dummy
+               echo powerpc-unknown-linux-gnu${LIBC}
+               exit 0
+               ;;
+         shelf_linux)
+               echo "${UNAME_MACHINE}-unknown-linux-gnu"
+               exit 0
+               ;;
+       esac
+
+       if test "${UNAME_MACHINE}" = "alpha" ; then
+               cat <<EOF >$dummy.s
+                       .data
+               \$Lformat:
+                       .byte 37,100,45,37,120,10,0     # "%d-%x\n"
+
+                       .text
+                       .globl main
+                       .align 4
+                       .ent main
+               main:
+                       .frame \$30,16,\$26,0
+                       ldgp \$29,0(\$27)
+                       .prologue 1
+                       .long 0x47e03d80 # implver \$0
+                       lda \$2,-1
+                       .long 0x47e20c21 # amask \$2,\$1
+                       lda \$16,\$Lformat
+                       mov \$0,\$17
+                       not \$1,\$18
+                       jsr \$26,printf
+                       ldgp \$29,0(\$26)
+                       mov 0,\$16
+                       jsr \$26,exit
+                       .end main
+EOF
+               LIBC=""
+               $CC_FOR_BUILD $dummy.s -o $dummy 2>/dev/null
+               if test "$?" = 0 ; then
+                       case `./$dummy` in
+                       0-0)
+                               UNAME_MACHINE="alpha"
+                               ;;
+                       1-0)
+                               UNAME_MACHINE="alphaev5"
+                               ;;
+                       1-1)
+                               UNAME_MACHINE="alphaev56"
+                               ;;
+                       1-101)
+                               UNAME_MACHINE="alphapca56"
+                               ;;
+                       2-303)
+                               UNAME_MACHINE="alphaev6"
+                               ;;
+                       2-307)
+                               UNAME_MACHINE="alphaev67"
+                               ;;
+                       esac
+
+                       objdump --private-headers $dummy | \
+                         grep ld.so.1 > /dev/null
+                       if test "$?" = 0 ; then
+                               LIBC="libc1"
+                       fi
+               fi
+               rm -f $dummy.s $dummy
+               echo ${UNAME_MACHINE}-unknown-linux-gnu${LIBC} ; exit 0
+       elif test "${UNAME_MACHINE}" = "mips" ; then
+         cat >$dummy.c <<EOF
+#ifdef __cplusplus
+#include <stdio.h>  /* for printf() prototype */
+       int main (int argc, char *argv[]) {
+#else
+       int main (argc, argv) int argc; char *argv[]; {
+#endif
+#ifdef __MIPSEB__
+  printf ("%s-unknown-linux-gnu\n", argv[1]);
+#endif
+#ifdef __MIPSEL__
+  printf ("%sel-unknown-linux-gnu\n", argv[1]);
+#endif
+  return 0;
+}
+EOF
+         $CC_FOR_BUILD $dummy.c -o $dummy 2>/dev/null && ./$dummy "${UNAME_MACHINE}" && rm $dummy.c $dummy && exit 0
+         rm -f $dummy.c $dummy
+       elif test "${UNAME_MACHINE}" = "s390"; then
+         echo s390-ibm-linux && exit 0
+       elif test "${UNAME_MACHINE}" = "x86_64"; then
+         echo x86_64-unknown-linux-gnu && exit 0
+       elif test "${UNAME_MACHINE}" = "parisc" -o "${UNAME_MACHINE}" = "hppa"; then
+         # Look for CPU level
+         case `grep '^cpu[^a-z]*:' /proc/cpuinfo 2>/dev/null | cut -d' ' -f2` in
+           PA7*)
+               echo hppa1.1-unknown-linux-gnu
+               ;;
+           PA8*)
+               echo hppa2.0-unknown-linux-gnu
+               ;;
+           *)
+               echo hppa-unknown-linux-gnu
+               ;;
+         esac
+         exit 0
+       else
+         # Either a pre-BFD a.out linker (linux-gnuoldld)
+         # or one that does not give us useful --help.
+         # GCC wants to distinguish between linux-gnuoldld and linux-gnuaout.
+         # If ld does not provide *any* "supported emulations:"
+         # that means it is gnuoldld.
+         test -z "$ld_supported_emulations" \
+           && echo "${UNAME_MACHINE}-pc-linux-gnuoldld" && exit 0
+
+         case "${UNAME_MACHINE}" in
+         i?86)
+           VENDOR=pc;
+           ;;
+         *)
+           VENDOR=unknown;
+           ;;
+         esac
+         # Determine whether the default compiler is a.out or elf
+         cat >$dummy.c <<EOF
+#include <features.h>
+#ifdef __cplusplus
+#include <stdio.h>  /* for printf() prototype */
+       int main (int argc, char *argv[]) {
+#else
+       int main (argc, argv) int argc; char *argv[]; {
+#endif
+#ifdef __ELF__
+# ifdef __GLIBC__
+#  if __GLIBC__ >= 2
+    printf ("%s-${VENDOR}-linux-gnu\n", argv[1]);
+#  else
+    printf ("%s-${VENDOR}-linux-gnulibc1\n", argv[1]);
+#  endif
+# else
+   printf ("%s-${VENDOR}-linux-gnulibc1\n", argv[1]);
+# endif
+#else
+  printf ("%s-${VENDOR}-linux-gnuaout\n", argv[1]);
+#endif
+  return 0;
+}
+EOF
+         $CC_FOR_BUILD $dummy.c -o $dummy 2>/dev/null && ./$dummy "${UNAME_MACHINE}" && rm $dummy.c $dummy && exit 0
+         rm -f $dummy.c $dummy
+         test x"${TENTATIVE}" != x && echo "${TENTATIVE}" && exit 0
+       fi ;;
+# ptx 4.0 does uname -s correctly, with DYNIX/ptx in there.  earlier versions
+# are messed up and put the nodename in both sysname and nodename.
+    i?86:DYNIX/ptx:4*:*)
+       echo i386-sequent-sysv4
+       exit 0 ;;
+    i?86:UNIX_SV:4.2MP:2.*)
+        # Unixware is an offshoot of SVR4, but it has its own version
+        # number series starting with 2...
+        # I am not positive that other SVR4 systems won't match this,
+       # I just have to hope.  -- rms.
+        # Use sysv4.2uw... so that sysv4* matches it.
+       echo ${UNAME_MACHINE}-pc-sysv4.2uw${UNAME_VERSION}
+       exit 0 ;;
+    i?86:*:4.*:* | i?86:SYSTEM_V:4.*:*)
+       UNAME_REL=`echo ${UNAME_RELEASE} | sed 's/\/MP$//'`
+       if grep Novell /usr/include/link.h >/dev/null 2>/dev/null; then
+               echo ${UNAME_MACHINE}-univel-sysv${UNAME_REL}
+       else
+               echo ${UNAME_MACHINE}-pc-sysv${UNAME_REL}
+       fi
+       exit 0 ;;
+    i?86:*:5:7*)
+        # Fixed at (any) Pentium or better
+        UNAME_MACHINE=i586
+        if [ ${UNAME_SYSTEM} = "UnixWare" ] ; then
+           echo ${UNAME_MACHINE}-sco-sysv${UNAME_RELEASE}uw${UNAME_VERSION}
+       else
+           echo ${UNAME_MACHINE}-pc-sysv${UNAME_RELEASE}
+       fi
+       exit 0 ;;
+    i?86:*:3.2:*)
+       if test -f /usr/options/cb.name; then
+               UNAME_REL=`sed -n 's/.*Version //p' </usr/options/cb.name`
+               echo ${UNAME_MACHINE}-pc-isc$UNAME_REL
+       elif /bin/uname -X 2>/dev/null >/dev/null ; then
+               UNAME_REL=`(/bin/uname -X|egrep Release|sed -e 's/.*= //')`
+               (/bin/uname -X|egrep i80486 >/dev/null) && UNAME_MACHINE=i486
+               (/bin/uname -X|egrep '^Machine.*Pentium' >/dev/null) \
+                       && UNAME_MACHINE=i586
+               (/bin/uname -X|egrep '^Machine.*Pent ?II' >/dev/null) \
+                       && UNAME_MACHINE=i686
+               (/bin/uname -X|egrep '^Machine.*Pentium Pro' >/dev/null) \
+                       && UNAME_MACHINE=i686
+               echo ${UNAME_MACHINE}-pc-sco$UNAME_REL
+       else
+               echo ${UNAME_MACHINE}-pc-sysv32
+       fi
+       exit 0 ;;
+    i?86:*DOS:*:*)
+       echo ${UNAME_MACHINE}-pc-msdosdjgpp
+       exit 0 ;;
+    pc:*:*:*)
+       # Left here for compatibility:
+        # uname -m prints for DJGPP always 'pc', but it prints nothing about
+        # the processor, so we play safe by assuming i386.
+       echo i386-pc-msdosdjgpp
+        exit 0 ;;
+    Intel:Mach:3*:*)
+       echo i386-pc-mach3
+       exit 0 ;;
+    paragon:*:*:*)
+       echo i860-intel-osf1
+       exit 0 ;;
+    i860:*:4.*:*) # i860-SVR4
+       if grep Stardent /usr/include/sys/uadmin.h >/dev/null 2>&1 ; then
+         echo i860-stardent-sysv${UNAME_RELEASE} # Stardent Vistra i860-SVR4
+       else # Add other i860-SVR4 vendors below as they are discovered.
+         echo i860-unknown-sysv${UNAME_RELEASE}  # Unknown i860-SVR4
+       fi
+       exit 0 ;;
+    mini*:CTIX:SYS*5:*)
+       # "miniframe"
+       echo m68010-convergent-sysv
+       exit 0 ;;
+    M68*:*:R3V[567]*:*)
+       test -r /sysV68 && echo 'm68k-motorola-sysv' && exit 0 ;;
+    3[34]??:*:4.0:3.0 | 3[34]??A:*:4.0:3.0 | 3[34]??,*:*:4.0:3.0 | 4850:*:4.0:3.0)
+       OS_REL=''
+       test -r /etc/.relid \
+       && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid`
+       /bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+         && echo i486-ncr-sysv4.3${OS_REL} && exit 0
+       /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \
+         && echo i586-ncr-sysv4.3${OS_REL} && exit 0 ;;
+    3[34]??:*:4.0:* | 3[34]??,*:*:4.0:*)
+        /bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+          && echo i486-ncr-sysv4 && exit 0 ;;
+    m68*:LynxOS:2.*:*)
+       echo m68k-unknown-lynxos${UNAME_RELEASE}
+       exit 0 ;;
+    mc68030:UNIX_System_V:4.*:*)
+       echo m68k-atari-sysv4
+       exit 0 ;;
+    i?86:LynxOS:2.*:* | i?86:LynxOS:3.[01]*:*)
+       echo i386-unknown-lynxos${UNAME_RELEASE}
+       exit 0 ;;
+    TSUNAMI:LynxOS:2.*:*)
+       echo sparc-unknown-lynxos${UNAME_RELEASE}
+       exit 0 ;;
+    rs6000:LynxOS:2.*:* | PowerPC:LynxOS:2.*:*)
+       echo rs6000-unknown-lynxos${UNAME_RELEASE}
+       exit 0 ;;
+    SM[BE]S:UNIX_SV:*:*)
+       echo mips-dde-sysv${UNAME_RELEASE}
+       exit 0 ;;
+    RM*:ReliantUNIX-*:*:*)
+       echo mips-sni-sysv4
+       exit 0 ;;
+    RM*:SINIX-*:*:*)
+       echo mips-sni-sysv4
+       exit 0 ;;
+    *:SINIX-*:*:*)
+       if uname -p 2>/dev/null >/dev/null ; then
+               UNAME_MACHINE=`(uname -p) 2>/dev/null`
+               echo ${UNAME_MACHINE}-sni-sysv4
+       else
+               echo ns32k-sni-sysv
+       fi
+       exit 0 ;;
+    PENTIUM:CPunix:4.0*:*) # Unisys `ClearPath HMP IX 4000' SVR4/MP effort
+                           # says <Richard.M.Bartel@ccMail.Census.GOV>
+        echo i586-unisys-sysv4
+        exit 0 ;;
+    *:UNIX_System_V:4*:FTX*)
+       # From Gerald Hewes <hewes@openmarket.com>.
+       # How about differentiating between stratus architectures? -djm
+       echo hppa1.1-stratus-sysv4
+       exit 0 ;;
+    *:*:*:FTX*)
+       # From seanf@swdc.stratus.com.
+       echo i860-stratus-sysv4
+       exit 0 ;;
+    mc68*:A/UX:*:*)
+       echo m68k-apple-aux${UNAME_RELEASE}
+       exit 0 ;;
+    news*:NEWS-OS:6*:*)
+       echo mips-sony-newsos6
+       exit 0 ;;
+    R[34]000:*System_V*:*:* | R4000:UNIX_SYSV:*:* | R*000:UNIX_SV:*:*)
+       if [ -d /usr/nec ]; then
+               echo mips-nec-sysv${UNAME_RELEASE}
+       else
+               echo mips-unknown-sysv${UNAME_RELEASE}
+       fi
+        exit 0 ;;
+    BeBox:BeOS:*:*)    # BeOS running on hardware made by Be, PPC only.
+       echo powerpc-be-beos
+       exit 0 ;;
+    BeMac:BeOS:*:*)    # BeOS running on Mac or Mac clone, PPC only.
+       echo powerpc-apple-beos
+       exit 0 ;;
+    BePC:BeOS:*:*)     # BeOS running on Intel PC compatible.
+       echo i586-pc-beos
+       exit 0 ;;
+    SX-4:SUPER-UX:*:*)
+       echo sx4-nec-superux${UNAME_RELEASE}
+       exit 0 ;;
+    SX-5:SUPER-UX:*:*)
+       echo sx5-nec-superux${UNAME_RELEASE}
+       exit 0 ;;
+    Power*:Rhapsody:*:*)
+       echo powerpc-apple-rhapsody${UNAME_RELEASE}
+       exit 0 ;;
+    *:Rhapsody:*:*)
+       echo ${UNAME_MACHINE}-apple-rhapsody${UNAME_RELEASE}
+       exit 0 ;;
+    *:Darwin:*:*)
+       echo `uname -p`-apple-darwin${UNAME_RELEASE}
+       exit 0 ;;
+    *:procnto*:*:* | *:QNX:[0123456789]*:*)
+       if test "${UNAME_MACHINE}" = "x86pc"; then
+               UNAME_MACHINE=pc
+       fi
+       echo `uname -p`-${UNAME_MACHINE}-nto-qnx
+       exit 0 ;;
+    *:QNX:*:4*)
+       echo i386-pc-qnx
+       exit 0 ;;
+    NSR-[KW]:NONSTOP_KERNEL:*:*)
+       echo nsr-tandem-nsk${UNAME_RELEASE}
+       exit 0 ;;
+    *:NonStop-UX:*:*)
+       echo mips-compaq-nonstopux
+       exit 0 ;;
+    BS2000:POSIX*:*:*)
+       echo bs2000-siemens-sysv
+       exit 0 ;;
+    DS/*:UNIX_System_V:*:*)
+       echo ${UNAME_MACHINE}-${UNAME_SYSTEM}-${UNAME_RELEASE}
+       exit 0 ;;
+    *:Plan9:*:*)
+       # "uname -m" is not consistent, so use $cputype instead. 386
+       # is converted to i386 for consistency with other x86
+       # operating systems.
+       if test "$cputype" = "386"; then
+           UNAME_MACHINE=i386
+       else
+           UNAME_MACHINE="$cputype"
+       fi
+       echo ${UNAME_MACHINE}-unknown-plan9
+       exit 0 ;;
+    i?86:OS/2:*:*)
+       # If we were able to find `uname', then EMX Unix compatibility
+       # is probably installed.
+       echo ${UNAME_MACHINE}-pc-os2-emx
+       exit 0 ;;
+esac
+
+#echo '(No uname command or uname output not recognized.)' 1>&2
+#echo "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" 1>&2
+
+cat >$dummy.c <<EOF
+#ifdef _SEQUENT_
+# include <sys/types.h>
+# include <sys/utsname.h>
+#endif
+main ()
+{
+#if defined (sony)
+#if defined (MIPSEB)
+  /* BFD wants "bsd" instead of "newsos".  Perhaps BFD should be changed,
+     I don't know....  */
+  printf ("mips-sony-bsd\n"); exit (0);
+#else
+#include <sys/param.h>
+  printf ("m68k-sony-newsos%s\n",
+#ifdef NEWSOS4
+          "4"
+#else
+         ""
+#endif
+         ); exit (0);
+#endif
+#endif
+
+#if defined (__arm) && defined (__acorn) && defined (__unix)
+  printf ("arm-acorn-riscix"); exit (0);
+#endif
+
+#if defined (hp300) && !defined (hpux)
+  printf ("m68k-hp-bsd\n"); exit (0);
+#endif
+
+#if defined (NeXT)
+#if !defined (__ARCHITECTURE__)
+#define __ARCHITECTURE__ "m68k"
+#endif
+  int version;
+  version=`(hostinfo | sed -n 's/.*NeXT Mach \([0-9]*\).*/\1/p') 2>/dev/null`;
+  if (version < 4)
+    printf ("%s-next-nextstep%d\n", __ARCHITECTURE__, version);
+  else
+    printf ("%s-next-openstep%d\n", __ARCHITECTURE__, version);
+  exit (0);
+#endif
+
+#if defined (MULTIMAX) || defined (n16)
+#if defined (UMAXV)
+  printf ("ns32k-encore-sysv\n"); exit (0);
+#else
+#if defined (CMU)
+  printf ("ns32k-encore-mach\n"); exit (0);
+#else
+  printf ("ns32k-encore-bsd\n"); exit (0);
+#endif
+#endif
+#endif
+
+#if defined (__386BSD__)
+  printf ("i386-pc-bsd\n"); exit (0);
+#endif
+
+#if defined (sequent)
+#if defined (i386)
+  printf ("i386-sequent-dynix\n"); exit (0);
+#endif
+#if defined (ns32000)
+  printf ("ns32k-sequent-dynix\n"); exit (0);
+#endif
+#endif
+
+#if defined (_SEQUENT_)
+    struct utsname un;
+
+    uname(&un);
+
+    if (strncmp(un.version, "V2", 2) == 0) {
+       printf ("i386-sequent-ptx2\n"); exit (0);
+    }
+    if (strncmp(un.version, "V1", 2) == 0) { /* XXX is V1 correct? */
+       printf ("i386-sequent-ptx1\n"); exit (0);
+    }
+    printf ("i386-sequent-ptx\n"); exit (0);
+
+#endif
+
+#if defined (vax)
+#if !defined (ultrix)
+  printf ("vax-dec-bsd\n"); exit (0);
+#else
+  printf ("vax-dec-ultrix\n"); exit (0);
+#endif
+#endif
+
+#if defined (alliant) && defined (i860)
+  printf ("i860-alliant-bsd\n"); exit (0);
+#endif
+
+  exit (1);
+}
+EOF
+
+$CC_FOR_BUILD $dummy.c -o $dummy 2>/dev/null && ./$dummy && rm $dummy.c $dummy && exit 0
+rm -f $dummy.c $dummy
+
+# Apollos put the system type in the environment.
+
+test -d /usr/apollo && { echo ${ISP}-apollo-${SYSTYPE}; exit 0; }
+
+# Convex versions that predate uname can use getsysinfo(1)
+
+if [ -x /usr/convex/getsysinfo ]
+then
+    case `getsysinfo -f cpu_type` in
+    c1*)
+       echo c1-convex-bsd
+       exit 0 ;;
+    c2*)
+       if getsysinfo -f scalar_acc
+       then echo c32-convex-bsd
+       else echo c2-convex-bsd
+       fi
+       exit 0 ;;
+    c34*)
+       echo c34-convex-bsd
+       exit 0 ;;
+    c38*)
+       echo c38-convex-bsd
+       exit 0 ;;
+    c4*)
+       echo c4-convex-bsd
+       exit 0 ;;
+    esac
+fi
+
+cat >&2 <<EOF
+$0: unable to guess system type
+
+The $version version of this script cannot recognize your system type.
+Please download the most up to date version of the config scripts:
+
+    ftp://ftp.gnu.org/pub/gnu/config/
+
+If the version you run ($0) is already up to date, please
+send the following data and any information you think might be
+pertinent to <config-patches@gnu.org> in order to provide the needed
+information to handle your system.
+
+config.guess version = $version
+
+uname -m = `(uname -m) 2>/dev/null || echo unknown`
+uname -r = `(uname -r) 2>/dev/null || echo unknown`
+uname -s = `(uname -s) 2>/dev/null || echo unknown`
+uname -v = `(uname -v) 2>/dev/null || echo unknown`
+
+/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null`
+/bin/uname -X     = `(/bin/uname -X) 2>/dev/null`
+
+hostinfo               = `(hostinfo) 2>/dev/null`
+/bin/universe          = `(/bin/universe) 2>/dev/null`
+/usr/bin/arch -k       = `(/usr/bin/arch -k) 2>/dev/null`
+/bin/arch              = `(/bin/arch) 2>/dev/null`
+/usr/bin/oslevel       = `(/usr/bin/oslevel) 2>/dev/null`
+/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null`
+
+UNAME_MACHINE = ${UNAME_MACHINE}
+UNAME_RELEASE = ${UNAME_RELEASE}
+UNAME_SYSTEM  = ${UNAME_SYSTEM}
+UNAME_VERSION = ${UNAME_VERSION}
+EOF
+
+exit 1
+
+# Local variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "timestamp='"
+# time-stamp-format: "%:y-%02m-%02d"
+# time-stamp-end: "'"
+# End:
diff --git a/config.sub b/config.sub
new file mode 100755 (executable)
index 0000000..923c57b
--- /dev/null
@@ -0,0 +1,1346 @@
+#! /bin/sh
+# Configuration validation subroutine script.
+#   Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000
+#   Free Software Foundation, Inc.
+
+timestamp='2000-12-20'
+
+# This file is (in principle) common to ALL GNU software.
+# The presence of a machine in this file suggests that SOME GNU software
+# can handle that machine.  It does not imply ALL GNU software can.
+#
+# This file is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# Please send patches to <config-patches@gnu.org>.
+#
+# Configuration subroutine to validate and canonicalize a configuration type.
+# Supply the specified configuration type as an argument.
+# If it is invalid, we print an error message on stderr and exit with code 1.
+# Otherwise, we print the canonical config type on stdout and succeed.
+
+# This file is supposed to be the same for all GNU packages
+# and recognize all the CPU types, system types and aliases
+# that are meaningful with *any* GNU software.
+# Each package is responsible for reporting which valid configurations
+# it does not support.  The user should be able to distinguish
+# a failure to support a valid configuration from a meaningless
+# configuration.
+
+# The goal of this file is to map all the various variations of a given
+# machine specification into a single specification in the form:
+#      CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM
+# or in some cases, the newer four-part form:
+#      CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM
+# It is wrong to echo any other type of specification.
+
+me=`echo "$0" | sed -e 's,.*/,,'`
+
+usage="\
+Usage: $0 [OPTION] CPU-MFR-OPSYS
+       $0 [OPTION] ALIAS
+
+Canonicalize a configuration name.
+
+Operation modes:
+  -h, --help         print this help, then exit
+  -t, --time-stamp   print date of last modification, then exit
+  -v, --version      print version number, then exit
+
+Report bugs and patches to <config-patches@gnu.org>."
+
+version="\
+GNU config.sub ($timestamp)
+
+Copyright (C) 1992, 93, 94, 95, 96, 97, 98, 99, 2000
+Free Software Foundation, Inc.
+
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
+
+help="
+Try \`$me --help' for more information."
+
+# Parse command line
+while test $# -gt 0 ; do
+  case $1 in
+    --time-stamp | --time* | -t )
+       echo "$timestamp" ; exit 0 ;;
+    --version | -v )
+       echo "$version" ; exit 0 ;;
+    --help | --h* | -h )
+       echo "$usage"; exit 0 ;;
+    -- )     # Stop option processing
+       shift; break ;;
+    - )        # Use stdin as input.
+       break ;;
+    -* )
+       echo "$me: invalid option $1$help"
+       exit 1 ;;
+
+    *local*)
+       # First pass through any local machine types.
+       echo $1
+       exit 0;;
+
+    * )
+       break ;;
+  esac
+done
+
+case $# in
+ 0) echo "$me: missing argument$help" >&2
+    exit 1;;
+ 1) ;;
+ *) echo "$me: too many arguments$help" >&2
+    exit 1;;
+esac
+
+# Separate what the user gave into CPU-COMPANY and OS or KERNEL-OS (if any).
+# Here we must recognize all the valid KERNEL-OS combinations.
+maybe_os=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\2/'`
+case $maybe_os in
+  nto-qnx* | linux-gnu* | storm-chaos*)
+    os=-$maybe_os
+    basic_machine=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'`
+    ;;
+  *)
+    basic_machine=`echo $1 | sed 's/-[^-]*$//'`
+    if [ $basic_machine != $1 ]
+    then os=`echo $1 | sed 's/.*-/-/'`
+    else os=; fi
+    ;;
+esac
+
+### Let's recognize common machines as not being operating systems so
+### that things like config.sub decstation-3100 work.  We also
+### recognize some manufacturers as not being operating systems, so we
+### can provide default operating systems below.
+case $os in
+       -sun*os*)
+               # Prevent following clause from handling this invalid input.
+               ;;
+       -dec* | -mips* | -sequent* | -encore* | -pc532* | -sgi* | -sony* | \
+       -att* | -7300* | -3300* | -delta* | -motorola* | -sun[234]* | \
+       -unicom* | -ibm* | -next | -hp | -isi* | -apollo | -altos* | \
+       -convergent* | -ncr* | -news | -32* | -3600* | -3100* | -hitachi* |\
+       -c[123]* | -convex* | -sun | -crds | -omron* | -dg | -ultra | -tti* | \
+       -harris | -dolphin | -highlevel | -gould | -cbm | -ns | -masscomp | \
+       -apple | -axis)
+               os=
+               basic_machine=$1
+               ;;
+       -sim | -cisco | -oki | -wec | -winbond)
+               os=
+               basic_machine=$1
+               ;;
+       -scout)
+               ;;
+       -wrs)
+               os=-vxworks
+               basic_machine=$1
+               ;;
+       -hiux*)
+               os=-hiuxwe2
+               ;;
+       -sco5)
+               os=-sco3.2v5
+               basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+               ;;
+       -sco4)
+               os=-sco3.2v4
+               basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+               ;;
+       -sco3.2.[4-9]*)
+               os=`echo $os | sed -e 's/sco3.2./sco3.2v/'`
+               basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+               ;;
+       -sco3.2v[4-9]*)
+               # Don't forget version if it is 3.2v4 or newer.
+               basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+               ;;
+       -sco*)
+               os=-sco3.2v2
+               basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+               ;;
+       -udk*)
+               basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+               ;;
+       -isc)
+               os=-isc2.2
+               basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+               ;;
+       -clix*)
+               basic_machine=clipper-intergraph
+               ;;
+       -isc*)
+               basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+               ;;
+       -lynx*)
+               os=-lynxos
+               ;;
+       -ptx*)
+               basic_machine=`echo $1 | sed -e 's/86-.*/86-sequent/'`
+               ;;
+       -windowsnt*)
+               os=`echo $os | sed -e 's/windowsnt/winnt/'`
+               ;;
+       -psos*)
+               os=-psos
+               ;;
+       -mint | -mint[0-9]*)
+               basic_machine=m68k-atari
+               os=-mint
+               ;;
+esac
+
+# Decode aliases for certain CPU-COMPANY combinations.
+case $basic_machine in
+       # Recognize the basic CPU types without company name.
+       # Some are omitted here because they have special meanings below.
+       tahoe | i860 | ia64 | m32r | m68k | m68000 | m88k | ns32k | arc \
+               | arm | arme[lb] | arm[bl]e | armv[2345] | armv[345][lb] | strongarm | xscale \
+               | pyramid | mn10200 | mn10300 | tron | a29k \
+               | 580 | i960 | h8300 \
+               | x86 | ppcbe | mipsbe | mipsle | shbe | shle \
+               | hppa | hppa1.0 | hppa1.1 | hppa2.0 | hppa2.0w | hppa2.0n \
+               | hppa64 \
+               | alpha | alphaev[4-8] | alphaev56 | alphapca5[67] \
+               | alphaev6[78] \
+               | we32k | ns16k | clipper | i370 | sh | sh[34] \
+               | powerpc | powerpcle \
+               | 1750a | dsp16xx | pdp11 | mips16 | mips64 | mipsel | mips64el \
+               | mips64orion | mips64orionel | mipstx39 | mipstx39el \
+               | mips64vr4300 | mips64vr4300el | mips64vr4100 | mips64vr4100el \
+               | mips64vr5000 | miprs64vr5000el | mcore \
+               | sparc | sparclet | sparclite | sparc64 | sparcv9 | v850 | c4x \
+               | thumb | d10v | d30v | fr30 | avr)
+               basic_machine=$basic_machine-unknown
+               ;;
+       m6811 | m68hc11 | m6812 | m68hc12)
+               # Motorola 68HC11/12.
+               basic_machine=$basic_machine-unknown
+               os=-none
+               ;;
+       m88110 | m680[12346]0 | m683?2 | m68360 | m5200 | z8k | v70 | h8500 | w65 | pj | pjl)
+               ;;
+
+       # We use `pc' rather than `unknown'
+       # because (1) that's what they normally are, and
+       # (2) the word "unknown" tends to confuse beginning users.
+       i[234567]86 | x86_64)
+         basic_machine=$basic_machine-pc
+         ;;
+       # Object if more than one company name word.
+       *-*-*)
+               echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2
+               exit 1
+               ;;
+       # Recognize the basic CPU types with company name.
+       # FIXME: clean up the formatting here.
+       vax-* | tahoe-* | i[234567]86-* | i860-* | ia64-* | m32r-* | m68k-* | m68000-* \
+             | m88k-* | sparc-* | ns32k-* | fx80-* | arc-* | c[123]* \
+             | arm-*  | armbe-* | armle-* | armv*-* | strongarm-* | xscale-* \
+             | mips-* | pyramid-* | tron-* | a29k-* | romp-* | rs6000-* \
+             | power-* | none-* | 580-* | cray2-* | h8300-* | h8500-* | i960-* \
+             | xmp-* | ymp-* \
+             | x86-* | ppcbe-* | mipsbe-* | mipsle-* | shbe-* | shle-* \
+             | hppa-* | hppa1.0-* | hppa1.1-* | hppa2.0-* | hppa2.0w-* \
+             | hppa2.0n-* | hppa64-* \
+             | alpha-* | alphaev[4-8]-* | alphaev56-* | alphapca5[67]-* \
+             | alphaev6[78]-* \
+             | we32k-* | cydra-* | ns16k-* | pn-* | np1-* | xps100-* \
+             | clipper-* | orion-* \
+             | sparclite-* | pdp11-* | sh-* | powerpc-* | powerpcle-* \
+             | sparc64-* | sparcv9-* | sparc86x-* | mips16-* | mips64-* | mipsel-* \
+             | mips64el-* | mips64orion-* | mips64orionel-* \
+             | mips64vr4100-* | mips64vr4100el-* | mips64vr4300-* | mips64vr4300el-* \
+             | mipstx39-* | mipstx39el-* | mcore-* \
+             | f30[01]-* | f700-* | s390-* | sv1-* | t3e-* \
+             | m88110-* | m680[01234]0-* | m683?2-* | m68360-* | z8k-* | d10v-* \
+             | thumb-* | v850-* | d30v-* | tic30-* | c30-* | fr30-* \
+             | bs2000-* | tic54x-* | c54x-* | x86_64-*)
+               ;;
+       # Recognize the various machine names and aliases which stand
+       # for a CPU type and a company and sometimes even an OS.
+       386bsd)
+               basic_machine=i386-unknown
+               os=-bsd
+               ;;
+       3b1 | 7300 | 7300-att | att-7300 | pc7300 | safari | unixpc)
+               basic_machine=m68000-att
+               ;;
+       3b*)
+               basic_machine=we32k-att
+               ;;
+       a29khif)
+               basic_machine=a29k-amd
+               os=-udi
+               ;;
+       adobe68k)
+               basic_machine=m68010-adobe
+               os=-scout
+               ;;
+       alliant | fx80)
+               basic_machine=fx80-alliant
+               ;;
+       altos | altos3068)
+               basic_machine=m68k-altos
+               ;;
+       am29k)
+               basic_machine=a29k-none
+               os=-bsd
+               ;;
+       amdahl)
+               basic_machine=580-amdahl
+               os=-sysv
+               ;;
+       amiga | amiga-*)
+               basic_machine=m68k-unknown
+               ;;
+       amigaos | amigados)
+               basic_machine=m68k-unknown
+               os=-amigaos
+               ;;
+       amigaunix | amix)
+               basic_machine=m68k-unknown
+               os=-sysv4
+               ;;
+       apollo68)
+               basic_machine=m68k-apollo
+               os=-sysv
+               ;;
+       apollo68bsd)
+               basic_machine=m68k-apollo
+               os=-bsd
+               ;;
+       aux)
+               basic_machine=m68k-apple
+               os=-aux
+               ;;
+       balance)
+               basic_machine=ns32k-sequent
+               os=-dynix
+               ;;
+       convex-c1)
+               basic_machine=c1-convex
+               os=-bsd
+               ;;
+       convex-c2)
+               basic_machine=c2-convex
+               os=-bsd
+               ;;
+       convex-c32)
+               basic_machine=c32-convex
+               os=-bsd
+               ;;
+       convex-c34)
+               basic_machine=c34-convex
+               os=-bsd
+               ;;
+       convex-c38)
+               basic_machine=c38-convex
+               os=-bsd
+               ;;
+       cray | ymp)
+               basic_machine=ymp-cray
+               os=-unicos
+               ;;
+       cray2)
+               basic_machine=cray2-cray
+               os=-unicos
+               ;;
+       [ctj]90-cray)
+               basic_machine=c90-cray
+               os=-unicos
+               ;;
+       crds | unos)
+               basic_machine=m68k-crds
+               ;;
+       cris | cris-* | etrax*)
+               basic_machine=cris-axis
+               ;;
+       da30 | da30-*)
+               basic_machine=m68k-da30
+               ;;
+       decstation | decstation-3100 | pmax | pmax-* | pmin | dec3100 | decstatn)
+               basic_machine=mips-dec
+               ;;
+       delta | 3300 | motorola-3300 | motorola-delta \
+             | 3300-motorola | delta-motorola)
+               basic_machine=m68k-motorola
+               ;;
+       delta88)
+               basic_machine=m88k-motorola
+               os=-sysv3
+               ;;
+       dpx20 | dpx20-*)
+               basic_machine=rs6000-bull
+               os=-bosx
+               ;;
+       dpx2* | dpx2*-bull)
+               basic_machine=m68k-bull
+               os=-sysv3
+               ;;
+       ebmon29k)
+               basic_machine=a29k-amd
+               os=-ebmon
+               ;;
+       elxsi)
+               basic_machine=elxsi-elxsi
+               os=-bsd
+               ;;
+       encore | umax | mmax)
+               basic_machine=ns32k-encore
+               ;;
+       es1800 | OSE68k | ose68k | ose | OSE)
+               basic_machine=m68k-ericsson
+               os=-ose
+               ;;
+       fx2800)
+               basic_machine=i860-alliant
+               ;;
+       genix)
+               basic_machine=ns32k-ns
+               ;;
+       gmicro)
+               basic_machine=tron-gmicro
+               os=-sysv
+               ;;
+       h3050r* | hiux*)
+               basic_machine=hppa1.1-hitachi
+               os=-hiuxwe2
+               ;;
+       h8300hms)
+               basic_machine=h8300-hitachi
+               os=-hms
+               ;;
+       h8300xray)
+               basic_machine=h8300-hitachi
+               os=-xray
+               ;;
+       h8500hms)
+               basic_machine=h8500-hitachi
+               os=-hms
+               ;;
+       harris)
+               basic_machine=m88k-harris
+               os=-sysv3
+               ;;
+       hp300-*)
+               basic_machine=m68k-hp
+               ;;
+       hp300bsd)
+               basic_machine=m68k-hp
+               os=-bsd
+               ;;
+       hp300hpux)
+               basic_machine=m68k-hp
+               os=-hpux
+               ;;
+       hp3k9[0-9][0-9] | hp9[0-9][0-9])
+               basic_machine=hppa1.0-hp
+               ;;
+       hp9k2[0-9][0-9] | hp9k31[0-9])
+               basic_machine=m68000-hp
+               ;;
+       hp9k3[2-9][0-9])
+               basic_machine=m68k-hp
+               ;;
+       hp9k6[0-9][0-9] | hp6[0-9][0-9])
+               basic_machine=hppa1.0-hp
+               ;;
+       hp9k7[0-79][0-9] | hp7[0-79][0-9])
+               basic_machine=hppa1.1-hp
+               ;;
+       hp9k78[0-9] | hp78[0-9])
+               # FIXME: really hppa2.0-hp
+               basic_machine=hppa1.1-hp
+               ;;
+       hp9k8[67]1 | hp8[67]1 | hp9k80[24] | hp80[24] | hp9k8[78]9 | hp8[78]9 | hp9k893 | hp893)
+               # FIXME: really hppa2.0-hp
+               basic_machine=hppa1.1-hp
+               ;;
+       hp9k8[0-9][13679] | hp8[0-9][13679])
+               basic_machine=hppa1.1-hp
+               ;;
+       hp9k8[0-9][0-9] | hp8[0-9][0-9])
+               basic_machine=hppa1.0-hp
+               ;;
+       hppa-next)
+               os=-nextstep3
+               ;;
+       hppaosf)
+               basic_machine=hppa1.1-hp
+               os=-osf
+               ;;
+       hppro)
+               basic_machine=hppa1.1-hp
+               os=-proelf
+               ;;
+       i370-ibm* | ibm*)
+               basic_machine=i370-ibm
+               ;;
+# I'm not sure what "Sysv32" means.  Should this be sysv3.2?
+       i[34567]86v32)
+               basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+               os=-sysv32
+               ;;
+       i[34567]86v4*)
+               basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+               os=-sysv4
+               ;;
+       i[34567]86v)
+               basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+               os=-sysv
+               ;;
+       i[34567]86sol2)
+               basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+               os=-solaris2
+               ;;
+       i386mach)
+               basic_machine=i386-mach
+               os=-mach
+               ;;
+       i386-vsta | vsta)
+               basic_machine=i386-unknown
+               os=-vsta
+               ;;
+       i386-go32 | go32)
+               basic_machine=i386-unknown
+               os=-go32
+               ;;
+       i386-mingw32 | mingw32)
+               basic_machine=i386-unknown
+               os=-mingw32
+               ;;
+       i[34567]86-pw32 | pw32)
+               basic_machine=i586-unknown
+               os=-pw32
+               ;;
+       iris | iris4d)
+               basic_machine=mips-sgi
+               case $os in
+                   -irix*)
+                       ;;
+                   *)
+                       os=-irix4
+                       ;;
+               esac
+               ;;
+       isi68 | isi)
+               basic_machine=m68k-isi
+               os=-sysv
+               ;;
+       m88k-omron*)
+               basic_machine=m88k-omron
+               ;;
+       magnum | m3230)
+               basic_machine=mips-mips
+               os=-sysv
+               ;;
+       merlin)
+               basic_machine=ns32k-utek
+               os=-sysv
+               ;;
+       miniframe)
+               basic_machine=m68000-convergent
+               ;;
+       *mint | -mint[0-9]* | *MiNT | *MiNT[0-9]*)
+               basic_machine=m68k-atari
+               os=-mint
+               ;;
+       mipsel*-linux*)
+               basic_machine=mipsel-unknown
+               os=-linux-gnu
+               ;;
+       mips*-linux*)
+               basic_machine=mips-unknown
+               os=-linux-gnu
+               ;;
+       mips3*-*)
+               basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'`
+               ;;
+       mips3*)
+               basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'`-unknown
+               ;;
+       mmix*)
+               basic_machine=mmix-knuth
+               os=-mmixware
+               ;;
+       monitor)
+               basic_machine=m68k-rom68k
+               os=-coff
+               ;;
+       msdos)
+               basic_machine=i386-unknown
+               os=-msdos
+               ;;
+       mvs)
+               basic_machine=i370-ibm
+               os=-mvs
+               ;;
+       ncr3000)
+               basic_machine=i486-ncr
+               os=-sysv4
+               ;;
+       netbsd386)
+               basic_machine=i386-unknown
+               os=-netbsd
+               ;;
+       netwinder)
+               basic_machine=armv4l-rebel
+               os=-linux
+               ;;
+       news | news700 | news800 | news900)
+               basic_machine=m68k-sony
+               os=-newsos
+               ;;
+       news1000)
+               basic_machine=m68030-sony
+               os=-newsos
+               ;;
+       news-3600 | risc-news)
+               basic_machine=mips-sony
+               os=-newsos
+               ;;
+       necv70)
+               basic_machine=v70-nec
+               os=-sysv
+               ;;
+       next | m*-next )
+               basic_machine=m68k-next
+               case $os in
+                   -nextstep* )
+                       ;;
+                   -ns2*)
+                     os=-nextstep2
+                       ;;
+                   *)
+                     os=-nextstep3
+                       ;;
+               esac
+               ;;
+       nh3000)
+               basic_machine=m68k-harris
+               os=-cxux
+               ;;
+       nh[45]000)
+               basic_machine=m88k-harris
+               os=-cxux
+               ;;
+       nindy960)
+               basic_machine=i960-intel
+               os=-nindy
+               ;;
+       mon960)
+               basic_machine=i960-intel
+               os=-mon960
+               ;;
+       nonstopux)
+               basic_machine=mips-compaq
+               os=-nonstopux
+               ;;
+       np1)
+               basic_machine=np1-gould
+               ;;
+       nsr-tandem)
+               basic_machine=nsr-tandem
+               ;;
+       op50n-* | op60c-*)
+               basic_machine=hppa1.1-oki
+               os=-proelf
+               ;;
+       OSE68000 | ose68000)
+               basic_machine=m68000-ericsson
+               os=-ose
+               ;;
+       os68k)
+               basic_machine=m68k-none
+               os=-os68k
+               ;;
+       pa-hitachi)
+               basic_machine=hppa1.1-hitachi
+               os=-hiuxwe2
+               ;;
+       paragon)
+               basic_machine=i860-intel
+               os=-osf
+               ;;
+       pbd)
+               basic_machine=sparc-tti
+               ;;
+       pbb)
+               basic_machine=m68k-tti
+               ;;
+        pc532 | pc532-*)
+               basic_machine=ns32k-pc532
+               ;;
+       pentium | p5 | k5 | k6 | nexgen)
+               basic_machine=i586-pc
+               ;;
+       pentiumpro | p6 | 6x86 | athlon)
+               basic_machine=i686-pc
+               ;;
+       pentiumii | pentium2)
+               basic_machine=i686-pc
+               ;;
+       pentium-* | p5-* | k5-* | k6-* | nexgen-*)
+               basic_machine=i586-`echo $basic_machine | sed 's/^[^-]*-//'`
+               ;;
+       pentiumpro-* | p6-* | 6x86-* | athlon-*)
+               basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'`
+               ;;
+       pentiumii-* | pentium2-*)
+               basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'`
+               ;;
+       pn)
+               basic_machine=pn-gould
+               ;;
+       power)  basic_machine=power-ibm
+               ;;
+       ppc)    basic_machine=powerpc-unknown
+               ;;
+       ppc-*)  basic_machine=powerpc-`echo $basic_machine | sed 's/^[^-]*-//'`
+               ;;
+       ppcle | powerpclittle | ppc-le | powerpc-little)
+               basic_machine=powerpcle-unknown
+               ;;
+       ppcle-* | powerpclittle-*)
+               basic_machine=powerpcle-`echo $basic_machine | sed 's/^[^-]*-//'`
+               ;;
+       ps2)
+               basic_machine=i386-ibm
+               ;;
+       rom68k)
+               basic_machine=m68k-rom68k
+               os=-coff
+               ;;
+       rm[46]00)
+               basic_machine=mips-siemens
+               ;;
+       rtpc | rtpc-*)
+               basic_machine=romp-ibm
+               ;;
+       sa29200)
+               basic_machine=a29k-amd
+               os=-udi
+               ;;
+       sequent)
+               basic_machine=i386-sequent
+               ;;
+       sh)
+               basic_machine=sh-hitachi
+               os=-hms
+               ;;
+       sparclite-wrs)
+               basic_machine=sparclite-wrs
+               os=-vxworks
+               ;;
+       sps7)
+               basic_machine=m68k-bull
+               os=-sysv2
+               ;;
+       spur)
+               basic_machine=spur-unknown
+               ;;
+       st2000)
+               basic_machine=m68k-tandem
+               ;;
+       stratus)
+               basic_machine=i860-stratus
+               os=-sysv4
+               ;;
+       sun2)
+               basic_machine=m68000-sun
+               ;;
+       sun2os3)
+               basic_machine=m68000-sun
+               os=-sunos3
+               ;;
+       sun2os4)
+               basic_machine=m68000-sun
+               os=-sunos4
+               ;;
+       sun3os3)
+               basic_machine=m68k-sun
+               os=-sunos3
+               ;;
+       sun3os4)
+               basic_machine=m68k-sun
+               os=-sunos4
+               ;;
+       sun4os3)
+               basic_machine=sparc-sun
+               os=-sunos3
+               ;;
+       sun4os4)
+               basic_machine=sparc-sun
+               os=-sunos4
+               ;;
+       sun4sol2)
+               basic_machine=sparc-sun
+               os=-solaris2
+               ;;
+       sun3 | sun3-*)
+               basic_machine=m68k-sun
+               ;;
+       sun4)
+               basic_machine=sparc-sun
+               ;;
+       sun386 | sun386i | roadrunner)
+               basic_machine=i386-sun
+               ;;
+       sv1)
+               basic_machine=sv1-cray
+               os=-unicos
+               ;;
+       symmetry)
+               basic_machine=i386-sequent
+               os=-dynix
+               ;;
+       t3e)
+               basic_machine=t3e-cray
+               os=-unicos
+               ;;
+       tic54x | c54x*)
+               basic_machine=tic54x-unknown
+               os=-coff
+               ;;
+       tx39)
+               basic_machine=mipstx39-unknown
+               ;;
+       tx39el)
+               basic_machine=mipstx39el-unknown
+               ;;
+       tower | tower-32)
+               basic_machine=m68k-ncr
+               ;;
+       udi29k)
+               basic_machine=a29k-amd
+               os=-udi
+               ;;
+       ultra3)
+               basic_machine=a29k-nyu
+               os=-sym1
+               ;;
+       v810 | necv810)
+               basic_machine=v810-nec
+               os=-none
+               ;;
+       vaxv)
+               basic_machine=vax-dec
+               os=-sysv
+               ;;
+       vms)
+               basic_machine=vax-dec
+               os=-vms
+               ;;
+       vpp*|vx|vx-*)
+               basic_machine=f301-fujitsu
+               ;;
+       vxworks960)
+               basic_machine=i960-wrs
+               os=-vxworks
+               ;;
+       vxworks68)
+               basic_machine=m68k-wrs
+               os=-vxworks
+               ;;
+       vxworks29k)
+               basic_machine=a29k-wrs
+               os=-vxworks
+               ;;
+       w65*)
+               basic_machine=w65-wdc
+               os=-none
+               ;;
+       w89k-*)
+               basic_machine=hppa1.1-winbond
+               os=-proelf
+               ;;
+       xmp)
+               basic_machine=xmp-cray
+               os=-unicos
+               ;;
+        xps | xps100)
+               basic_machine=xps100-honeywell
+               ;;
+       z8k-*-coff)
+               basic_machine=z8k-unknown
+               os=-sim
+               ;;
+       none)
+               basic_machine=none-none
+               os=-none
+               ;;
+
+# Here we handle the default manufacturer of certain CPU types.  It is in
+# some cases the only manufacturer, in others, it is the most popular.
+       w89k)
+               basic_machine=hppa1.1-winbond
+               ;;
+       op50n)
+               basic_machine=hppa1.1-oki
+               ;;
+       op60c)
+               basic_machine=hppa1.1-oki
+               ;;
+       mips)
+               if [ x$os = x-linux-gnu ]; then
+                       basic_machine=mips-unknown
+               else
+                       basic_machine=mips-mips
+               fi
+               ;;
+       romp)
+               basic_machine=romp-ibm
+               ;;
+       rs6000)
+               basic_machine=rs6000-ibm
+               ;;
+       vax)
+               basic_machine=vax-dec
+               ;;
+       pdp11)
+               basic_machine=pdp11-dec
+               ;;
+       we32k)
+               basic_machine=we32k-att
+               ;;
+       sh3 | sh4)
+               basic_machine=sh-unknown
+               ;;
+       sparc | sparcv9)
+               basic_machine=sparc-sun
+               ;;
+        cydra)
+               basic_machine=cydra-cydrome
+               ;;
+       orion)
+               basic_machine=orion-highlevel
+               ;;
+       orion105)
+               basic_machine=clipper-highlevel
+               ;;
+       mac | mpw | mac-mpw)
+               basic_machine=m68k-apple
+               ;;
+       pmac | pmac-mpw)
+               basic_machine=powerpc-apple
+               ;;
+       c4x*)
+               basic_machine=c4x-none
+               os=-coff
+               ;;
+       *)
+               echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2
+               exit 1
+               ;;
+esac
+
+# Here we canonicalize certain aliases for manufacturers.
+case $basic_machine in
+       *-digital*)
+               basic_machine=`echo $basic_machine | sed 's/digital.*/dec/'`
+               ;;
+       *-commodore*)
+               basic_machine=`echo $basic_machine | sed 's/commodore.*/cbm/'`
+               ;;
+       *)
+               ;;
+esac
+
+# Decode manufacturer-specific aliases for certain operating systems.
+
+if [ x"$os" != x"" ]
+then
+case $os in
+        # First match some system type aliases
+        # that might get confused with valid system types.
+       # -solaris* is a basic system type, with this one exception.
+       -solaris1 | -solaris1.*)
+               os=`echo $os | sed -e 's|solaris1|sunos4|'`
+               ;;
+       -solaris)
+               os=-solaris2
+               ;;
+       -svr4*)
+               os=-sysv4
+               ;;
+       -unixware*)
+               os=-sysv4.2uw
+               ;;
+       -gnu/linux*)
+               os=`echo $os | sed -e 's|gnu/linux|linux-gnu|'`
+               ;;
+       # First accept the basic system types.
+       # The portable systems comes first.
+       # Each alternative MUST END IN A *, to match a version number.
+       # -sysv* is not here because it comes later, after sysvr4.
+       -gnu* | -bsd* | -mach* | -minix* | -genix* | -ultrix* | -irix* \
+             | -*vms* | -sco* | -esix* | -isc* | -aix* | -sunos | -sunos[34]*\
+             | -hpux* | -unos* | -osf* | -luna* | -dgux* | -solaris* | -sym* \
+             | -amigaos* | -amigados* | -msdos* | -newsos* | -unicos* | -aof* \
+             | -aos* \
+             | -nindy* | -vxsim* | -vxworks* | -ebmon* | -hms* | -mvs* \
+             | -clix* | -riscos* | -uniplus* | -iris* | -rtu* | -xenix* \
+             | -hiux* | -386bsd* | -netbsd* | -openbsd* | -freebsd* | -riscix* \
+             | -lynxos* | -bosx* | -nextstep* | -cxux* | -aout* | -elf* | -oabi* \
+             | -ptx* | -coff* | -ecoff* | -winnt* | -domain* | -vsta* \
+             | -udi* | -eabi* | -lites* | -ieee* | -go32* | -aux* \
+             | -cygwin* | -pe* | -psos* | -moss* | -proelf* | -rtems* \
+             | -mingw32* | -linux-gnu* | -uxpv* | -beos* | -mpeix* | -udk* \
+             | -interix* | -uwin* | -rhapsody* | -darwin* | -opened* \
+             | -openstep* | -oskit* | -conix* | -pw32* | -nonstopux* | -storm-chaos*)
+       # Remember, each alternative MUST END IN *, to match a version number.
+               ;;
+       -qnx*)
+               case $basic_machine in
+                   x86-* | i[34567]86-*)
+                       ;;
+                   *)
+                       os=-nto$os
+                       ;;
+               esac
+               ;;
+       -nto*)
+               os=-nto-qnx
+               ;;
+       -sim | -es1800* | -hms* | -xray | -os68k* | -none* | -v88r* \
+             | -windows* | -osx | -abug | -netware* | -os9* | -beos* \
+             | -macos* | -mpw* | -magic* | -mmixware* | -mon960* | -lnews*)
+               ;;
+       -mac*)
+               os=`echo $os | sed -e 's|mac|macos|'`
+               ;;
+       -linux*)
+               os=`echo $os | sed -e 's|linux|linux-gnu|'`
+               ;;
+       -sunos5*)
+               os=`echo $os | sed -e 's|sunos5|solaris2|'`
+               ;;
+       -sunos6*)
+               os=`echo $os | sed -e 's|sunos6|solaris3|'`
+               ;;
+       -opened*)
+               os=-openedition
+               ;;
+       -wince*)
+               os=-wince
+               ;;
+       -osfrose*)
+               os=-osfrose
+               ;;
+       -osf*)
+               os=-osf
+               ;;
+       -utek*)
+               os=-bsd
+               ;;
+       -dynix*)
+               os=-bsd
+               ;;
+       -acis*)
+               os=-aos
+               ;;
+       -386bsd)
+               os=-bsd
+               ;;
+       -ctix* | -uts*)
+               os=-sysv
+               ;;
+       -ns2 )
+               os=-nextstep2
+               ;;
+       -nsk*)
+               os=-nsk
+               ;;
+       # Preserve the version number of sinix5.
+       -sinix5.*)
+               os=`echo $os | sed -e 's|sinix|sysv|'`
+               ;;
+       -sinix*)
+               os=-sysv4
+               ;;
+       -triton*)
+               os=-sysv3
+               ;;
+       -oss*)
+               os=-sysv3
+               ;;
+       -svr4)
+               os=-sysv4
+               ;;
+       -svr3)
+               os=-sysv3
+               ;;
+       -sysvr4)
+               os=-sysv4
+               ;;
+       # This must come after -sysvr4.
+       -sysv*)
+               ;;
+       -ose*)
+               os=-ose
+               ;;
+       -es1800*)
+               os=-ose
+               ;;
+       -xenix)
+               os=-xenix
+               ;;
+        -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*)
+               os=-mint
+               ;;
+       -none)
+               ;;
+       *)
+               # Get rid of the `-' at the beginning of $os.
+               os=`echo $os | sed 's/[^-]*-//'`
+               echo Invalid configuration \`$1\': system \`$os\' not recognized 1>&2
+               exit 1
+               ;;
+esac
+else
+
+# Here we handle the default operating systems that come with various machines.
+# The value should be what the vendor currently ships out the door with their
+# machine or put another way, the most popular os provided with the machine.
+
+# Note that if you're going to try to match "-MANUFACTURER" here (say,
+# "-sun"), then you have to tell the case statement up towards the top
+# that MANUFACTURER isn't an operating system.  Otherwise, code above
+# will signal an error saying that MANUFACTURER isn't an operating
+# system, and we'll never get to this point.
+
+case $basic_machine in
+       *-acorn)
+               os=-riscix1.2
+               ;;
+       arm*-rebel)
+               os=-linux
+               ;;
+       arm*-semi)
+               os=-aout
+               ;;
+        pdp11-*)
+               os=-none
+               ;;
+       *-dec | vax-*)
+               os=-ultrix4.2
+               ;;
+       m68*-apollo)
+               os=-domain
+               ;;
+       i386-sun)
+               os=-sunos4.0.2
+               ;;
+       m68000-sun)
+               os=-sunos3
+               # This also exists in the configure program, but was not the
+               # default.
+               # os=-sunos4
+               ;;
+       m68*-cisco)
+               os=-aout
+               ;;
+       mips*-cisco)
+               os=-elf
+               ;;
+       mips*-*)
+               os=-elf
+               ;;
+       *-tti)  # must be before sparc entry or we get the wrong os.
+               os=-sysv3
+               ;;
+       sparc-* | *-sun)
+               os=-sunos4.1.1
+               ;;
+       *-be)
+               os=-beos
+               ;;
+       *-ibm)
+               os=-aix
+               ;;
+       *-wec)
+               os=-proelf
+               ;;
+       *-winbond)
+               os=-proelf
+               ;;
+       *-oki)
+               os=-proelf
+               ;;
+       *-hp)
+               os=-hpux
+               ;;
+       *-hitachi)
+               os=-hiux
+               ;;
+       i860-* | *-att | *-ncr | *-altos | *-motorola | *-convergent)
+               os=-sysv
+               ;;
+       *-cbm)
+               os=-amigaos
+               ;;
+       *-dg)
+               os=-dgux
+               ;;
+       *-dolphin)
+               os=-sysv3
+               ;;
+       m68k-ccur)
+               os=-rtu
+               ;;
+       m88k-omron*)
+               os=-luna
+               ;;
+       *-next )
+               os=-nextstep
+               ;;
+       *-sequent)
+               os=-ptx
+               ;;
+       *-crds)
+               os=-unos
+               ;;
+       *-ns)
+               os=-genix
+               ;;
+       i370-*)
+               os=-mvs
+               ;;
+       *-next)
+               os=-nextstep3
+               ;;
+        *-gould)
+               os=-sysv
+               ;;
+        *-highlevel)
+               os=-bsd
+               ;;
+       *-encore)
+               os=-bsd
+               ;;
+        *-sgi)
+               os=-irix
+               ;;
+        *-siemens)
+               os=-sysv4
+               ;;
+       *-masscomp)
+               os=-rtu
+               ;;
+       f30[01]-fujitsu | f700-fujitsu)
+               os=-uxpv
+               ;;
+       *-rom68k)
+               os=-coff
+               ;;
+       *-*bug)
+               os=-coff
+               ;;
+       *-apple)
+               os=-macos
+               ;;
+       *-atari*)
+               os=-mint
+               ;;
+       *)
+               os=-none
+               ;;
+esac
+fi
+
+# Here we handle the case where we know the os, and the CPU type, but not the
+# manufacturer.  We pick the logical manufacturer.
+vendor=unknown
+case $basic_machine in
+       *-unknown)
+               case $os in
+                       -riscix*)
+                               vendor=acorn
+                               ;;
+                       -sunos*)
+                               vendor=sun
+                               ;;
+                       -aix*)
+                               vendor=ibm
+                               ;;
+                       -beos*)
+                               vendor=be
+                               ;;
+                       -hpux*)
+                               vendor=hp
+                               ;;
+                       -mpeix*)
+                               vendor=hp
+                               ;;
+                       -hiux*)
+                               vendor=hitachi
+                               ;;
+                       -unos*)
+                               vendor=crds
+                               ;;
+                       -dgux*)
+                               vendor=dg
+                               ;;
+                       -luna*)
+                               vendor=omron
+                               ;;
+                       -genix*)
+                               vendor=ns
+                               ;;
+                       -mvs* | -opened*)
+                               vendor=ibm
+                               ;;
+                       -ptx*)
+                               vendor=sequent
+                               ;;
+                       -vxsim* | -vxworks*)
+                               vendor=wrs
+                               ;;
+                       -aux*)
+                               vendor=apple
+                               ;;
+                       -hms*)
+                               vendor=hitachi
+                               ;;
+                       -mpw* | -macos*)
+                               vendor=apple
+                               ;;
+                       -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*)
+                               vendor=atari
+                               ;;
+               esac
+               basic_machine=`echo $basic_machine | sed "s/unknown/$vendor/"`
+               ;;
+esac
+
+echo $basic_machine$os
+exit 0
+
+# Local variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "timestamp='"
+# time-stamp-format: "%:y-%02m-%02d"
+# time-stamp-end: "'"
+# End:
diff --git a/configure.in b/configure.in
new file mode 100644 (file)
index 0000000..f15ec5a
--- /dev/null
@@ -0,0 +1,296 @@
+dnl Process this file with autoconf to create a configure script.
+
+dnl General initialization.
+AC_REVISION([$Id: configure.in,v 1.77 2004/01/02 01:49:15 entrope Exp $])
+AC_PREREQ(2.57)
+AC_INIT(srvx, 1.3, srvx-bugs@lists.sourceforge.net)
+CODENAME=surge
+AC_CONFIG_HEADERS(src/config.h)
+AC_CONFIG_SRCDIR(src/opserv.c)
+dnl AM_CANONICAL_TARGET must be before AM_INIT_AUTOMAKE() or autoconf whines
+AC_CANONICAL_TARGET
+AM_INIT_AUTOMAKE([gnu 1.6])
+AM_MAINTAINER_MODE
+
+dnl Compiler/runtime feature checks.
+AC_TYPE_SIGNAL
+AC_C_CONST
+AC_C_INLINE
+
+dnl Checks for programs.
+AC_PROG_AWK
+AC_PROG_CC
+AC_PROG_RANLIB
+AC_PROG_INSTALL
+AC_PROG_LN_S
+AC_PROG_MAKE_SET
+AC_PROG_GCC_TRADITIONAL
+
+dnl nice that unixes can all follow a standard.
+case $target in
+  *-freebsd2* | *-freebsdelf2* | *-freebsd*out3*)
+    ANSI_SRC=""
+    ;;
+  *-freebsd3* | *-freebsdelf3* | *-freebsd*out3*)
+    ANSI_SRC=""
+    AC_DEFINE(BROKEN_REGEX, 1, [Define if the system regex library is unreliable.])
+        BROKEN_REGEX=yes
+    ;;
+  *-solaris*)
+    EXTRA_DEFINE="-D__SOLARIS__"
+    ANSI_SRC="-fno-builtin"
+    ;;
+  *-cygwin)
+    ANSI_SRC="-fno-builtin"
+    ;;
+  *-linux*)
+    dnl -D_GNU_SOURCE needed for strsignal()
+    EXTRA_DEFINE="-D_GNU_SOURCE"
+    ANSI_SRC=""
+    ;;
+  *)
+    ANSI_SRC=""
+    ;;
+esac
+CFLAGS="$CFLAGS $EXTRA_DEFINE"
+
+dnl Checks for libraries.
+AC_CHECK_LIB(socket, socket)
+AC_CHECK_LIB(nsl, gethostbyname)
+
+dnl Checks for header files.
+AC_HEADER_STDC
+
+dnl will be used for portability stuff
+AC_HEADER_TIME
+AC_STRUCT_TM
+
+dnl Would rather not bail on headers, BSD has alot of the functions elsewhere. -Jedi
+AC_CHECK_HEADERS(fcntl.h malloc.h netdb.h netinet/in.h sys/resource.h sys/timeb.h sys/times.h sys/param.h sys/socket.h sys/time.h sys/types.h sys/wait.h unistd.h getopt.h memory.h regex.h arpa/inet.h sys/mman.h sys/stat.h,,)
+
+dnl portability stuff, hurray! -Jedi
+AC_CHECK_FUNCS(gettimeofday)
+if test $ac_cv_func_gettimeofday = no; then
+  AC_CHECK_FUNCS(ftime,,AC_MSG_ERROR([ftime or gettimeofday required.  srvx build will fail.]))
+fi
+
+dnl We have fallbacks in case these are missing, so just check for them.
+AC_CHECK_FUNCS(bcopy memcpy memset strdup strerror strsignal localtime_r setrlimit inet_ntoa getopt getopt_long regcomp regexec regfree sysconf,,)
+
+dnl Check for absolutely required library functions.
+AC_CHECK_FUNCS(select socket strcspn strspn strtod strtoul,,AC_MSG_ERROR([a required function was not found.  srvx build will fail.]))
+
+dnl Check for functions (and how to get them).
+AC_FUNC_ALLOCA
+AC_FUNC_MMAP
+
+AC_CACHE_CHECK([for sin_len], ac_cv_sin_len,
+[AC_COMPILE_IFELSE([AC_LANG_PROGRAM([#include <sys/types.h>
+#include <netinet/in.h>],[struct sockaddr_in *sin; sin->sin_len = 0;])],
+ac_cv_sin_len="yes", ac_cv_sin_len="no")])
+if test $ac_cv_sin_len = yes ; then
+  AC_DEFINE(HAVE_SIN_LEN, 1, [Define if struct sockaddr_in contains a sin_len field])
+fi
+
+dnl Can only check with -Werror, but the rest of configure doesn't like -Werror
+OLD_CFLAGS=$CFLAGS
+CFLAGS="$CFLAGS -W -Wall -Werror"
+
+dnl Now figure out how to printf() a time_t
+AC_MSG_CHECKING(for time_t format)
+AC_CACHE_VAL(ac_cv_fmt_time_t, [
+ac_cv_fmt_time_t=no
+AC_COMPILE_IFELSE([#include <sys/types.h>
+#include <stdio.h>
+void myfunc(void) {
+  time_t test=0;
+  printf("%li", test);
+}], ac_cv_fmt_time_t="\"%li\"")
+if test $ac_cv_fmt_time_t = no; then
+AC_COMPILE_IFELSE([#include <sys/types.h>
+#include <stdio.h>
+void myfunc(void) {
+  time_t test=0;
+  printf("%i", test);
+}], ac_cv_fmt_time_t="\"%i\"")
+fi
+if test $ac_cv_fmt_time_t = no; then
+AC_MSG_ERROR([Cannot detect format string for time_t
+Please check sys/types.h for the typedef of time_t and submit to a developer])
+fi
+])
+AC_DEFINE_UNQUOTED(FMT_TIME_T, $ac_cv_fmt_time_t, [Define to printf format for a time_t variable])
+AC_MSG_RESULT($ac_cv_fmt_time_t)
+
+dnl How to copy one va_list to another?
+AC_CACHE_CHECK([for va_copy], ac_cv_c_va_copy, [AC_LINK_IFELSE(
+  [AC_LANG_PROGRAM([#include <stdarg.h>], [va_list ap1, ap2; va_copy(ap1, ap2);])],
+  [ac_cv_c_va_copy="yes"],
+  [ac_cv_c_va_copy="no"]
+)])
+if test "$ac_cv_c_va_copy" = "yes" ; then
+  AC_DEFINE(HAVE_VA_COPY, 1, [Define if we have va_copy])
+fi
+
+AC_CACHE_CHECK([for __va_copy], ac_cv_c___va_copy, [AC_LINK_IFELSE(
+  [AC_LANG_PROGRAM([#include <stdarg.h>], [va_list ap1, ap2; __va_copy(ap1, ap2);])],
+  [ac_cv_c___va_copy="yes"],
+  [ac_cv_c___va_copy="no"]
+)])
+if test "$ac_cv_c___va_copy" = "yes" ; then
+  AC_DEFINE(HAVE___VA_COPY, 1, [Define if we have __va_copy])
+fi
+
+dnl Now fix things back up
+CFLAGS=$OLD_CFLAGS
+
+dnl Optional features.
+AC_MSG_CHECKING(which malloc to use)
+AC_ARG_WITH(malloc,
+[  --with-malloc=type      Enables use of a special malloc library; one of:
+                          system (the default), boehm-gc, dmalloc, mpatrol],
+[],
+[withval="system"])
+if test "x$withval" = "xsystem" ; then
+  AC_MSG_RESULT(system)
+  AC_DEFINE(WITH_MALLOC_SYSTEM, 1, [Define if using the system's malloc])
+elif test "x$withval" = "xdmalloc" ; then
+  AC_MSG_RESULT(dmalloc)
+  AC_CHECK_HEADERS(dmalloc.h,,AC_MSG_ERROR([dmalloc header file missing.  dmalloc build will fail.]))
+  AC_CHECK_LIB(dmalloc,malloc,,AC_MSG_ERROR([dmalloc library is missing.  dmalloc build will fail.]))
+  AC_DEFINE(WITH_MALLOC_DMALLOC, 1, [Define if using the dmalloc debugging malloc package])
+elif test "x$withval" = "xmpatrol" ; then
+  AC_MSG_RESULT(mpatrol)
+  AC_CHECK_HEADERS(mpatrol.h,,AC_MSG_ERROR([mpatrol header file missing.  mpatrol build will fail.]))
+  dnl Using mpatrol requires linking against libelf, at least on Linux.
+  AC_CHECK_LIB(elf, elf_begin)
+  AC_CHECK_LIB(mpatrol,__mp_atexit,,AC_MSG_ERROR([mpatrol library is missing completely.  mpatrol build will fail.]))
+  AC_DEFINE(WITH_MALLOC_MPATROL, 1, [Define if using the mpatrol malloc debugging package])
+elif test "x$withval" = "xboehm-gc" ; then
+  AC_MSG_RESULT(boehm-gc)
+  AC_CHECK_HEADERS(gc/gc.h,,AC_MSG_ERROR([Boehm GC header file missing.  boehm-gc build will fail.]))
+  AC_CHECK_LIB(dl, dlopen, , AC_MSG_ERROR([libdl library is missing.  boehm-gc build will fail.]))
+  AC_CHECK_LIB(gc, GC_gcollect, , AC_MSG_ERROR([Boehm GC library is missing.  boehm-gc build will fail.]))
+  AC_DEFINE(WITH_MALLOC_BOEHM_GC, 1, [Define if using the Boehm GC to garbage collect and check memory leaks])
+else
+  AC_MSG_ERROR([Unknown malloc type $withval])
+fi
+
+AC_MSG_CHECKING(which protocol to use)
+AC_ARG_WITH(protocol,
+[  --with-protocol=name    Choose IRC dialect to support; one of:
+                          p10 (the default), bahamut],
+[],
+[withval="p10"])
+if test "x$withval" = "xp10" ; then
+  AC_MSG_RESULT(P10)
+  AC_DEFINE(WITH_PROTOCOL_P10, 1, [Define if using the P10 dialect of IRC])
+  MODULE_OBJS="$MODULE_OBJS proto-p10.\$(OBJEXT)"
+  PROTO_FILES=proto-p10.c
+elif test "x$withval" = "xbahamut" ; then
+  AC_MSG_RESULT(Bahamut)
+  AC_DEFINE(WITH_PROTOCOL_BAHAMUT, 1, [Define if using the Bahamut dialect of IRC])
+  MODULE_OBJS="$MODULE_OBJS proto-bahamut.\$(OBJEXT)"
+else
+  AC_MSG_ERROR([Unknown IRC dialect $withval])
+fi
+
+AC_ARG_WITH(getopt,
+[  --without-getopt        Disables building of the GNU getopt library],
+[if test "$withval" = no; then
+  AC_DEFINE(IGNORE_GETOPT, 1, [Define to disable built-in getopt library])
+fi])
+
+AC_MSG_CHECKING(whether to enable tokenization)
+AC_ARG_ENABLE(tokens,
+[  --disable-tokens        Disables tokenization of P10 protocol output
+                           (tokens required if linking to ircu 2.10.11)],
+[],[enableval=yes])
+if test "z$enableval" = zno ; then
+  AC_MSG_RESULT(no)
+else
+  AC_DEFINE(ENABLE_TOKENS, 1, [Define if tokenized P10 desired])
+  AC_MSG_RESULT(yes)
+fi
+
+AC_MSG_CHECKING(whether to enable debug behaviors)
+AC_ARG_ENABLE(debug,
+[  --enable-debug          Enables debugging behaviors],
+[
+  CPPFLAGS="$CPPFLAGS"
+  AC_MSG_RESULT(yes)
+],
+[
+  CPPFLAGS="$CPPFLAGS -DNDEBUG"
+  AC_MSG_RESULT(no)
+])
+
+AC_MSG_CHECKING(whether to enable lame tricks)
+AC_ARG_ENABLE(lame-tricks,
+[  --disable-lame-tricks   Disable lame ChanServ tricks (!wut, !unf, !ping, !8ball, !d, !huggle)],
+[if test "$enableval" = no; then
+   AC_DEFINE(NO_LAME_TRICKS, 1, [Define to disable lame ChanServ tricks])
+   AC_MSG_RESULT(no)
+ else
+   AC_MSG_RESULT(yes)
+ fi
+],
+[
+  AC_MSG_RESULT(yes)
+])
+
+if test -e src ; then
+  if test ! -d src ; then
+    AC_MSG_ERROR([src exists but is not a directory; please move it out of the way.])
+  fi
+else
+  mkdir src
+fi
+AC_MSG_CHECKING(for extra module files)
+MODULE_DEFINES="src/modules-list.h"
+echo > $MODULE_DEFINES
+touch $MODULE_DEFINES
+AC_ARG_ENABLE(modules,
+[  --enable-modules=list,of,modules   Enable extra modules],
+[
+  OIFS="$IFS"
+  IFS=','
+  EXTRA_MODULE_OBJS=""
+  module_list=""
+  dnl Must use a separate file because autoconf can't stand newlines in an AC_SUBSTed variable.
+  for module in $enableval ; do
+    module=`echo $module | sed -e s/^mod-// -e s/\.c\$//`
+    EXTRA_MODULE_OBJS="$EXTRA_MODULE_OBJS mod-$module.\$(OBJEXT)"
+    module_list="$module_list $module"
+    echo "WITH_MODULE($module)" >> $MODULE_DEFINES
+  done
+  IFS="$OIFS"
+  MODULE_OBJS="$MODULE_OBJS $EXTRA_MODULE_OBJS"
+  AC_DEFINE(EXTRA_MODULES, 1, [Define if there are extra modules to be initialized.])
+  AC_MSG_RESULT($module_list)
+],
+[
+  AC_MSG_RESULT(none)
+])
+
+MY_SUBDIRS=""
+RX_INCLUDES=""
+RX_LIBS=""
+if test "${BROKEN_REGEX}" = yes -o "${ac_cv_func_regcomp}" = no; then
+  MY_SUBDIRS="rx $MY_SUBDIRS"
+  RX_INCLUDES="-I../rx"
+  RX_LIBS="../rx/librx.a"
+fi
+MY_SUBDIRS="$MY_SUBDIRS src"
+CFLAGS="$CFLAGS $ANSI_SRC -W -Wall"
+if test "z$USE_MAINTAINER_MODE" = zyes ; then
+  CFLAGS="$CFLAGS -Werror"
+fi
+
+AC_DEFINE_UNQUOTED(CODENAME, "${CODENAME}", [Code name for this release])
+AC_SUBST(MODULE_OBJS)
+AC_SUBST(MY_SUBDIRS)
+AC_SUBST(RX_INCLUDES)
+AC_SUBST(RX_LIBS)
+AC_CONFIG_FILES(Makefile rx/Makefile src/Makefile)
+AC_OUTPUT
diff --git a/depcomp b/depcomp
new file mode 100755 (executable)
index 0000000..6589965
--- /dev/null
+++ b/depcomp
@@ -0,0 +1,411 @@
+#! /bin/sh
+
+# depcomp - compile a program generating dependencies as side-effects
+# Copyright 1999, 2000 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+
+# This program 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 General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+# 02111-1307, USA.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# Originally written by Alexandre Oliva <oliva@dcc.unicamp.br>.
+
+if test -z "$depmode" || test -z "$source" || test -z "$object"; then
+  echo "depcomp: Variables source, object and depmode must be set" 1>&2
+  exit 1
+fi
+# `libtool' can also be set to `yes' or `no'.
+
+depfile=${depfile-`echo "$object" | sed 's,\([^/]*\)$,.deps/\1,;s/\.\([^.]*\)$/.P\1/'`}
+tmpdepfile=${tmpdepfile-`echo "$depfile" | sed 's/\.\([^.]*\)$/.T\1/'`}
+
+rm -f "$tmpdepfile"
+
+# Some modes work just like other modes, but use different flags.  We
+# parameterize here, but still list the modes in the big case below,
+# to make depend.m4 easier to write.  Note that we *cannot* use a case
+# here, because this file can only contain one case statement.
+if test "$depmode" = hp; then
+  # HP compiler uses -M and no extra arg.
+  gccflag=-M
+  depmode=gcc
+fi
+
+if test "$depmode" = dashXmstdout; then
+   # This is just like dashmstdout with a different argument.
+   dashmflag=-xM
+   depmode=dashmstdout
+fi
+
+case "$depmode" in
+gcc3)
+## gcc 3 implements dependency tracking that does exactly what
+## we want.  Yay!  Note: for some reason libtool 1.4 doesn't like
+## it if -MD -MP comes after the -MF stuff.  Hmm.
+  "$@" -MT "$object" -MD -MP -MF "$tmpdepfile"
+  stat=$?
+  if test $stat -eq 0; then :
+  else
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  mv "$tmpdepfile" "$depfile"
+  ;;
+
+gcc)
+## There are various ways to get dependency output from gcc.  Here's
+## why we pick this rather obscure method:
+## - Don't want to use -MD because we'd like the dependencies to end
+##   up in a subdir.  Having to rename by hand is ugly.
+##   (We might end up doing this anyway to support other compilers.)
+## - The DEPENDENCIES_OUTPUT environment variable makes gcc act like
+##   -MM, not -M (despite what the docs say).
+## - Using -M directly means running the compiler twice (even worse
+##   than renaming).
+  if test -z "$gccflag"; then
+    gccflag=-MD,
+  fi
+  "$@" -Wp,"$gccflag$tmpdepfile"
+  stat=$?
+  if test $stat -eq 0; then :
+  else
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  alpha=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
+## The second -e expression handles DOS-style file names with drive letters.
+  sed -e 's/^[^:]*: / /' \
+      -e 's/^['$alpha']:\/[^:]*: / /' < "$tmpdepfile" >> "$depfile"
+## This next piece of magic avoids the `deleted header file' problem.
+## The problem is that when a header file which appears in a .P file
+## is deleted, the dependency causes make to die (because there is
+## typically no way to rebuild the header).  We avoid this by adding
+## dummy dependencies for each header file.  Too bad gcc doesn't do
+## this for us directly.
+  tr ' ' '
+' < "$tmpdepfile" |
+## Some versions of gcc put a space before the `:'.  On the theory
+## that the space means something, we add a space to the output as
+## well.
+## Some versions of the HPUX 10.20 sed can't process this invocation
+## correctly.  Breaking it into two sed invocations is a workaround.
+    sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+hp)
+  # This case exists only to let depend.m4 do its work.  It works by
+  # looking at the text of this script.  This case will never be run,
+  # since it is checked for above.
+  exit 1
+  ;;
+
+sgi)
+  if test "$libtool" = yes; then
+    "$@" "-Wp,-MDupdate,$tmpdepfile"
+  else
+    "$@" -MDupdate "$tmpdepfile"
+  fi
+  stat=$?
+  if test $stat -eq 0; then :
+  else
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  rm -f "$depfile"
+
+  if test -f "$tmpdepfile"; then  # yes, the sourcefile depend on other files
+    echo "$object : \\" > "$depfile"
+
+    # Clip off the initial element (the dependent).  Don't try to be
+    # clever and replace this with sed code, as IRIX sed won't handle
+    # lines with more than a fixed number of characters (4096 in
+    # IRIX 6.2 sed, 8192 in IRIX 6.5).  We also remove comment lines;
+    # the IRIX cc adds comments like `#:fec' to the end of the
+    # dependency line.
+    tr ' ' '
+' < "$tmpdepfile" \
+    | sed -e 's/^.*\.o://' -e 's/#.*$//' -e '/^$/ d' | \
+    tr '
+' ' ' >> $depfile
+    echo >> $depfile
+
+    # The second pass generates a dummy entry for each header file.
+    tr ' ' '
+' < "$tmpdepfile" \
+   | sed -e 's/^.*\.o://' -e 's/#.*$//' -e '/^$/ d' -e 's/$/:/' \
+   >> $depfile
+  else
+    # The sourcefile does not contain any dependencies, so just
+    # store a dummy comment line, to avoid errors with the Makefile
+    # "include basename.Plo" scheme.
+    echo "#dummy" > "$depfile"
+  fi
+  rm -f "$tmpdepfile"
+  ;;
+
+aix)
+  # The C for AIX Compiler uses -M and outputs the dependencies
+  # in a .u file.  This file always lives in the current directory.
+  # Also, the AIX compiler puts `$object:' at the start of each line;
+  # $object doesn't have directory information.
+  stripped=`echo "$object" | sed -e 's,^.*/,,' -e 's/\(.*\)\..*$/\1/'`
+  tmpdepfile="$stripped.u"
+  outname="$stripped.o"
+  if test "$libtool" = yes; then
+    "$@" -Wc,-M
+  else
+    "$@" -M
+  fi
+
+  stat=$?
+  if test $stat -eq 0; then :
+  else
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+
+  if test -f "$tmpdepfile"; then
+    # Each line is of the form `foo.o: dependent.h'.
+    # Do two passes, one to just change these to
+    # `$object: dependent.h' and one to simply `dependent.h:'.
+    sed -e "s,^$outname:,$object :," < "$tmpdepfile" > "$depfile"
+    sed -e "s,^$outname: \(.*\)$,\1:," < "$tmpdepfile" >> "$depfile"
+  else
+    # The sourcefile does not contain any dependencies, so just
+    # store a dummy comment line, to avoid errors with the Makefile
+    # "include basename.Plo" scheme.
+    echo "#dummy" > "$depfile"
+  fi
+  rm -f "$tmpdepfile"
+  ;;
+
+tru64)
+   # The Tru64 AIX compiler uses -MD to generate dependencies as a side
+   # effect.  `cc -MD -o foo.o ...' puts the dependencies into `foo.o.d'.
+   # At least on Alpha/Redhat 6.1, Compaq CCC V6.2-504 seems to put 
+   # dependencies in `foo.d' instead, so we check for that too.
+   # Subdirectories are respected.
+
+   tmpdepfile1="$object.d"
+   tmpdepfile2=`echo "$object" | sed -e 's/.o$/.d/'` 
+   if test "$libtool" = yes; then
+      "$@" -Wc,-MD
+   else
+      "$@" -MD
+   fi
+
+   stat=$?
+   if test $stat -eq 0; then :
+   else
+      rm -f "$tmpdepfile1" "$tmpdepfile2"
+      exit $stat
+   fi
+
+   if test -f "$tmpdepfile1"; then
+      tmpdepfile="$tmpdepfile1"
+   else
+      tmpdepfile="$tmpdepfile2"
+   fi
+   if test -f "$tmpdepfile"; then
+      sed -e "s,^.*\.[a-z]*:,$object:," < "$tmpdepfile" > "$depfile"
+      # That's a space and a tab in the [].
+      sed -e 's,^.*\.[a-z]*:[  ]*,,' -e 's,$,:,' < "$tmpdepfile" >> "$depfile"
+   else
+      echo "#dummy" > "$depfile"
+   fi
+   rm -f "$tmpdepfile"
+   ;;
+
+#nosideeffect)
+  # This comment above is used by automake to tell side-effect
+  # dependency tracking mechanisms from slower ones.
+
+dashmstdout)
+  # Important note: in order to support this mode, a compiler *must*
+  # always write the proprocessed file to stdout, regardless of -o,
+  # because we must use -o when running libtool.
+  test -z "$dashmflag" && dashmflag=-M
+  ( IFS=" "
+    case " $* " in
+    *" --mode=compile "*) # this is libtool, let us make it quiet
+      for arg
+      do # cycle over the arguments
+        case "$arg" in
+       "--mode=compile")
+         # insert --quiet before "--mode=compile"
+         set fnord "$@" --quiet
+         shift # fnord
+         ;;
+       esac
+       set fnord "$@" "$arg"
+       shift # fnord
+       shift # "$arg"
+      done
+      ;;
+    esac
+    "$@" $dashmflag | sed 's:^[^:]*\:[         ]*:'"$object"'\: :' > "$tmpdepfile"
+  ) &
+  proc=$!
+  "$@"
+  stat=$?
+  wait "$proc"
+  if test "$stat" != 0; then exit $stat; fi
+  rm -f "$depfile"
+  cat < "$tmpdepfile" > "$depfile"
+  tr ' ' '
+' < "$tmpdepfile" | \
+## Some versions of the HPUX 10.20 sed can't process this invocation
+## correctly.  Breaking it into two sed invocations is a workaround.
+    sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+dashXmstdout)
+  # This case only exists to satisfy depend.m4.  It is never actually
+  # run, as this mode is specially recognized in the preamble.
+  exit 1
+  ;;
+
+makedepend)
+  # X makedepend
+  (
+    shift
+    cleared=no
+    for arg in "$@"; do
+      case $cleared in no)
+        set ""; shift
+       cleared=yes
+      esac
+      case "$arg" in
+        -D*|-I*)
+         set fnord "$@" "$arg"; shift;;
+       -*)
+         ;;
+       *)
+         set fnord "$@" "$arg"; shift;;
+      esac
+    done
+    obj_suffix="`echo $object | sed 's/^.*\././'`"
+    touch "$tmpdepfile"
+    ${MAKEDEPEND-makedepend} 2>/dev/null -o"$obj_suffix" -f"$tmpdepfile" "$@"
+  ) &
+  proc=$!
+  "$@"
+  stat=$?
+  wait "$proc"
+  if test "$stat" != 0; then exit $stat; fi
+  rm -f "$depfile"
+  cat < "$tmpdepfile" > "$depfile"
+  tail +3 "$tmpdepfile" | tr ' ' '
+' | \
+## Some versions of the HPUX 10.20 sed can't process this invocation
+## correctly.  Breaking it into two sed invocations is a workaround.
+    sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile" "$tmpdepfile".bak
+  ;;
+
+cpp)
+  # Important note: in order to support this mode, a compiler *must*
+  # always write the proprocessed file to stdout, regardless of -o,
+  # because we must use -o when running libtool.
+  ( IFS=" "
+    case " $* " in
+    *" --mode=compile "*)
+      for arg
+      do # cycle over the arguments
+        case $arg in
+       "--mode=compile")
+         # insert --quiet before "--mode=compile"
+         set fnord "$@" --quiet
+         shift # fnord
+         ;;
+       esac
+       set fnord "$@" "$arg"
+       shift # fnord
+       shift # "$arg"
+      done
+      ;;
+    esac
+    "$@" -E |
+    sed -n '/^# [0-9][0-9]* "\([^"]*\)".*/ s:: \1 \\:p' |
+    sed '$ s: \\$::' > "$tmpdepfile"
+  ) &
+  proc=$!
+  "$@"
+  stat=$?
+  wait "$proc"
+  if test "$stat" != 0; then exit $stat; fi
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  cat < "$tmpdepfile" >> "$depfile"
+  sed < "$tmpdepfile" '/^$/d;s/^ //;s/ \\$//;s/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+msvisualcpp)
+  # Important note: in order to support this mode, a compiler *must*
+  # always write the proprocessed file to stdout, regardless of -o,
+  # because we must use -o when running libtool.
+  ( IFS=" "
+    case " $* " in
+    *" --mode=compile "*)
+      for arg
+      do # cycle over the arguments
+        case $arg in
+       "--mode=compile")
+         # insert --quiet before "--mode=compile"
+         set fnord "$@" --quiet
+         shift # fnord
+         ;;
+       esac
+       set fnord "$@" "$arg"
+       shift # fnord
+       shift # "$arg"
+      done
+      ;;
+    esac
+    "$@" -E |
+    sed -n '/^#line [0-9][0-9]* "\([^"]*\)"/ s::echo "`cygpath -u \\"\1\\"`":p' | sort | uniq > "$tmpdepfile"
+  ) &
+  proc=$!
+  "$@"
+  stat=$?
+  wait "$proc"
+  if test "$stat" != 0; then exit $stat; fi
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  . "$tmpdepfile" | sed 's% %\\ %g' | sed -n '/^\(.*\)$/ s::   \1 \\:p' >> "$depfile"
+  echo "       " >> "$depfile"
+  . "$tmpdepfile" | sed 's% %\\ %g' | sed -n '/^\(.*\)$/ s::\1\::p' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+none)
+  exec "$@"
+  ;;
+
+*)
+  echo "Unknown depmode $depmode" 1>&2
+  exit 1
+  ;;
+esac
+
+exit 0
diff --git a/docs/access-levels.txt b/docs/access-levels.txt
new file mode 100644 (file)
index 0000000..97a4bb2
--- /dev/null
@@ -0,0 +1,31 @@
+There are different types of access within srvx.  This document
+attempts to list the privileges and restrictions for each (in the
+default configuration -- OpServ and ChanServ may be changed at
+runtime, and the defaults can be changed in the source).
+
+The basic classes of users are these:
+  Unauthenticated user
+  Unauthenticated oper
+  Authenticated user
+  Authenticated support helper
+  Authenticated network helper
+  Authenticated oper
+
+"Staff" is used to refer to the set of opers (either authenticated or
+not) plus authenticated support or networkhelpers.  A staff member
+also has an "OpServ access level."  If they are authenticated, it is
+the access level of their handle.  If they are not authenticated, it
+is zero.
+
+For ChanServ operations on a channel, there is also a "channel access
+level."  For peons, this is 100; for ops, 200; for masters, 300; for
+co-owners, 400; for owners, 500; and for staff members with security
+override on, 600 (if they have a non-override account with sufficient
+access, that is used instead).
+
+Authenticated opers and network helpers may use the !god mode in
+ChanServ to toggle security override on and off.  Support helpers have
+security override on if (and only if) they are in a designated
+"support channel."  On GamesNET, this is #support.
+
+(More documentation goes here.)
diff --git a/docs/coding-style.txt b/docs/coding-style.txt
new file mode 100644 (file)
index 0000000..6f3c5ff
--- /dev/null
@@ -0,0 +1,47 @@
+This file contains various development notes useful when coding srvx.
+
+HEADER FILES
+------------
+If a file or feature is not defined in ANSI C89, an autoconf feature
+test should be used to verify that it is supported.  ANSI C89
+specifies that the following header files exist:
+  <assert.h> <ctype.h> <errno.h> <float.h> <limits.h> <locale.h>
+  <math.h> <setjmp.h> <signal.h> <stdarg.h> <stddef.h> <stdio.h>
+  <stdlib.h> <string.h> <time.h>
+You may unconditionally include the above headers, or any that are
+shipped with srvx.  ALL other headers must be conditional.
+
+DEBUG MODE
+----------
+Many libraries are in debug mode by default, and require the C
+preprocessor symbol NDEBUG to be set to disable the debug features
+(this is inspired by the behavior of assert() from the C standard).
+Code in srvx should follow this convention.
+
+ASSERTIONS
+----------
+If you know that a certain condition is illegal, you should use the
+assert() macro to verify that it is false.  Please do NOT silently
+fail by returning in these conditions.
+
+HELP FILES
+----------
+To maintain consistency, this is the format being used for the syntax
+examples in the help files:
+  - Text constants have no brackets around them
+  - Required arguments use <description-of-argument>
+  - Optional arguments use [description-of-argument]
+        Example: /msg $S COMMAND <arg1> [arg2]
+                Where COMMAND is a constant, arg1 is required, arg2 is optional
+  - If arguments have different valid values, separate them with a |
+        Example: /msg $S COMMAND <a|b|c> [foo|bar|baz]
+  - Arguments that are optional but have an extra argument (optional or not)
+    when they are used have this format: [<arg1> <arg2>] or [<arg1> [arg2]]
+  - Repeating arguments have this format: <arg1> <arg2> [<arg1> <arg2>]...
+    where both arg1 and arg2 are required for each repetition. If only one
+    argument is used in each repetition, no <> goes around the argument name
+    ([arg1]...)
+  - Commands like ChanServ's SET have the format /msg $S SET [<option> [value]]
+  - If there is a special case not covered above, try to find another command
+    that has a similar syntax, and adapt it. If that is not possible, use
+    something that makes sense.
diff --git a/docs/cookies.txt b/docs/cookies.txt
new file mode 100644 (file)
index 0000000..aa33806
--- /dev/null
@@ -0,0 +1,61 @@
+EMAIL COOKIE AUTHENTICATION
+---------------------------
+
+NickServ can use email authentication for various things, offloading
+some human support.  If email cookies are enabled, each handle can
+have an associated email address (if they're not enabled, current
+email addresses are preserved, but not displayed or used.)  If email
+cookies are disabled, the rest of this section does not apply.
+
+Cookies (10-character case-sensitive alphanumeric strings; they are
+base64-encoded random numbers) are used for the following things:
+
+- Handle activation.  When a new handle is registered, its password is
+  set to an unusable string.  A cookie is sent to that email address,
+  and can later be used to auth and change the password (as for
+  forgotten password changes, below).
+
+- Changing email addresses.  When an authed user requests that their
+  email address be changed, half of the cookie is sent to each; both
+  halves must be presented to complete the change.
+
+- Allowauth (in addition to the normal staff allowauth command).  A
+  cookie is sent to the handle's address, and if the user responds
+  with that cookie, they are allowauth'ed.
+
+- Changing forgotten passwords.  A user may request a cookie be sent
+  to their email address; this will allow them to auth and change
+  their password.
+
+The following limitations apply:
+
+- Only one cookie will be issued per handle at a time.  The current
+  cookie must be used or time out before another one is issued.
+
+- Cookies time out after a configurable amount of time (defaults to 24
+  hours).
+
+- Only one un-activated handle is allowed per email address.
+
+The following commands are provided (overriding non-cookie commands of
+the same name, if there is overlap):
+
+- REGISTER <handle> <password> [<email_addr>]
+  - Registers the handle.  If email address provided, emails user with
+    a cookie that allows them to activate their handle.  Otherwise,
+    sets password to what they request.
+- SET EMAIL <new_email>
+  - Mails cookie to new email address (if one already exists, mails
+    half to new, half to old).
+- AUTHCOOKIE <handle>
+  - Emails cookie for authentication.
+- RESETPASS <handle> <newpass>
+  - Begins password reset process for a handle.
+- COOKIE [<handle>] <cookie>
+  - If handle's cookie type is REGSTER, activates a handle that was
+    registered using REGISTER, setting handle's password to what is
+    specified.
+  - If handle's cookie type is EMAIL_CHANGE, changes email address.
+  - If handle's cookie type is PASSWORD_CHANGE, changes password.
+  - If handle's cookie type is ALLOWAUTH, allows user to auth if
+    password matches.
diff --git a/docs/helpserv.txt b/docs/helpserv.txt
new file mode 100644 (file)
index 0000000..a38a71f
--- /dev/null
@@ -0,0 +1,115 @@
+How HelpServ Works
+------------------
+
+HelpServ is a bot designed to help with queues of questions (or
+support requests).  It's designed to work well in multiple channels
+at once.
+
+There is a one-to-one mapping between help bots and channels they act
+in; this is to allow privmsgs to work without having users specify a
+channel (which is much easier to use).  For example, HelpServ might be
+in #support, and Interview could be in #counterstrike; both would be
+bots of this kind, but they would have totally separate user lists and
+data sets.
+
+Each channel/help bot has a set of "helper" users, defined by the
+channel admin(s).  In the rest of this document, I will use "helper"
+to indicate someone on the helper list for the channel, and "user" to
+indicate all other users.  There are also "manager" users and one
+"owner" for the bot.
+
+There are two primary modes: "command mode" and "help mode."  Helpers
+default to command mode, and other users default to help mode.
+Command mode uses a command parser similar to the standard services.
+Help mode queues each user's messages into a virtual folder for later
+perusal by a helper.  A helper who is actively using "help mode" may
+issue "command mode" lines by prefixing them with a "command word" for
+the channel.
+
+In Help Mode
+------------
+
+When a normal user joins the channel, a customizable greeting is sent.
+This usually tells the user to privmsg the bot for help (or to queue a
+question).  It is recommended that this include a URL to a FAQ.  The
+bot then shuts up until the user msgs them again.
+
+When a user msgs the bot for the first time after they join, it opens
+a virtual folder for them.  The time of this request is filed, and
+they are added to a queue.  It also sends another customizable
+message.  The virtual folder is kept active until a helper closes it
+or the user is "lost"; when the virtual folder is closed, it is
+written out to a file (with statistics on when it was opened, closed,
+if (and when) it was picked up by a helper, etc).
+
+A user is "lost" when:
+ - For QUIT-based requests (or queues), the user disconnects from IRC.
+ - For PART-based requests, the user parts the channel.
+ - For handle-based requests, the handle is unregistered.
+The "default" mode for queues is close-based -- which behaves as
+handle-based if the user is authed and quit-based otherwise.
+
+Helpers may also annotate an existing request, using the NOTE command.
+
+In Command Mode
+---------------
+
+The following commands are defined:
+
+LIST - show the currently open requests (unassigned first, then
+  assigned)
+
+NEXT - acts as PICKUP for the next unassigned request
+
+PICKUP <reqno> - assign request number to self
+
+REASSIGN <reqno> <helpernick|*handle> - reassign request to another
+  (online) helper
+
+CLOSE <reqno> <comment> - close request, with comment
+CLOSELAST <comment>     - close last PICKUP'ed or HANDOFF'ed request
+
+NOTE [<reqno>] <comment> - add a note to a request (if not a number, use last assigned)
+
+TOGMODE - switch between command mode and help mode
+
+IGNORE <handle>         - Ignore the specified handle (or hostmask)
+IGNORE <nick!user@host> - in the future.  Does not close current requests.
+
+ADDMANAGER <nick|*handle> - add or remove a manager or helper
+DELMANAGER <nick|*handle>   (managers may add and remove users
+ADDHELPER <nick|*handle>    and see statistics for anyone)
+DELHELPER <nick|*handle>
+
+PAGE [<explanation>] - send a page, if enabled
+
+SET [<option> <value>] - set an option:
+  - GREETING - message sent when user joins channel
+  - OPENREQ - message sent when user opens a request
+  - CLOSEREQ - message sent to requester when a request is closed
+  - CMDWORD - "command mode" escape word (for helpers in help mode)
+  - PAGEDEST - target for pages
+  - PAGEMSG - default page text (if no explanation when PAGE invoked)
+  - IDLETIME - how long (with users, not helpers in the channel) before
+               bot complains to PAGEDEST
+  - WHINETIME - how long before an unhandled request will make the bot
+                complain to PAGEDEST
+  - WHINEINTERVAL - how frequently the bot will complain about unhandled
+                    requests, or users in channel without heleprs there
+  - REASSIGN - privileges to use REASSIGN (helper, manager, owner)
+
+STATS [<nick|*handle>] - show statistics for nick/handle (must be self,
+                         or self must be a manager; defaults to self)
+
+STATSREPORT - show overall statistics (such as?)
+
+Statistics
+----------
+
+For any helper (includes managers), the following statistics are kept:
+
+- Time in channel (total, this week, this month)
+- Requests "picked up"
+- Requests closed
+- Requests handed off
+- Handoffs received
diff --git a/docs/ircd-modes.txt b/docs/ircd-modes.txt
new file mode 100644 (file)
index 0000000..4bb70ec
--- /dev/null
@@ -0,0 +1,35 @@
+These mode lists were gathered from the connect logs at
+netsplit.de/networks/ a while back.  They may be a little out of date.
+
+NETWORK/IRCD                    USER MODES              CHANNEL MODES
+------------                    ----------              -------------
+DALnet (Bahamut)                dioswkg Aabcefhnry      biklmnopstv LMrRc
+IRCnet (2.10.3)                  io w   aOr             biklmnopstv aeIOqr
+Undernet (ircu)                 dioswkg                 biklmnopstv
+EFnet (hybrid6.2)               dioswk  Obcfnrxyz       biklmnopstv e
+QuakeNet (ircu+lain)            dioswkg                 biklmnopstv
+IRC-Hispano (ircu2.10.H)        dioswkg hrxX            biklmnopstv Rr
+PTnet (Bahamut?)                 iowskg AabcfhOrR       biklmnopstv cdR
+EnterTheGame (Bahamut)          diowskg Aabcefhnry      biklmnopstv cRr
+WebChat (ConferenceRoom)         iowskg AabcEeFGghjLMmnpRrSTtXxy  biklmnopstv AacdejLMNOruz
+GamesNET (ircu)                 dioswkg                 biklmnopstv
+BRASnet (bras)                  dioswk  AaBbcefhjNOprRTvyz        biklmnopstv AacdeKOqRrSe
+GalaxyNet (glx)                 dioswkg                 biklmnopstv
+Voila (ircu)                    dioswkg                 biklmnopstv
+IrCQ-Net (hybrid 6b)            diosw   bcfrknxyz       biklmnopstv ed
+OpenProjectsNet (dancer-ircd)   abBcCdDeEfFgGhHiIkKlLmMnNopPrRsSUvVwWxXyYzZ0123459*@ bcdefFhiIklmnoPqstv
+UniBG (hybrid6.0+UniBG)         diows   bcfknrxyz       biklmnopstv ed
+HanIRC.org (ircu+hangul)        dioswkg                 biklmnopstv
+NewNet (nn+wiz)                 dioswkg cflu            biklmnopstv
+BrasIRC-1 (ircbr)               diowskg Aabcefhnry      biklmnopstv cRr
+ChatNet (CN-BHT+MSGCHAT)        diowskg Aabcefhnry      biklmnopstv cRr
+Planetarion (ircu.paIRCd)       dioswkg                 biklmnopstv
+GRNet (Bahamut+)                dioswkg Aacefhnrty      biklmnopstv cLMRr
+ArabChat (Bahamut+)             dioswkg AabcefhknRry    biklmnopstv cLMRr
+AustNet (austhex.servd)         dioswkg achlnTtv        biklmnopstv er
+LinkNet (? 3.8.7)               ?                       S?
+criten.net (Bahamut)            dioswkg Aabcefhnry      biklmnopstv cLMRr
+azzurra.net (Bahamut+)          diosw g Aabcefhnrxyz    biklmnopstv cRr
+oz.org (ozircd)                 dioswk  f               biklmnopstv
+ircgate.net (Unreal)            dioswk  AaBCceFfGgHhIjNOqrSTtvWxz  biklmnopstv AabCcefGHhiKLNOQqRrSuVz
+nitro.net (Bahamut+)            diosw g Aabcefhknry     biklmnopstv cRr
diff --git a/docs/malloc-compare.txt b/docs/malloc-compare.txt
new file mode 100644 (file)
index 0000000..9556385
--- /dev/null
@@ -0,0 +1,8 @@
+                SYSTEM          DMALLOC         MPATROL         BOEHM-GC
+After linking
+TIME            0h00m21s        0h52m06s        fail            0h00m43s
+SIZE            96996kB         273340kB        fail            167432kB
+
+Notes: boehm-gc complains a lot about memory leaks during startup,
+even though the dmalloc does not report it as leaked.  Not sure why
+this is so; presumably a GC root has not been registered correctly.
diff --git a/install-sh b/install-sh
new file mode 100755 (executable)
index 0000000..2c212cc
--- /dev/null
@@ -0,0 +1,250 @@
+#! /bin/sh
+#
+# install - install a program, script, or datafile
+# This comes from X11R5 (mit/util/scripts/install.sh).
+#
+# Copyright 1991 by the Massachusetts Institute of Technology
+#
+# Permission to use, copy, modify, distribute, and sell this software and its
+# documentation for any purpose is hereby granted without fee, provided that
+# the above copyright notice appear in all copies and that both that
+# copyright notice and this permission notice appear in supporting
+# documentation, and that the name of M.I.T. not be used in advertising or
+# publicity pertaining to distribution of the software without specific,
+# written prior permission.  M.I.T. makes no representations about the
+# suitability of this software for any purpose.  It is provided "as is"
+# without express or implied warranty.
+#
+# Calling this script install-sh is preferred over install.sh, to prevent
+# `make' implicit rules from creating a file called install from it
+# when there is no Makefile.
+#
+# This script is compatible with the BSD install script, but was written
+# from scratch.
+#
+
+
+# set DOITPROG to echo to test this script
+
+# Don't use :- since 4.3BSD and earlier shells don't like it.
+doit="${DOITPROG-}"
+
+
+# put in absolute paths if you don't have them in your path; or use env. vars.
+
+mvprog="${MVPROG-mv}"
+cpprog="${CPPROG-cp}"
+chmodprog="${CHMODPROG-chmod}"
+chownprog="${CHOWNPROG-chown}"
+chgrpprog="${CHGRPPROG-chgrp}"
+stripprog="${STRIPPROG-strip}"
+rmprog="${RMPROG-rm}"
+mkdirprog="${MKDIRPROG-mkdir}"
+
+transformbasename=""
+transform_arg=""
+instcmd="$mvprog"
+chmodcmd="$chmodprog 0755"
+chowncmd=""
+chgrpcmd=""
+stripcmd=""
+rmcmd="$rmprog -f"
+mvcmd="$mvprog"
+src=""
+dst=""
+dir_arg=""
+
+while [ x"$1" != x ]; do
+    case $1 in
+       -c) instcmd="$cpprog"
+           shift
+           continue;;
+
+       -d) dir_arg=true
+           shift
+           continue;;
+
+       -m) chmodcmd="$chmodprog $2"
+           shift
+           shift
+           continue;;
+
+       -o) chowncmd="$chownprog $2"
+           shift
+           shift
+           continue;;
+
+       -g) chgrpcmd="$chgrpprog $2"
+           shift
+           shift
+           continue;;
+
+       -s) stripcmd="$stripprog"
+           shift
+           continue;;
+
+       -t=*) transformarg=`echo $1 | sed 's/-t=//'`
+           shift
+           continue;;
+
+       -b=*) transformbasename=`echo $1 | sed 's/-b=//'`
+           shift
+           continue;;
+
+       *)  if [ x"$src" = x ]
+           then
+               src=$1
+           else
+               # this colon is to work around a 386BSD /bin/sh bug
+               :
+               dst=$1
+           fi
+           shift
+           continue;;
+    esac
+done
+
+if [ x"$src" = x ]
+then
+       echo "install:  no input file specified"
+       exit 1
+else
+       true
+fi
+
+if [ x"$dir_arg" != x ]; then
+       dst=$src
+       src=""
+       
+       if [ -d $dst ]; then
+               instcmd=:
+       else
+               instcmd=mkdir
+       fi
+else
+
+# Waiting for this to be detected by the "$instcmd $src $dsttmp" command
+# might cause directories to be created, which would be especially bad 
+# if $src (and thus $dsttmp) contains '*'.
+
+       if [ -f $src -o -d $src ]
+       then
+               true
+       else
+               echo "install:  $src does not exist"
+               exit 1
+       fi
+       
+       if [ x"$dst" = x ]
+       then
+               echo "install:  no destination specified"
+               exit 1
+       else
+               true
+       fi
+
+# If destination is a directory, append the input filename; if your system
+# does not like double slashes in filenames, you may need to add some logic
+
+       if [ -d $dst ]
+       then
+               dst="$dst"/`basename $src`
+       else
+               true
+       fi
+fi
+
+## this sed command emulates the dirname command
+dstdir=`echo $dst | sed -e 's,[^/]*$,,;s,/$,,;s,^$,.,'`
+
+# Make sure that the destination directory exists.
+#  this part is taken from Noah Friedman's mkinstalldirs script
+
+# Skip lots of stat calls in the usual case.
+if [ ! -d "$dstdir" ]; then
+defaultIFS='   
+'
+IFS="${IFS-${defaultIFS}}"
+
+oIFS="${IFS}"
+# Some sh's can't handle IFS=/ for some reason.
+IFS='%'
+set - `echo ${dstdir} | sed -e 's@/@%@g' -e 's@^%@/@'`
+IFS="${oIFS}"
+
+pathcomp=''
+
+while [ $# -ne 0 ] ; do
+       pathcomp="${pathcomp}${1}"
+       shift
+
+       if [ ! -d "${pathcomp}" ] ;
+        then
+               $mkdirprog "${pathcomp}"
+       else
+               true
+       fi
+
+       pathcomp="${pathcomp}/"
+done
+fi
+
+if [ x"$dir_arg" != x ]
+then
+       $doit $instcmd $dst &&
+
+       if [ x"$chowncmd" != x ]; then $doit $chowncmd $dst; else true ; fi &&
+       if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dst; else true ; fi &&
+       if [ x"$stripcmd" != x ]; then $doit $stripcmd $dst; else true ; fi &&
+       if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dst; else true ; fi
+else
+
+# If we're going to rename the final executable, determine the name now.
+
+       if [ x"$transformarg" = x ] 
+       then
+               dstfile=`basename $dst`
+       else
+               dstfile=`basename $dst $transformbasename | 
+                       sed $transformarg`$transformbasename
+       fi
+
+# don't allow the sed command to completely eliminate the filename
+
+       if [ x"$dstfile" = x ] 
+       then
+               dstfile=`basename $dst`
+       else
+               true
+       fi
+
+# Make a temp file name in the proper directory.
+
+       dsttmp=$dstdir/#inst.$$#
+
+# Move or copy the file name to the temp name
+
+       $doit $instcmd $src $dsttmp &&
+
+       trap "rm -f ${dsttmp}" 0 &&
+
+# and set any options; do chmod last to preserve setuid bits
+
+# If any of these fail, we abort the whole thing.  If we want to
+# ignore errors from any of these, just make sure not to ignore
+# errors from the above "$doit $instcmd $src $dsttmp" command.
+
+       if [ x"$chowncmd" != x ]; then $doit $chowncmd $dsttmp; else true;fi &&
+       if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dsttmp; else true;fi &&
+       if [ x"$stripcmd" != x ]; then $doit $stripcmd $dsttmp; else true;fi &&
+       if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dsttmp; else true;fi &&
+
+# Now rename the file to the real destination.
+
+       $doit $rmcmd -f $dstdir/$dstfile &&
+       $doit $mvcmd $dsttmp $dstdir/$dstfile 
+
+fi &&
+
+
+exit 0
diff --git a/ltmain.sh b/ltmain.sh
new file mode 100644 (file)
index 0000000..6e5bf36
--- /dev/null
+++ b/ltmain.sh
@@ -0,0 +1,4984 @@
+# ltmain.sh - Provide generalized library-building support services.
+# NOTE: Changing this file will not affect anything until you rerun configure.
+#
+# Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001
+# Free Software Foundation, Inc.
+# Originally by Gordon Matzigkeit <gord@gnu.ai.mit.edu>, 1996
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# Check that we have a working $echo.
+if test "X$1" = X--no-reexec; then
+  # Discard the --no-reexec flag, and continue.
+  shift
+elif test "X$1" = X--fallback-echo; then
+  # Avoid inline document here, it may be left over
+  :
+elif test "X`($echo '\t') 2>/dev/null`" = 'X\t'; then
+  # Yippee, $echo works!
+  :
+else
+  # Restart under the correct shell, and then maybe $echo will work.
+  exec $SHELL "$0" --no-reexec ${1+"$@"}
+fi
+
+if test "X$1" = X--fallback-echo; then
+  # used as fallback echo
+  shift
+  cat <<EOF
+$*
+EOF
+  exit 0
+fi
+
+# The name of this program.
+progname=`$echo "$0" | sed 's%^.*/%%'`
+modename="$progname"
+
+# Constants.
+PROGRAM=ltmain.sh
+PACKAGE=libtool
+VERSION=1.4.2
+TIMESTAMP=" (1.922.2.53 2001/09/11 03:18:52)"
+
+default_mode=
+help="Try \`$progname --help' for more information."
+magic="%%%MAGIC variable%%%"
+mkdir="mkdir"
+mv="mv -f"
+rm="rm -f"
+
+# Sed substitution that helps us do robust quoting.  It backslashifies
+# metacharacters that are still active within double-quoted strings.
+Xsed='sed -e 1s/^X//'
+sed_quote_subst='s/\([\\`\\"$\\\\]\)/\\\1/g'
+SP2NL='tr \040 \012'
+NL2SP='tr \015\012 \040\040'
+
+# NLS nuisances.
+# Only set LANG and LC_ALL to C if already set.
+# These must not be set unconditionally because not all systems understand
+# e.g. LANG=C (notably SCO).
+# We save the old values to restore during execute mode.
+if test "${LC_ALL+set}" = set; then
+  save_LC_ALL="$LC_ALL"; LC_ALL=C; export LC_ALL
+fi
+if test "${LANG+set}" = set; then
+  save_LANG="$LANG"; LANG=C; export LANG
+fi
+
+# Make sure IFS has a sensible default
+: ${IFS="      "}
+
+if test "$build_libtool_libs" != yes && test "$build_old_libs" != yes; then
+  echo "$modename: not configured to build any kind of library" 1>&2
+  echo "Fatal configuration error.  See the $PACKAGE docs for more information." 1>&2
+  exit 1
+fi
+
+# Global variables.
+mode=$default_mode
+nonopt=
+prev=
+prevopt=
+run=
+show="$echo"
+show_help=
+execute_dlfiles=
+lo2o="s/\\.lo\$/.${objext}/"
+o2lo="s/\\.${objext}\$/.lo/"
+
+# Parse our command line options once, thoroughly.
+while test $# -gt 0
+do
+  arg="$1"
+  shift
+
+  case $arg in
+  -*=*) optarg=`$echo "X$arg" | $Xsed -e 's/[-_a-zA-Z0-9]*=//'` ;;
+  *) optarg= ;;
+  esac
+
+  # If the previous option needs an argument, assign it.
+  if test -n "$prev"; then
+    case $prev in
+    execute_dlfiles)
+      execute_dlfiles="$execute_dlfiles $arg"
+      ;;
+    *)
+      eval "$prev=\$arg"
+      ;;
+    esac
+
+    prev=
+    prevopt=
+    continue
+  fi
+
+  # Have we seen a non-optional argument yet?
+  case $arg in
+  --help)
+    show_help=yes
+    ;;
+
+  --version)
+    echo "$PROGRAM (GNU $PACKAGE) $VERSION$TIMESTAMP"
+    exit 0
+    ;;
+
+  --config)
+    sed -e '1,/^# ### BEGIN LIBTOOL CONFIG/d' -e '/^# ### END LIBTOOL CONFIG/,$d' $0
+    exit 0
+    ;;
+
+  --debug)
+    echo "$progname: enabling shell trace mode"
+    set -x
+    ;;
+
+  --dry-run | -n)
+    run=:
+    ;;
+
+  --features)
+    echo "host: $host"
+    if test "$build_libtool_libs" = yes; then
+      echo "enable shared libraries"
+    else
+      echo "disable shared libraries"
+    fi
+    if test "$build_old_libs" = yes; then
+      echo "enable static libraries"
+    else
+      echo "disable static libraries"
+    fi
+    exit 0
+    ;;
+
+  --finish) mode="finish" ;;
+
+  --mode) prevopt="--mode" prev=mode ;;
+  --mode=*) mode="$optarg" ;;
+
+  --quiet | --silent)
+    show=:
+    ;;
+
+  -dlopen)
+    prevopt="-dlopen"
+    prev=execute_dlfiles
+    ;;
+
+  -*)
+    $echo "$modename: unrecognized option \`$arg'" 1>&2
+    $echo "$help" 1>&2
+    exit 1
+    ;;
+
+  *)
+    nonopt="$arg"
+    break
+    ;;
+  esac
+done
+
+if test -n "$prevopt"; then
+  $echo "$modename: option \`$prevopt' requires an argument" 1>&2
+  $echo "$help" 1>&2
+  exit 1
+fi
+
+# If this variable is set in any of the actions, the command in it
+# will be execed at the end.  This prevents here-documents from being
+# left over by shells.
+exec_cmd=
+
+if test -z "$show_help"; then
+
+  # Infer the operation mode.
+  if test -z "$mode"; then
+    case $nonopt in
+    *cc | *++ | gcc* | *-gcc*)
+      mode=link
+      for arg
+      do
+       case $arg in
+       -c)
+          mode=compile
+          break
+          ;;
+       esac
+      done
+      ;;
+    *db | *dbx | *strace | *truss)
+      mode=execute
+      ;;
+    *install*|cp|mv)
+      mode=install
+      ;;
+    *rm)
+      mode=uninstall
+      ;;
+    *)
+      # If we have no mode, but dlfiles were specified, then do execute mode.
+      test -n "$execute_dlfiles" && mode=execute
+
+      # Just use the default operation mode.
+      if test -z "$mode"; then
+       if test -n "$nonopt"; then
+         $echo "$modename: warning: cannot infer operation mode from \`$nonopt'" 1>&2
+       else
+         $echo "$modename: warning: cannot infer operation mode without MODE-ARGS" 1>&2
+       fi
+      fi
+      ;;
+    esac
+  fi
+
+  # Only execute mode is allowed to have -dlopen flags.
+  if test -n "$execute_dlfiles" && test "$mode" != execute; then
+    $echo "$modename: unrecognized option \`-dlopen'" 1>&2
+    $echo "$help" 1>&2
+    exit 1
+  fi
+
+  # Change the help message to a mode-specific one.
+  generic_help="$help"
+  help="Try \`$modename --help --mode=$mode' for more information."
+
+  # These modes are in order of execution frequency so that they run quickly.
+  case $mode in
+  # libtool compile mode
+  compile)
+    modename="$modename: compile"
+    # Get the compilation command and the source file.
+    base_compile=
+    prev=
+    lastarg=
+    srcfile="$nonopt"
+    suppress_output=
+
+    user_target=no
+    for arg
+    do
+      case $prev in
+      "") ;;
+      xcompiler)
+       # Aesthetically quote the previous argument.
+       prev=
+       lastarg=`$echo "X$arg" | $Xsed -e "$sed_quote_subst"`
+
+       case $arg in
+       # Double-quote args containing other shell metacharacters.
+       # Many Bourne shells cannot handle close brackets correctly
+       # in scan sets, so we specify it separately.
+       *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \   ]*|*]*|"")
+         arg="\"$arg\""
+         ;;
+       esac
+
+       # Add the previous argument to base_compile.
+       if test -z "$base_compile"; then
+         base_compile="$lastarg"
+       else
+         base_compile="$base_compile $lastarg"
+       fi
+       continue
+       ;;
+      esac
+
+      # Accept any command-line options.
+      case $arg in
+      -o)
+       if test "$user_target" != "no"; then
+         $echo "$modename: you cannot specify \`-o' more than once" 1>&2
+         exit 1
+       fi
+       user_target=next
+       ;;
+
+      -static)
+       build_old_libs=yes
+       continue
+       ;;
+
+      -prefer-pic)
+       pic_mode=yes
+       continue
+       ;;
+
+      -prefer-non-pic)
+       pic_mode=no
+       continue
+       ;;
+
+      -Xcompiler)
+       prev=xcompiler
+       continue
+       ;;
+
+      -Wc,*)
+       args=`$echo "X$arg" | $Xsed -e "s/^-Wc,//"`
+       lastarg=
+       save_ifs="$IFS"; IFS=','
+       for arg in $args; do
+         IFS="$save_ifs"
+
+         # Double-quote args containing other shell metacharacters.
+         # Many Bourne shells cannot handle close brackets correctly
+         # in scan sets, so we specify it separately.
+         case $arg in
+           *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \       ]*|*]*|"")
+           arg="\"$arg\""
+           ;;
+         esac
+         lastarg="$lastarg $arg"
+       done
+       IFS="$save_ifs"
+       lastarg=`$echo "X$lastarg" | $Xsed -e "s/^ //"`
+
+       # Add the arguments to base_compile.
+       if test -z "$base_compile"; then
+         base_compile="$lastarg"
+       else
+         base_compile="$base_compile $lastarg"
+       fi
+       continue
+       ;;
+      esac
+
+      case $user_target in
+      next)
+       # The next one is the -o target name
+       user_target=yes
+       continue
+       ;;
+      yes)
+       # We got the output file
+       user_target=set
+       libobj="$arg"
+       continue
+       ;;
+      esac
+
+      # Accept the current argument as the source file.
+      lastarg="$srcfile"
+      srcfile="$arg"
+
+      # Aesthetically quote the previous argument.
+
+      # Backslashify any backslashes, double quotes, and dollar signs.
+      # These are the only characters that are still specially
+      # interpreted inside of double-quoted scrings.
+      lastarg=`$echo "X$lastarg" | $Xsed -e "$sed_quote_subst"`
+
+      # Double-quote args containing other shell metacharacters.
+      # Many Bourne shells cannot handle close brackets correctly
+      # in scan sets, so we specify it separately.
+      case $lastarg in
+      *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \    ]*|*]*|"")
+       lastarg="\"$lastarg\""
+       ;;
+      esac
+
+      # Add the previous argument to base_compile.
+      if test -z "$base_compile"; then
+       base_compile="$lastarg"
+      else
+       base_compile="$base_compile $lastarg"
+      fi
+    done
+
+    case $user_target in
+    set)
+      ;;
+    no)
+      # Get the name of the library object.
+      libobj=`$echo "X$srcfile" | $Xsed -e 's%^.*/%%'`
+      ;;
+    *)
+      $echo "$modename: you must specify a target with \`-o'" 1>&2
+      exit 1
+      ;;
+    esac
+
+    # Recognize several different file suffixes.
+    # If the user specifies -o file.o, it is replaced with file.lo
+    xform='[cCFSfmso]'
+    case $libobj in
+    *.ada) xform=ada ;;
+    *.adb) xform=adb ;;
+    *.ads) xform=ads ;;
+    *.asm) xform=asm ;;
+    *.c++) xform=c++ ;;
+    *.cc) xform=cc ;;
+    *.cpp) xform=cpp ;;
+    *.cxx) xform=cxx ;;
+    *.f90) xform=f90 ;;
+    *.for) xform=for ;;
+    esac
+
+    libobj=`$echo "X$libobj" | $Xsed -e "s/\.$xform$/.lo/"`
+
+    case $libobj in
+    *.lo) obj=`$echo "X$libobj" | $Xsed -e "$lo2o"` ;;
+    *)
+      $echo "$modename: cannot determine name of library object from \`$libobj'" 1>&2
+      exit 1
+      ;;
+    esac
+
+    if test -z "$base_compile"; then
+      $echo "$modename: you must specify a compilation command" 1>&2
+      $echo "$help" 1>&2
+      exit 1
+    fi
+
+    # Delete any leftover library objects.
+    if test "$build_old_libs" = yes; then
+      removelist="$obj $libobj"
+    else
+      removelist="$libobj"
+    fi
+
+    $run $rm $removelist
+    trap "$run $rm $removelist; exit 1" 1 2 15
+
+    # On Cygwin there's no "real" PIC flag so we must build both object types
+    case $host_os in
+    cygwin* | mingw* | pw32* | os2*)
+      pic_mode=default
+      ;;
+    esac
+    if test $pic_mode = no && test "$deplibs_check_method" != pass_all; then
+      # non-PIC code in shared libraries is not supported
+      pic_mode=default
+    fi
+
+    # Calculate the filename of the output object if compiler does
+    # not support -o with -c
+    if test "$compiler_c_o" = no; then
+      output_obj=`$echo "X$srcfile" | $Xsed -e 's%^.*/%%' -e 's%\.[^.]*$%%'`.${objext}
+      lockfile="$output_obj.lock"
+      removelist="$removelist $output_obj $lockfile"
+      trap "$run $rm $removelist; exit 1" 1 2 15
+    else
+      need_locks=no
+      lockfile=
+    fi
+
+    # Lock this critical section if it is needed
+    # We use this script file to make the link, it avoids creating a new file
+    if test "$need_locks" = yes; then
+      until $run ln "$0" "$lockfile" 2>/dev/null; do
+       $show "Waiting for $lockfile to be removed"
+       sleep 2
+      done
+    elif test "$need_locks" = warn; then
+      if test -f "$lockfile"; then
+       echo "\
+*** ERROR, $lockfile exists and contains:
+`cat $lockfile 2>/dev/null`
+
+This indicates that another process is trying to use the same
+temporary object file, and libtool could not work around it because
+your compiler does not support \`-c' and \`-o' together.  If you
+repeat this compilation, it may succeed, by chance, but you had better
+avoid parallel builds (make -j) in this platform, or get a better
+compiler."
+
+       $run $rm $removelist
+       exit 1
+      fi
+      echo $srcfile > "$lockfile"
+    fi
+
+    if test -n "$fix_srcfile_path"; then
+      eval srcfile=\"$fix_srcfile_path\"
+    fi
+
+    # Only build a PIC object if we are building libtool libraries.
+    if test "$build_libtool_libs" = yes; then
+      # Without this assignment, base_compile gets emptied.
+      fbsd_hideous_sh_bug=$base_compile
+
+      if test "$pic_mode" != no; then
+       # All platforms use -DPIC, to notify preprocessed assembler code.
+       command="$base_compile $srcfile $pic_flag -DPIC"
+      else
+       # Don't build PIC code
+       command="$base_compile $srcfile"
+      fi
+      if test "$build_old_libs" = yes; then
+       lo_libobj="$libobj"
+       dir=`$echo "X$libobj" | $Xsed -e 's%/[^/]*$%%'`
+       if test "X$dir" = "X$libobj"; then
+         dir="$objdir"
+       else
+         dir="$dir/$objdir"
+       fi
+       libobj="$dir/"`$echo "X$libobj" | $Xsed -e 's%^.*/%%'`
+
+       if test -d "$dir"; then
+         $show "$rm $libobj"
+         $run $rm $libobj
+       else
+         $show "$mkdir $dir"
+         $run $mkdir $dir
+         status=$?
+         if test $status -ne 0 && test ! -d $dir; then
+           exit $status
+         fi
+       fi
+      fi
+      if test "$compiler_o_lo" = yes; then
+       output_obj="$libobj"
+       command="$command -o $output_obj"
+      elif test "$compiler_c_o" = yes; then
+       output_obj="$obj"
+       command="$command -o $output_obj"
+      fi
+
+      $run $rm "$output_obj"
+      $show "$command"
+      if $run eval "$command"; then :
+      else
+       test -n "$output_obj" && $run $rm $removelist
+       exit 1
+      fi
+
+      if test "$need_locks" = warn &&
+        test x"`cat $lockfile 2>/dev/null`" != x"$srcfile"; then
+       echo "\
+*** ERROR, $lockfile contains:
+`cat $lockfile 2>/dev/null`
+
+but it should contain:
+$srcfile
+
+This indicates that another process is trying to use the same
+temporary object file, and libtool could not work around it because
+your compiler does not support \`-c' and \`-o' together.  If you
+repeat this compilation, it may succeed, by chance, but you had better
+avoid parallel builds (make -j) in this platform, or get a better
+compiler."
+
+       $run $rm $removelist
+       exit 1
+      fi
+
+      # Just move the object if needed, then go on to compile the next one
+      if test x"$output_obj" != x"$libobj"; then
+       $show "$mv $output_obj $libobj"
+       if $run $mv $output_obj $libobj; then :
+       else
+         error=$?
+         $run $rm $removelist
+         exit $error
+       fi
+      fi
+
+      # If we have no pic_flag, then copy the object into place and finish.
+      if (test -z "$pic_flag" || test "$pic_mode" != default) &&
+        test "$build_old_libs" = yes; then
+       # Rename the .lo from within objdir to obj
+       if test -f $obj; then
+         $show $rm $obj
+         $run $rm $obj
+       fi
+
+       $show "$mv $libobj $obj"
+       if $run $mv $libobj $obj; then :
+       else
+         error=$?
+         $run $rm $removelist
+         exit $error
+       fi
+
+       xdir=`$echo "X$obj" | $Xsed -e 's%/[^/]*$%%'`
+       if test "X$xdir" = "X$obj"; then
+         xdir="."
+       else
+         xdir="$xdir"
+       fi
+       baseobj=`$echo "X$obj" | $Xsed -e "s%.*/%%"`
+       libobj=`$echo "X$baseobj" | $Xsed -e "$o2lo"`
+       # Now arrange that obj and lo_libobj become the same file
+       $show "(cd $xdir && $LN_S $baseobj $libobj)"
+       if $run eval '(cd $xdir && $LN_S $baseobj $libobj)'; then
+         # Unlock the critical section if it was locked
+         if test "$need_locks" != no; then
+           $run $rm "$lockfile"
+         fi
+         exit 0
+       else
+         error=$?
+         $run $rm $removelist
+         exit $error
+       fi
+      fi
+
+      # Allow error messages only from the first compilation.
+      suppress_output=' >/dev/null 2>&1'
+    fi
+
+    # Only build a position-dependent object if we build old libraries.
+    if test "$build_old_libs" = yes; then
+      if test "$pic_mode" != yes; then
+       # Don't build PIC code
+       command="$base_compile $srcfile"
+      else
+       # All platforms use -DPIC, to notify preprocessed assembler code.
+       command="$base_compile $srcfile $pic_flag -DPIC"
+      fi
+      if test "$compiler_c_o" = yes; then
+       command="$command -o $obj"
+       output_obj="$obj"
+      fi
+
+      # Suppress compiler output if we already did a PIC compilation.
+      command="$command$suppress_output"
+      $run $rm "$output_obj"
+      $show "$command"
+      if $run eval "$command"; then :
+      else
+       $run $rm $removelist
+       exit 1
+      fi
+
+      if test "$need_locks" = warn &&
+        test x"`cat $lockfile 2>/dev/null`" != x"$srcfile"; then
+       echo "\
+*** ERROR, $lockfile contains:
+`cat $lockfile 2>/dev/null`
+
+but it should contain:
+$srcfile
+
+This indicates that another process is trying to use the same
+temporary object file, and libtool could not work around it because
+your compiler does not support \`-c' and \`-o' together.  If you
+repeat this compilation, it may succeed, by chance, but you had better
+avoid parallel builds (make -j) in this platform, or get a better
+compiler."
+
+       $run $rm $removelist
+       exit 1
+      fi
+
+      # Just move the object if needed
+      if test x"$output_obj" != x"$obj"; then
+       $show "$mv $output_obj $obj"
+       if $run $mv $output_obj $obj; then :
+       else
+         error=$?
+         $run $rm $removelist
+         exit $error
+       fi
+      fi
+
+      # Create an invalid libtool object if no PIC, so that we do not
+      # accidentally link it into a program.
+      if test "$build_libtool_libs" != yes; then
+       $show "echo timestamp > $libobj"
+       $run eval "echo timestamp > \$libobj" || exit $?
+      else
+       # Move the .lo from within objdir
+       $show "$mv $libobj $lo_libobj"
+       if $run $mv $libobj $lo_libobj; then :
+       else
+         error=$?
+         $run $rm $removelist
+         exit $error
+       fi
+      fi
+    fi
+
+    # Unlock the critical section if it was locked
+    if test "$need_locks" != no; then
+      $run $rm "$lockfile"
+    fi
+
+    exit 0
+    ;;
+
+  # libtool link mode
+  link | relink)
+    modename="$modename: link"
+    case $host in
+    *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2*)
+      # It is impossible to link a dll without this setting, and
+      # we shouldn't force the makefile maintainer to figure out
+      # which system we are compiling for in order to pass an extra
+      # flag for every libtool invokation.
+      # allow_undefined=no
+
+      # FIXME: Unfortunately, there are problems with the above when trying
+      # to make a dll which has undefined symbols, in which case not
+      # even a static library is built.  For now, we need to specify
+      # -no-undefined on the libtool link line when we can be certain
+      # that all symbols are satisfied, otherwise we get a static library.
+      allow_undefined=yes
+      ;;
+    *)
+      allow_undefined=yes
+      ;;
+    esac
+    libtool_args="$nonopt"
+    compile_command="$nonopt"
+    finalize_command="$nonopt"
+
+    compile_rpath=
+    finalize_rpath=
+    compile_shlibpath=
+    finalize_shlibpath=
+    convenience=
+    old_convenience=
+    deplibs=
+    old_deplibs=
+    compiler_flags=
+    linker_flags=
+    dllsearchpath=
+    lib_search_path=`pwd`
+
+    avoid_version=no
+    dlfiles=
+    dlprefiles=
+    dlself=no
+    export_dynamic=no
+    export_symbols=
+    export_symbols_regex=
+    generated=
+    libobjs=
+    ltlibs=
+    module=no
+    no_install=no
+    objs=
+    prefer_static_libs=no
+    preload=no
+    prev=
+    prevarg=
+    release=
+    rpath=
+    xrpath=
+    perm_rpath=
+    temp_rpath=
+    thread_safe=no
+    vinfo=
+
+    # We need to know -static, to get the right output filenames.
+    for arg
+    do
+      case $arg in
+      -all-static | -static)
+       if test "X$arg" = "X-all-static"; then
+         if test "$build_libtool_libs" = yes && test -z "$link_static_flag"; then
+           $echo "$modename: warning: complete static linking is impossible in this configuration" 1>&2
+         fi
+         if test -n "$link_static_flag"; then
+           dlopen_self=$dlopen_self_static
+         fi
+       else
+         if test -z "$pic_flag" && test -n "$link_static_flag"; then
+           dlopen_self=$dlopen_self_static
+         fi
+       fi
+       build_libtool_libs=no
+       build_old_libs=yes
+       prefer_static_libs=yes
+       break
+       ;;
+      esac
+    done
+
+    # See if our shared archives depend on static archives.
+    test -n "$old_archive_from_new_cmds" && build_old_libs=yes
+
+    # Go through the arguments, transforming them on the way.
+    while test $# -gt 0; do
+      arg="$1"
+      shift
+      case $arg in
+      *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \    ]*|*]*|"")
+       qarg=\"`$echo "X$arg" | $Xsed -e "$sed_quote_subst"`\" ### testsuite: skip nested quoting test
+       ;;
+      *) qarg=$arg ;;
+      esac
+      libtool_args="$libtool_args $qarg"
+
+      # If the previous option needs an argument, assign it.
+      if test -n "$prev"; then
+       case $prev in
+       output)
+         compile_command="$compile_command @OUTPUT@"
+         finalize_command="$finalize_command @OUTPUT@"
+         ;;
+       esac
+
+       case $prev in
+       dlfiles|dlprefiles)
+         if test "$preload" = no; then
+           # Add the symbol object into the linking commands.
+           compile_command="$compile_command @SYMFILE@"
+           finalize_command="$finalize_command @SYMFILE@"
+           preload=yes
+         fi
+         case $arg in
+         *.la | *.lo) ;;  # We handle these cases below.
+         force)
+           if test "$dlself" = no; then
+             dlself=needless
+             export_dynamic=yes
+           fi
+           prev=
+           continue
+           ;;
+         self)
+           if test "$prev" = dlprefiles; then
+             dlself=yes
+           elif test "$prev" = dlfiles && test "$dlopen_self" != yes; then
+             dlself=yes
+           else
+             dlself=needless
+             export_dynamic=yes
+           fi
+           prev=
+           continue
+           ;;
+         *)
+           if test "$prev" = dlfiles; then
+             dlfiles="$dlfiles $arg"
+           else
+             dlprefiles="$dlprefiles $arg"
+           fi
+           prev=
+           continue
+           ;;
+         esac
+         ;;
+       expsyms)
+         export_symbols="$arg"
+         if test ! -f "$arg"; then
+           $echo "$modename: symbol file \`$arg' does not exist"
+           exit 1
+         fi
+         prev=
+         continue
+         ;;
+       expsyms_regex)
+         export_symbols_regex="$arg"
+         prev=
+         continue
+         ;;
+       release)
+         release="-$arg"
+         prev=
+         continue
+         ;;
+       rpath | xrpath)
+         # We need an absolute path.
+         case $arg in
+         [\\/]* | [A-Za-z]:[\\/]*) ;;
+         *)
+           $echo "$modename: only absolute run-paths are allowed" 1>&2
+           exit 1
+           ;;
+         esac
+         if test "$prev" = rpath; then
+           case "$rpath " in
+           *" $arg "*) ;;
+           *) rpath="$rpath $arg" ;;
+           esac
+         else
+           case "$xrpath " in
+           *" $arg "*) ;;
+           *) xrpath="$xrpath $arg" ;;
+           esac
+         fi
+         prev=
+         continue
+         ;;
+       xcompiler)
+         compiler_flags="$compiler_flags $qarg"
+         prev=
+         compile_command="$compile_command $qarg"
+         finalize_command="$finalize_command $qarg"
+         continue
+         ;;
+       xlinker)
+         linker_flags="$linker_flags $qarg"
+         compiler_flags="$compiler_flags $wl$qarg"
+         prev=
+         compile_command="$compile_command $wl$qarg"
+         finalize_command="$finalize_command $wl$qarg"
+         continue
+         ;;
+       *)
+         eval "$prev=\"\$arg\""
+         prev=
+         continue
+         ;;
+       esac
+      fi # test -n $prev
+
+      prevarg="$arg"
+
+      case $arg in
+      -all-static)
+       if test -n "$link_static_flag"; then
+         compile_command="$compile_command $link_static_flag"
+         finalize_command="$finalize_command $link_static_flag"
+       fi
+       continue
+       ;;
+
+      -allow-undefined)
+       # FIXME: remove this flag sometime in the future.
+       $echo "$modename: \`-allow-undefined' is deprecated because it is the default" 1>&2
+       continue
+       ;;
+
+      -avoid-version)
+       avoid_version=yes
+       continue
+       ;;
+
+      -dlopen)
+       prev=dlfiles
+       continue
+       ;;
+
+      -dlpreopen)
+       prev=dlprefiles
+       continue
+       ;;
+
+      -export-dynamic)
+       export_dynamic=yes
+       continue
+       ;;
+
+      -export-symbols | -export-symbols-regex)
+       if test -n "$export_symbols" || test -n "$export_symbols_regex"; then
+         $echo "$modename: more than one -exported-symbols argument is not allowed"
+         exit 1
+       fi
+       if test "X$arg" = "X-export-symbols"; then
+         prev=expsyms
+       else
+         prev=expsyms_regex
+       fi
+       continue
+       ;;
+
+      # The native IRIX linker understands -LANG:*, -LIST:* and -LNO:*
+      # so, if we see these flags be careful not to treat them like -L
+      -L[A-Z][A-Z]*:*)
+       case $with_gcc/$host in
+       no/*-*-irix*)
+         compile_command="$compile_command $arg"
+         finalize_command="$finalize_command $arg"
+         ;;
+       esac
+       continue
+       ;;
+
+      -L*)
+       dir=`$echo "X$arg" | $Xsed -e 's/^-L//'`
+       # We need an absolute path.
+       case $dir in
+       [\\/]* | [A-Za-z]:[\\/]*) ;;
+       *)
+         absdir=`cd "$dir" && pwd`
+         if test -z "$absdir"; then
+           $echo "$modename: cannot determine absolute directory name of \`$dir'" 1>&2
+           exit 1
+         fi
+         dir="$absdir"
+         ;;
+       esac
+       case "$deplibs " in
+       *" -L$dir "*) ;;
+       *)
+         deplibs="$deplibs -L$dir"
+         lib_search_path="$lib_search_path $dir"
+         ;;
+       esac
+       case $host in
+       *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2*)
+         case :$dllsearchpath: in
+         *":$dir:"*) ;;
+         *) dllsearchpath="$dllsearchpath:$dir";;
+         esac
+         ;;
+       esac
+       continue
+       ;;
+
+      -l*)
+       if test "X$arg" = "X-lc" || test "X$arg" = "X-lm"; then
+         case $host in
+         *-*-cygwin* | *-*-pw32* | *-*-beos*)
+           # These systems don't actually have a C or math library (as such)
+           continue
+           ;;
+         *-*-mingw* | *-*-os2*)
+           # These systems don't actually have a C library (as such)
+           test "X$arg" = "X-lc" && continue
+           ;;
+         *-*-openbsd*)
+           # Do not include libc due to us having libc/libc_r.
+           test "X$arg" = "X-lc" && continue
+           ;;
+         esac
+        elif test "X$arg" = "X-lc_r"; then
+         case $host in
+         *-*-openbsd*)
+           # Do not include libc_r directly, use -pthread flag.
+           continue
+           ;;
+         esac
+       fi
+       deplibs="$deplibs $arg"
+       continue
+       ;;
+
+      -module)
+       module=yes
+       continue
+       ;;
+
+      -no-fast-install)
+       fast_install=no
+       continue
+       ;;
+
+      -no-install)
+       case $host in
+       *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2*)
+         # The PATH hackery in wrapper scripts is required on Windows
+         # in order for the loader to find any dlls it needs.
+         $echo "$modename: warning: \`-no-install' is ignored for $host" 1>&2
+         $echo "$modename: warning: assuming \`-no-fast-install' instead" 1>&2
+         fast_install=no
+         ;;
+       *) no_install=yes ;;
+       esac
+       continue
+       ;;
+
+      -no-undefined)
+       allow_undefined=no
+       continue
+       ;;
+
+      -o) prev=output ;;
+
+      -release)
+       prev=release
+       continue
+       ;;
+
+      -rpath)
+       prev=rpath
+       continue
+       ;;
+
+      -R)
+       prev=xrpath
+       continue
+       ;;
+
+      -R*)
+       dir=`$echo "X$arg" | $Xsed -e 's/^-R//'`
+       # We need an absolute path.
+       case $dir in
+       [\\/]* | [A-Za-z]:[\\/]*) ;;
+       *)
+         $echo "$modename: only absolute run-paths are allowed" 1>&2
+         exit 1
+         ;;
+       esac
+       case "$xrpath " in
+       *" $dir "*) ;;
+       *) xrpath="$xrpath $dir" ;;
+       esac
+       continue
+       ;;
+
+      -static)
+       # The effects of -static are defined in a previous loop.
+       # We used to do the same as -all-static on platforms that
+       # didn't have a PIC flag, but the assumption that the effects
+       # would be equivalent was wrong.  It would break on at least
+       # Digital Unix and AIX.
+       continue
+       ;;
+
+      -thread-safe)
+       thread_safe=yes
+       continue
+       ;;
+
+      -version-info)
+       prev=vinfo
+       continue
+       ;;
+
+      -Wc,*)
+       args=`$echo "X$arg" | $Xsed -e "$sed_quote_subst" -e 's/^-Wc,//'`
+       arg=
+       save_ifs="$IFS"; IFS=','
+       for flag in $args; do
+         IFS="$save_ifs"
+         case $flag in
+           *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \       ]*|*]*|"")
+           flag="\"$flag\""
+           ;;
+         esac
+         arg="$arg $wl$flag"
+         compiler_flags="$compiler_flags $flag"
+       done
+       IFS="$save_ifs"
+       arg=`$echo "X$arg" | $Xsed -e "s/^ //"`
+       ;;
+
+      -Wl,*)
+       args=`$echo "X$arg" | $Xsed -e "$sed_quote_subst" -e 's/^-Wl,//'`
+       arg=
+       save_ifs="$IFS"; IFS=','
+       for flag in $args; do
+         IFS="$save_ifs"
+         case $flag in
+           *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \       ]*|*]*|"")
+           flag="\"$flag\""
+           ;;
+         esac
+         arg="$arg $wl$flag"
+         compiler_flags="$compiler_flags $wl$flag"
+         linker_flags="$linker_flags $flag"
+       done
+       IFS="$save_ifs"
+       arg=`$echo "X$arg" | $Xsed -e "s/^ //"`
+       ;;
+
+      -Xcompiler)
+       prev=xcompiler
+       continue
+       ;;
+
+      -Xlinker)
+       prev=xlinker
+       continue
+       ;;
+
+      # Some other compiler flag.
+      -* | +*)
+       # Unknown arguments in both finalize_command and compile_command need
+       # to be aesthetically quoted because they are evaled later.
+       arg=`$echo "X$arg" | $Xsed -e "$sed_quote_subst"`
+       case $arg in
+       *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \   ]*|*]*|"")
+         arg="\"$arg\""
+         ;;
+       esac
+       ;;
+
+      *.lo | *.$objext)
+       # A library or standard object.
+       if test "$prev" = dlfiles; then
+         # This file was specified with -dlopen.
+         if test "$build_libtool_libs" = yes && test "$dlopen_support" = yes; then
+           dlfiles="$dlfiles $arg"
+           prev=
+           continue
+         else
+           # If libtool objects are unsupported, then we need to preload.
+           prev=dlprefiles
+         fi
+       fi
+
+       if test "$prev" = dlprefiles; then
+         # Preload the old-style object.
+         dlprefiles="$dlprefiles "`$echo "X$arg" | $Xsed -e "$lo2o"`
+         prev=
+       else
+         case $arg in
+         *.lo) libobjs="$libobjs $arg" ;;
+         *) objs="$objs $arg" ;;
+         esac
+       fi
+       ;;
+
+      *.$libext)
+       # An archive.
+       deplibs="$deplibs $arg"
+       old_deplibs="$old_deplibs $arg"
+       continue
+       ;;
+
+      *.la)
+       # A libtool-controlled library.
+
+       if test "$prev" = dlfiles; then
+         # This library was specified with -dlopen.
+         dlfiles="$dlfiles $arg"
+         prev=
+       elif test "$prev" = dlprefiles; then
+         # The library was specified with -dlpreopen.
+         dlprefiles="$dlprefiles $arg"
+         prev=
+       else
+         deplibs="$deplibs $arg"
+       fi
+       continue
+       ;;
+
+      # Some other compiler argument.
+      *)
+       # Unknown arguments in both finalize_command and compile_command need
+       # to be aesthetically quoted because they are evaled later.
+       arg=`$echo "X$arg" | $Xsed -e "$sed_quote_subst"`
+       case $arg in
+       *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \   ]*|*]*|"")
+         arg="\"$arg\""
+         ;;
+       esac
+       ;;
+      esac # arg
+
+      # Now actually substitute the argument into the commands.
+      if test -n "$arg"; then
+       compile_command="$compile_command $arg"
+       finalize_command="$finalize_command $arg"
+      fi
+    done # argument parsing loop
+
+    if test -n "$prev"; then
+      $echo "$modename: the \`$prevarg' option requires an argument" 1>&2
+      $echo "$help" 1>&2
+      exit 1
+    fi
+
+    if test "$export_dynamic" = yes && test -n "$export_dynamic_flag_spec"; then
+      eval arg=\"$export_dynamic_flag_spec\"
+      compile_command="$compile_command $arg"
+      finalize_command="$finalize_command $arg"
+    fi
+
+    # calculate the name of the file, without its directory
+    outputname=`$echo "X$output" | $Xsed -e 's%^.*/%%'`
+    libobjs_save="$libobjs"
+
+    if test -n "$shlibpath_var"; then
+      # get the directories listed in $shlibpath_var
+      eval shlib_search_path=\`\$echo \"X\${$shlibpath_var}\" \| \$Xsed -e \'s/:/ /g\'\`
+    else
+      shlib_search_path=
+    fi
+    eval sys_lib_search_path=\"$sys_lib_search_path_spec\"
+    eval sys_lib_dlsearch_path=\"$sys_lib_dlsearch_path_spec\"
+
+    output_objdir=`$echo "X$output" | $Xsed -e 's%/[^/]*$%%'`
+    if test "X$output_objdir" = "X$output"; then
+      output_objdir="$objdir"
+    else
+      output_objdir="$output_objdir/$objdir"
+    fi
+    # Create the object directory.
+    if test ! -d $output_objdir; then
+      $show "$mkdir $output_objdir"
+      $run $mkdir $output_objdir
+      status=$?
+      if test $status -ne 0 && test ! -d $output_objdir; then
+       exit $status
+      fi
+    fi
+
+    # Determine the type of output
+    case $output in
+    "")
+      $echo "$modename: you must specify an output file" 1>&2
+      $echo "$help" 1>&2
+      exit 1
+      ;;
+    *.$libext) linkmode=oldlib ;;
+    *.lo | *.$objext) linkmode=obj ;;
+    *.la) linkmode=lib ;;
+    *) linkmode=prog ;; # Anything else should be a program.
+    esac
+
+    specialdeplibs=
+    libs=
+    # Find all interdependent deplibs by searching for libraries
+    # that are linked more than once (e.g. -la -lb -la)
+    for deplib in $deplibs; do
+      case "$libs " in
+      *" $deplib "*) specialdeplibs="$specialdeplibs $deplib" ;;
+      esac
+      libs="$libs $deplib"
+    done
+    deplibs=
+    newdependency_libs=
+    newlib_search_path=
+    need_relink=no # whether we're linking any uninstalled libtool libraries
+    notinst_deplibs= # not-installed libtool libraries
+    notinst_path= # paths that contain not-installed libtool libraries
+    case $linkmode in
+    lib)
+       passes="conv link"
+       for file in $dlfiles $dlprefiles; do
+         case $file in
+         *.la) ;;
+         *)
+           $echo "$modename: libraries can \`-dlopen' only libtool libraries: $file" 1>&2
+           exit 1
+           ;;
+         esac
+       done
+       ;;
+    prog)
+       compile_deplibs=
+       finalize_deplibs=
+       alldeplibs=no
+       newdlfiles=
+       newdlprefiles=
+       passes="conv scan dlopen dlpreopen link"
+       ;;
+    *)  passes="conv"
+       ;;
+    esac
+    for pass in $passes; do
+      if test $linkmode = prog; then
+       # Determine which files to process
+       case $pass in
+       dlopen)
+         libs="$dlfiles"
+         save_deplibs="$deplibs" # Collect dlpreopened libraries
+         deplibs=
+         ;;
+       dlpreopen) libs="$dlprefiles" ;;
+       link) libs="$deplibs %DEPLIBS% $dependency_libs" ;;
+       esac
+      fi
+      for deplib in $libs; do
+       lib=
+       found=no
+       case $deplib in
+       -l*)
+         if test $linkmode = oldlib && test $linkmode = obj; then
+           $echo "$modename: warning: \`-l' is ignored for archives/objects: $deplib" 1>&2
+           continue
+         fi
+         if test $pass = conv; then
+           deplibs="$deplib $deplibs"
+           continue
+         fi
+         name=`$echo "X$deplib" | $Xsed -e 's/^-l//'`
+         for searchdir in $newlib_search_path $lib_search_path $sys_lib_search_path $shlib_search_path; do
+           # Search the libtool library
+           lib="$searchdir/lib${name}.la"
+           if test -f "$lib"; then
+             found=yes
+             break
+           fi
+         done
+         if test "$found" != yes; then
+           # deplib doesn't seem to be a libtool library
+           if test "$linkmode,$pass" = "prog,link"; then
+             compile_deplibs="$deplib $compile_deplibs"
+             finalize_deplibs="$deplib $finalize_deplibs"
+           else
+             deplibs="$deplib $deplibs"
+             test $linkmode = lib && newdependency_libs="$deplib $newdependency_libs"
+           fi
+           continue
+         fi
+         ;; # -l
+       -L*)
+         case $linkmode in
+         lib)
+           deplibs="$deplib $deplibs"
+           test $pass = conv && continue
+           newdependency_libs="$deplib $newdependency_libs"
+           newlib_search_path="$newlib_search_path "`$echo "X$deplib" | $Xsed -e 's/^-L//'`
+           ;;
+         prog)
+           if test $pass = conv; then
+             deplibs="$deplib $deplibs"
+             continue
+           fi
+           if test $pass = scan; then
+             deplibs="$deplib $deplibs"
+             newlib_search_path="$newlib_search_path "`$echo "X$deplib" | $Xsed -e 's/^-L//'`
+           else
+             compile_deplibs="$deplib $compile_deplibs"
+             finalize_deplibs="$deplib $finalize_deplibs"
+           fi
+           ;;
+         *)
+           $echo "$modename: warning: \`-L' is ignored for archives/objects: $deplib" 1>&2
+           ;;
+         esac # linkmode
+         continue
+         ;; # -L
+       -R*)
+         if test $pass = link; then
+           dir=`$echo "X$deplib" | $Xsed -e 's/^-R//'`
+           # Make sure the xrpath contains only unique directories.
+           case "$xrpath " in
+           *" $dir "*) ;;
+           *) xrpath="$xrpath $dir" ;;
+           esac
+         fi
+         deplibs="$deplib $deplibs"
+         continue
+         ;;
+       *.la) lib="$deplib" ;;
+       *.$libext)
+         if test $pass = conv; then
+           deplibs="$deplib $deplibs"
+           continue
+         fi
+         case $linkmode in
+         lib)
+           if test "$deplibs_check_method" != pass_all; then
+             echo
+             echo "*** Warning: This library needs some functionality provided by $deplib."
+             echo "*** I have the capability to make that library automatically link in when"
+             echo "*** you link to this library.  But I can only do this if you have a"
+             echo "*** shared version of the library, which you do not appear to have."
+           else
+             echo
+             echo "*** Warning: Linking the shared library $output against the"
+             echo "*** static library $deplib is not portable!"
+             deplibs="$deplib $deplibs"
+           fi
+           continue
+           ;;
+         prog)
+           if test $pass != link; then
+             deplibs="$deplib $deplibs"
+           else
+             compile_deplibs="$deplib $compile_deplibs"
+             finalize_deplibs="$deplib $finalize_deplibs"
+           fi
+           continue
+           ;;
+         esac # linkmode
+         ;; # *.$libext
+       *.lo | *.$objext)
+         if test $pass = dlpreopen || test "$dlopen_support" != yes || test "$build_libtool_libs" = no; then
+           # If there is no dlopen support or we're linking statically,
+           # we need to preload.
+           newdlprefiles="$newdlprefiles $deplib"
+           compile_deplibs="$deplib $compile_deplibs"
+           finalize_deplibs="$deplib $finalize_deplibs"
+         else
+           newdlfiles="$newdlfiles $deplib"
+         fi
+         continue
+         ;;
+       %DEPLIBS%)
+         alldeplibs=yes
+         continue
+         ;;
+       esac # case $deplib
+       if test $found = yes || test -f "$lib"; then :
+       else
+         $echo "$modename: cannot find the library \`$lib'" 1>&2
+         exit 1
+       fi
+
+       # Check to see that this really is a libtool archive.
+       if (sed -e '2q' $lib | egrep "^# Generated by .*$PACKAGE") >/dev/null 2>&1; then :
+       else
+         $echo "$modename: \`$lib' is not a valid libtool archive" 1>&2
+         exit 1
+       fi
+
+       ladir=`$echo "X$lib" | $Xsed -e 's%/[^/]*$%%'`
+       test "X$ladir" = "X$lib" && ladir="."
+
+       dlname=
+       dlopen=
+       dlpreopen=
+       libdir=
+       library_names=
+       old_library=
+       # If the library was installed with an old release of libtool,
+       # it will not redefine variable installed.
+       installed=yes
+
+       # Read the .la file
+       case $lib in
+       */* | *\\*) . $lib ;;
+       *) . ./$lib ;;
+       esac
+
+       if test "$linkmode,$pass" = "lib,link" ||
+          test "$linkmode,$pass" = "prog,scan" ||
+          { test $linkmode = oldlib && test $linkmode = obj; }; then
+          # Add dl[pre]opened files of deplib
+         test -n "$dlopen" && dlfiles="$dlfiles $dlopen"
+         test -n "$dlpreopen" && dlprefiles="$dlprefiles $dlpreopen"
+       fi
+
+       if test $pass = conv; then
+         # Only check for convenience libraries
+         deplibs="$lib $deplibs"
+         if test -z "$libdir"; then
+           if test -z "$old_library"; then
+             $echo "$modename: cannot find name of link library for \`$lib'" 1>&2
+             exit 1
+           fi
+           # It is a libtool convenience library, so add in its objects.
+           convenience="$convenience $ladir/$objdir/$old_library"
+           old_convenience="$old_convenience $ladir/$objdir/$old_library"
+           tmp_libs=
+           for deplib in $dependency_libs; do
+             deplibs="$deplib $deplibs"
+             case "$tmp_libs " in
+             *" $deplib "*) specialdeplibs="$specialdeplibs $deplib" ;;
+             esac
+             tmp_libs="$tmp_libs $deplib"
+           done
+         elif test $linkmode != prog && test $linkmode != lib; then
+           $echo "$modename: \`$lib' is not a convenience library" 1>&2
+           exit 1
+         fi
+         continue
+       fi # $pass = conv
+
+       # Get the name of the library we link against.
+       linklib=
+       for l in $old_library $library_names; do
+         linklib="$l"
+       done
+       if test -z "$linklib"; then
+         $echo "$modename: cannot find name of link library for \`$lib'" 1>&2
+         exit 1
+       fi
+
+       # This library was specified with -dlopen.
+       if test $pass = dlopen; then
+         if test -z "$libdir"; then
+           $echo "$modename: cannot -dlopen a convenience library: \`$lib'" 1>&2
+           exit 1
+         fi
+         if test -z "$dlname" || test "$dlopen_support" != yes || test "$build_libtool_libs" = no; then
+           # If there is no dlname, no dlopen support or we're linking
+           # statically, we need to preload.
+           dlprefiles="$dlprefiles $lib"
+         else
+           newdlfiles="$newdlfiles $lib"
+         fi
+         continue
+       fi # $pass = dlopen
+
+       # We need an absolute path.
+       case $ladir in
+       [\\/]* | [A-Za-z]:[\\/]*) abs_ladir="$ladir" ;;
+       *)
+         abs_ladir=`cd "$ladir" && pwd`
+         if test -z "$abs_ladir"; then
+           $echo "$modename: warning: cannot determine absolute directory name of \`$ladir'" 1>&2
+           $echo "$modename: passing it literally to the linker, although it might fail" 1>&2
+           abs_ladir="$ladir"
+         fi
+         ;;
+       esac
+       laname=`$echo "X$lib" | $Xsed -e 's%^.*/%%'`
+
+       # Find the relevant object directory and library name.
+       if test "X$installed" = Xyes; then
+         if test ! -f "$libdir/$linklib" && test -f "$abs_ladir/$linklib"; then
+           $echo "$modename: warning: library \`$lib' was moved." 1>&2
+           dir="$ladir"
+           absdir="$abs_ladir"
+           libdir="$abs_ladir"
+         else
+           dir="$libdir"
+           absdir="$libdir"
+         fi
+       else
+         dir="$ladir/$objdir"
+         absdir="$abs_ladir/$objdir"
+         # Remove this search path later
+         notinst_path="$notinst_path $abs_ladir"
+       fi # $installed = yes
+       name=`$echo "X$laname" | $Xsed -e 's/\.la$//' -e 's/^lib//'`
+
+       # This library was specified with -dlpreopen.
+       if test $pass = dlpreopen; then
+         if test -z "$libdir"; then
+           $echo "$modename: cannot -dlpreopen a convenience library: \`$lib'" 1>&2
+           exit 1
+         fi
+         # Prefer using a static library (so that no silly _DYNAMIC symbols
+         # are required to link).
+         if test -n "$old_library"; then
+           newdlprefiles="$newdlprefiles $dir/$old_library"
+         # Otherwise, use the dlname, so that lt_dlopen finds it.
+         elif test -n "$dlname"; then
+           newdlprefiles="$newdlprefiles $dir/$dlname"
+         else
+           newdlprefiles="$newdlprefiles $dir/$linklib"
+         fi
+       fi # $pass = dlpreopen
+
+       if test -z "$libdir"; then
+         # Link the convenience library
+         if test $linkmode = lib; then
+           deplibs="$dir/$old_library $deplibs"
+         elif test "$linkmode,$pass" = "prog,link"; then
+           compile_deplibs="$dir/$old_library $compile_deplibs"
+           finalize_deplibs="$dir/$old_library $finalize_deplibs"
+         else
+           deplibs="$lib $deplibs"
+         fi
+         continue
+       fi
+
+       if test $linkmode = prog && test $pass != link; then
+         newlib_search_path="$newlib_search_path $ladir"
+         deplibs="$lib $deplibs"
+
+         linkalldeplibs=no
+         if test "$link_all_deplibs" != no || test -z "$library_names" ||
+            test "$build_libtool_libs" = no; then
+           linkalldeplibs=yes
+         fi
+
+         tmp_libs=
+         for deplib in $dependency_libs; do
+           case $deplib in
+           -L*) newlib_search_path="$newlib_search_path "`$echo "X$deplib" | $Xsed -e 's/^-L//'`;; ### testsuite: skip nested quoting test
+           esac
+           # Need to link against all dependency_libs?
+           if test $linkalldeplibs = yes; then
+             deplibs="$deplib $deplibs"
+           else
+             # Need to hardcode shared library paths
+             # or/and link against static libraries
+             newdependency_libs="$deplib $newdependency_libs"
+           fi
+           case "$tmp_libs " in
+           *" $deplib "*) specialdeplibs="$specialdeplibs $deplib" ;;
+           esac
+           tmp_libs="$tmp_libs $deplib"
+         done # for deplib
+         continue
+       fi # $linkmode = prog...
+
+       link_static=no # Whether the deplib will be linked statically
+       if test -n "$library_names" &&
+          { test "$prefer_static_libs" = no || test -z "$old_library"; }; then
+         # Link against this shared library
+
+         if test "$linkmode,$pass" = "prog,link" ||
+          { test $linkmode = lib && test $hardcode_into_libs = yes; }; then
+           # Hardcode the library path.
+           # Skip directories that are in the system default run-time
+           # search path.
+           case " $sys_lib_dlsearch_path " in
+           *" $absdir "*) ;;
+           *)
+             case "$compile_rpath " in
+             *" $absdir "*) ;;
+             *) compile_rpath="$compile_rpath $absdir"
+             esac
+             ;;
+           esac
+           case " $sys_lib_dlsearch_path " in
+           *" $libdir "*) ;;
+           *)
+             case "$finalize_rpath " in
+             *" $libdir "*) ;;
+             *) finalize_rpath="$finalize_rpath $libdir"
+             esac
+             ;;
+           esac
+           if test $linkmode = prog; then
+             # We need to hardcode the library path
+             if test -n "$shlibpath_var"; then
+               # Make sure the rpath contains only unique directories.
+               case "$temp_rpath " in
+               *" $dir "*) ;;
+               *" $absdir "*) ;;
+               *) temp_rpath="$temp_rpath $dir" ;;
+               esac
+             fi
+           fi
+         fi # $linkmode,$pass = prog,link...
+
+         if test "$alldeplibs" = yes &&
+            { test "$deplibs_check_method" = pass_all ||
+              { test "$build_libtool_libs" = yes &&
+                test -n "$library_names"; }; }; then
+           # We only need to search for static libraries
+           continue
+         fi
+
+         if test "$installed" = no; then
+           notinst_deplibs="$notinst_deplibs $lib"
+           need_relink=yes
+         fi
+
+         if test -n "$old_archive_from_expsyms_cmds"; then
+           # figure out the soname
+           set dummy $library_names
+           realname="$2"
+           shift; shift
+           libname=`eval \\$echo \"$libname_spec\"`
+           # use dlname if we got it. it's perfectly good, no?
+           if test -n "$dlname"; then
+             soname="$dlname"
+           elif test -n "$soname_spec"; then
+             # bleh windows
+             case $host in
+             *cygwin*)
+               major=`expr $current - $age`
+               versuffix="-$major"
+               ;;
+             esac
+             eval soname=\"$soname_spec\"
+           else
+             soname="$realname"
+           fi
+
+           # Make a new name for the extract_expsyms_cmds to use
+           soroot="$soname"
+           soname=`echo $soroot | sed -e 's/^.*\///'`
+           newlib="libimp-`echo $soname | sed 's/^lib//;s/\.dll$//'`.a"
+
+           # If the library has no export list, then create one now
+           if test -f "$output_objdir/$soname-def"; then :
+           else
+             $show "extracting exported symbol list from \`$soname'"
+             save_ifs="$IFS"; IFS='~'
+             eval cmds=\"$extract_expsyms_cmds\"
+             for cmd in $cmds; do
+               IFS="$save_ifs"
+               $show "$cmd"
+               $run eval "$cmd" || exit $?
+             done
+             IFS="$save_ifs"
+           fi
+
+           # Create $newlib
+           if test -f "$output_objdir/$newlib"; then :; else
+             $show "generating import library for \`$soname'"
+             save_ifs="$IFS"; IFS='~'
+             eval cmds=\"$old_archive_from_expsyms_cmds\"
+             for cmd in $cmds; do
+               IFS="$save_ifs"
+               $show "$cmd"
+               $run eval "$cmd" || exit $?
+             done
+             IFS="$save_ifs"
+           fi
+           # make sure the library variables are pointing to the new library
+           dir=$output_objdir
+           linklib=$newlib
+         fi # test -n $old_archive_from_expsyms_cmds
+
+         if test $linkmode = prog || test "$mode" != relink; then
+           add_shlibpath=
+           add_dir=
+           add=
+           lib_linked=yes
+           case $hardcode_action in
+           immediate | unsupported)
+             if test "$hardcode_direct" = no; then
+               add="$dir/$linklib"
+             elif test "$hardcode_minus_L" = no; then
+               case $host in
+               *-*-sunos*) add_shlibpath="$dir" ;;
+               esac
+               add_dir="-L$dir"
+               add="-l$name"
+             elif test "$hardcode_shlibpath_var" = no; then
+               add_shlibpath="$dir"
+               add="-l$name"
+             else
+               lib_linked=no
+             fi
+             ;;
+           relink)
+             if test "$hardcode_direct" = yes; then
+               add="$dir/$linklib"
+             elif test "$hardcode_minus_L" = yes; then
+               add_dir="-L$dir"
+               add="-l$name"
+             elif test "$hardcode_shlibpath_var" = yes; then
+               add_shlibpath="$dir"
+               add="-l$name"
+             else
+               lib_linked=no
+             fi
+             ;;
+           *) lib_linked=no ;;
+           esac
+
+           if test "$lib_linked" != yes; then
+             $echo "$modename: configuration error: unsupported hardcode properties"
+             exit 1
+           fi
+
+           if test -n "$add_shlibpath"; then
+             case :$compile_shlibpath: in
+             *":$add_shlibpath:"*) ;;
+             *) compile_shlibpath="$compile_shlibpath$add_shlibpath:" ;;
+             esac
+           fi
+           if test $linkmode = prog; then
+             test -n "$add_dir" && compile_deplibs="$add_dir $compile_deplibs"
+             test -n "$add" && compile_deplibs="$add $compile_deplibs"
+           else
+             test -n "$add_dir" && deplibs="$add_dir $deplibs"
+             test -n "$add" && deplibs="$add $deplibs"
+             if test "$hardcode_direct" != yes && \
+                test "$hardcode_minus_L" != yes && \
+                test "$hardcode_shlibpath_var" = yes; then
+               case :$finalize_shlibpath: in
+               *":$libdir:"*) ;;
+               *) finalize_shlibpath="$finalize_shlibpath$libdir:" ;;
+               esac
+             fi
+           fi
+         fi
+
+         if test $linkmode = prog || test "$mode" = relink; then
+           add_shlibpath=
+           add_dir=
+           add=
+           # Finalize command for both is simple: just hardcode it.
+           if test "$hardcode_direct" = yes; then
+             add="$libdir/$linklib"
+           elif test "$hardcode_minus_L" = yes; then
+             add_dir="-L$libdir"
+             add="-l$name"
+           elif test "$hardcode_shlibpath_var" = yes; then
+             case :$finalize_shlibpath: in
+             *":$libdir:"*) ;;
+             *) finalize_shlibpath="$finalize_shlibpath$libdir:" ;;
+             esac
+             add="-l$name"
+           else
+             # We cannot seem to hardcode it, guess we'll fake it.
+             add_dir="-L$libdir"
+             add="-l$name"
+           fi
+
+           if test $linkmode = prog; then
+             test -n "$add_dir" && finalize_deplibs="$add_dir $finalize_deplibs"
+             test -n "$add" && finalize_deplibs="$add $finalize_deplibs"
+           else
+             test -n "$add_dir" && deplibs="$add_dir $deplibs"
+             test -n "$add" && deplibs="$add $deplibs"
+           fi
+         fi
+       elif test $linkmode = prog; then
+         if test "$alldeplibs" = yes &&
+            { test "$deplibs_check_method" = pass_all ||
+              { test "$build_libtool_libs" = yes &&
+                test -n "$library_names"; }; }; then
+           # We only need to search for static libraries
+           continue
+         fi
+
+         # Try to link the static library
+         # Here we assume that one of hardcode_direct or hardcode_minus_L
+         # is not unsupported.  This is valid on all known static and
+         # shared platforms.
+         if test "$hardcode_direct" != unsupported; then
+           test -n "$old_library" && linklib="$old_library"
+           compile_deplibs="$dir/$linklib $compile_deplibs"
+           finalize_deplibs="$dir/$linklib $finalize_deplibs"
+         else
+           compile_deplibs="-l$name -L$dir $compile_deplibs"
+           finalize_deplibs="-l$name -L$dir $finalize_deplibs"
+         fi
+       elif test "$build_libtool_libs" = yes; then
+         # Not a shared library
+         if test "$deplibs_check_method" != pass_all; then
+           # We're trying link a shared library against a static one
+           # but the system doesn't support it.
+
+           # Just print a warning and add the library to dependency_libs so
+           # that the program can be linked against the static library.
+           echo
+           echo "*** Warning: This library needs some functionality provided by $lib."
+           echo "*** I have the capability to make that library automatically link in when"
+           echo "*** you link to this library.  But I can only do this if you have a"
+           echo "*** shared version of the library, which you do not appear to have."
+           if test "$module" = yes; then
+             echo "*** Therefore, libtool will create a static module, that should work "
+             echo "*** as long as the dlopening application is linked with the -dlopen flag."
+             if test -z "$global_symbol_pipe"; then
+               echo
+               echo "*** However, this would only work if libtool was able to extract symbol"
+               echo "*** lists from a program, using \`nm' or equivalent, but libtool could"
+               echo "*** not find such a program.  So, this module is probably useless."
+               echo "*** \`nm' from GNU binutils and a full rebuild may help."
+             fi
+             if test "$build_old_libs" = no; then
+               build_libtool_libs=module
+               build_old_libs=yes
+             else
+               build_libtool_libs=no
+             fi
+           fi
+         else
+           convenience="$convenience $dir/$old_library"
+           old_convenience="$old_convenience $dir/$old_library"
+           deplibs="$dir/$old_library $deplibs"
+           link_static=yes
+         fi
+       fi # link shared/static library?
+
+       if test $linkmode = lib; then
+         if test -n "$dependency_libs" &&
+            { test $hardcode_into_libs != yes || test $build_old_libs = yes ||
+              test $link_static = yes; }; then
+           # Extract -R from dependency_libs
+           temp_deplibs=
+           for libdir in $dependency_libs; do
+             case $libdir in
+             -R*) temp_xrpath=`$echo "X$libdir" | $Xsed -e 's/^-R//'`
+                  case " $xrpath " in
+                  *" $temp_xrpath "*) ;;
+                  *) xrpath="$xrpath $temp_xrpath";;
+                  esac;;
+             *) temp_deplibs="$temp_deplibs $libdir";;
+             esac
+           done
+           dependency_libs="$temp_deplibs"
+         fi
+
+         newlib_search_path="$newlib_search_path $absdir"
+         # Link against this library
+         test "$link_static" = no && newdependency_libs="$abs_ladir/$laname $newdependency_libs"
+         # ... and its dependency_libs
+         tmp_libs=
+         for deplib in $dependency_libs; do
+           newdependency_libs="$deplib $newdependency_libs"
+           case "$tmp_libs " in
+           *" $deplib "*) specialdeplibs="$specialdeplibs $deplib" ;;
+           esac
+           tmp_libs="$tmp_libs $deplib"
+         done
+
+         if test $link_all_deplibs != no; then
+           # Add the search paths of all dependency libraries
+           for deplib in $dependency_libs; do
+             case $deplib in
+             -L*) path="$deplib" ;;
+             *.la)
+               dir=`$echo "X$deplib" | $Xsed -e 's%/[^/]*$%%'`
+               test "X$dir" = "X$deplib" && dir="."
+               # We need an absolute path.
+               case $dir in
+               [\\/]* | [A-Za-z]:[\\/]*) absdir="$dir" ;;
+               *)
+                 absdir=`cd "$dir" && pwd`
+                 if test -z "$absdir"; then
+                   $echo "$modename: warning: cannot determine absolute directory name of \`$dir'" 1>&2
+                   absdir="$dir"
+                 fi
+                 ;;
+               esac
+               if grep "^installed=no" $deplib > /dev/null; then
+                 path="-L$absdir/$objdir"
+               else
+                 eval libdir=`sed -n -e 's/^libdir=\(.*\)$/\1/p' $deplib`
+                 if test -z "$libdir"; then
+                   $echo "$modename: \`$deplib' is not a valid libtool archive" 1>&2
+                   exit 1
+                 fi
+                 if test "$absdir" != "$libdir"; then
+                   $echo "$modename: warning: \`$deplib' seems to be moved" 1>&2
+                 fi
+                 path="-L$absdir"
+               fi
+               ;;
+             *) continue ;;
+             esac
+             case " $deplibs " in
+             *" $path "*) ;;
+             *) deplibs="$deplibs $path" ;;
+             esac
+           done
+         fi # link_all_deplibs != no
+       fi # linkmode = lib
+      done # for deplib in $libs
+      if test $pass = dlpreopen; then
+       # Link the dlpreopened libraries before other libraries
+       for deplib in $save_deplibs; do
+         deplibs="$deplib $deplibs"
+       done
+      fi
+      if test $pass != dlopen; then
+       test $pass != scan && dependency_libs="$newdependency_libs"
+       if test $pass != conv; then
+         # Make sure lib_search_path contains only unique directories.
+         lib_search_path=
+         for dir in $newlib_search_path; do
+           case "$lib_search_path " in
+           *" $dir "*) ;;
+           *) lib_search_path="$lib_search_path $dir" ;;
+           esac
+         done
+         newlib_search_path=
+       fi
+
+       if test "$linkmode,$pass" != "prog,link"; then
+         vars="deplibs"
+       else
+         vars="compile_deplibs finalize_deplibs"
+       fi
+       for var in $vars dependency_libs; do
+         # Add libraries to $var in reverse order
+         eval tmp_libs=\"\$$var\"
+         new_libs=
+         for deplib in $tmp_libs; do
+           case $deplib in
+           -L*) new_libs="$deplib $new_libs" ;;
+           *)
+             case " $specialdeplibs " in
+             *" $deplib "*) new_libs="$deplib $new_libs" ;;
+             *)
+               case " $new_libs " in
+               *" $deplib "*) ;;
+               *) new_libs="$deplib $new_libs" ;;
+               esac
+               ;;
+             esac
+             ;;
+           esac
+         done
+         tmp_libs=
+         for deplib in $new_libs; do
+           case $deplib in
+           -L*)
+             case " $tmp_libs " in
+             *" $deplib "*) ;;
+             *) tmp_libs="$tmp_libs $deplib" ;;
+             esac
+             ;;
+           *) tmp_libs="$tmp_libs $deplib" ;;
+           esac
+         done
+         eval $var=\"$tmp_libs\"
+       done # for var
+      fi
+      if test "$pass" = "conv" &&
+       { test "$linkmode" = "lib" || test "$linkmode" = "prog"; }; then
+       libs="$deplibs" # reset libs
+       deplibs=
+      fi
+    done # for pass
+    if test $linkmode = prog; then
+      dlfiles="$newdlfiles"
+      dlprefiles="$newdlprefiles"
+    fi
+
+    case $linkmode in
+    oldlib)
+      if test -n "$dlfiles$dlprefiles" || test "$dlself" != no; then
+       $echo "$modename: warning: \`-dlopen' is ignored for archives" 1>&2
+      fi
+
+      if test -n "$rpath"; then
+       $echo "$modename: warning: \`-rpath' is ignored for archives" 1>&2
+      fi
+
+      if test -n "$xrpath"; then
+       $echo "$modename: warning: \`-R' is ignored for archives" 1>&2
+      fi
+
+      if test -n "$vinfo"; then
+       $echo "$modename: warning: \`-version-info' is ignored for archives" 1>&2
+      fi
+
+      if test -n "$release"; then
+       $echo "$modename: warning: \`-release' is ignored for archives" 1>&2
+      fi
+
+      if test -n "$export_symbols" || test -n "$export_symbols_regex"; then
+       $echo "$modename: warning: \`-export-symbols' is ignored for archives" 1>&2
+      fi
+
+      # Now set the variables for building old libraries.
+      build_libtool_libs=no
+      oldlibs="$output"
+      objs="$objs$old_deplibs"
+      ;;
+
+    lib)
+      # Make sure we only generate libraries of the form `libNAME.la'.
+      case $outputname in
+      lib*)
+       name=`$echo "X$outputname" | $Xsed -e 's/\.la$//' -e 's/^lib//'`
+       eval libname=\"$libname_spec\"
+       ;;
+      *)
+       if test "$module" = no; then
+         $echo "$modename: libtool library \`$output' must begin with \`lib'" 1>&2
+         $echo "$help" 1>&2
+         exit 1
+       fi
+       if test "$need_lib_prefix" != no; then
+         # Add the "lib" prefix for modules if required
+         name=`$echo "X$outputname" | $Xsed -e 's/\.la$//'`
+         eval libname=\"$libname_spec\"
+       else
+         libname=`$echo "X$outputname" | $Xsed -e 's/\.la$//'`
+       fi
+       ;;
+      esac
+
+      if test -n "$objs"; then
+       if test "$deplibs_check_method" != pass_all; then
+         $echo "$modename: cannot build libtool library \`$output' from non-libtool objects on this host:$objs" 2>&1
+         exit 1
+       else
+         echo
+         echo "*** Warning: Linking the shared library $output against the non-libtool"
+         echo "*** objects $objs is not portable!"
+         libobjs="$libobjs $objs"
+       fi
+      fi
+
+      if test "$dlself" != no; then
+       $echo "$modename: warning: \`-dlopen self' is ignored for libtool libraries" 1>&2
+      fi
+
+      set dummy $rpath
+      if test $# -gt 2; then
+       $echo "$modename: warning: ignoring multiple \`-rpath's for a libtool library" 1>&2
+      fi
+      install_libdir="$2"
+
+      oldlibs=
+      if test -z "$rpath"; then
+       if test "$build_libtool_libs" = yes; then
+         # Building a libtool convenience library.
+         libext=al
+         oldlibs="$output_objdir/$libname.$libext $oldlibs"
+         build_libtool_libs=convenience
+         build_old_libs=yes
+       fi
+
+       if test -n "$vinfo"; then
+         $echo "$modename: warning: \`-version-info' is ignored for convenience libraries" 1>&2
+       fi
+
+       if test -n "$release"; then
+         $echo "$modename: warning: \`-release' is ignored for convenience libraries" 1>&2
+       fi
+      else
+
+       # Parse the version information argument.
+       save_ifs="$IFS"; IFS=':'
+       set dummy $vinfo 0 0 0
+       IFS="$save_ifs"
+
+       if test -n "$8"; then
+         $echo "$modename: too many parameters to \`-version-info'" 1>&2
+         $echo "$help" 1>&2
+         exit 1
+       fi
+
+       current="$2"
+       revision="$3"
+       age="$4"
+
+       # Check that each of the things are valid numbers.
+       case $current in
+       0 | [1-9] | [1-9][0-9] | [1-9][0-9][0-9]) ;;
+       *)
+         $echo "$modename: CURRENT \`$current' is not a nonnegative integer" 1>&2
+         $echo "$modename: \`$vinfo' is not valid version information" 1>&2
+         exit 1
+         ;;
+       esac
+
+       case $revision in
+       0 | [1-9] | [1-9][0-9] | [1-9][0-9][0-9]) ;;
+       *)
+         $echo "$modename: REVISION \`$revision' is not a nonnegative integer" 1>&2
+         $echo "$modename: \`$vinfo' is not valid version information" 1>&2
+         exit 1
+         ;;
+       esac
+
+       case $age in
+       0 | [1-9] | [1-9][0-9] | [1-9][0-9][0-9]) ;;
+       *)
+         $echo "$modename: AGE \`$age' is not a nonnegative integer" 1>&2
+         $echo "$modename: \`$vinfo' is not valid version information" 1>&2
+         exit 1
+         ;;
+       esac
+
+       if test $age -gt $current; then
+         $echo "$modename: AGE \`$age' is greater than the current interface number \`$current'" 1>&2
+         $echo "$modename: \`$vinfo' is not valid version information" 1>&2
+         exit 1
+       fi
+
+       # Calculate the version variables.
+       major=
+       versuffix=
+       verstring=
+       case $version_type in
+       none) ;;
+
+       darwin)
+         # Like Linux, but with the current version available in
+         # verstring for coding it into the library header
+         major=.`expr $current - $age`
+         versuffix="$major.$age.$revision"
+         # Darwin ld doesn't like 0 for these options...
+         minor_current=`expr $current + 1`
+         verstring="-compatibility_version $minor_current -current_version $minor_current.$revision"
+         ;;
+
+       freebsd-aout)
+         major=".$current"
+         versuffix=".$current.$revision";
+         ;;
+
+       freebsd-elf)
+         major=".$current"
+         versuffix=".$current";
+         ;;
+
+       irix)
+         major=`expr $current - $age + 1`
+         verstring="sgi$major.$revision"
+
+         # Add in all the interfaces that we are compatible with.
+         loop=$revision
+         while test $loop != 0; do
+           iface=`expr $revision - $loop`
+           loop=`expr $loop - 1`
+           verstring="sgi$major.$iface:$verstring"
+         done
+
+         # Before this point, $major must not contain `.'.
+         major=.$major
+         versuffix="$major.$revision"
+         ;;
+
+       linux)
+         major=.`expr $current - $age`
+         versuffix="$major.$age.$revision"
+         ;;
+
+       osf)
+         major=`expr $current - $age`
+         versuffix=".$current.$age.$revision"
+         verstring="$current.$age.$revision"
+
+         # Add in all the interfaces that we are compatible with.
+         loop=$age
+         while test $loop != 0; do
+           iface=`expr $current - $loop`
+           loop=`expr $loop - 1`
+           verstring="$verstring:${iface}.0"
+         done
+
+         # Make executables depend on our current version.
+         verstring="$verstring:${current}.0"
+         ;;
+
+       sunos)
+         major=".$current"
+         versuffix=".$current.$revision"
+         ;;
+
+       windows)
+         # Use '-' rather than '.', since we only want one
+         # extension on DOS 8.3 filesystems.
+         major=`expr $current - $age`
+         versuffix="-$major"
+         ;;
+
+       *)
+         $echo "$modename: unknown library version type \`$version_type'" 1>&2
+         echo "Fatal configuration error.  See the $PACKAGE docs for more information." 1>&2
+         exit 1
+         ;;
+       esac
+
+       # Clear the version info if we defaulted, and they specified a release.
+       if test -z "$vinfo" && test -n "$release"; then
+         major=
+         verstring="0.0"
+         case $version_type in
+         darwin)
+           # we can't check for "0.0" in archive_cmds due to quoting
+           # problems, so we reset it completely
+           verstring=""
+           ;;
+         *)
+           verstring="0.0"
+           ;;
+         esac
+         if test "$need_version" = no; then
+           versuffix=
+         else
+           versuffix=".0.0"
+         fi
+       fi
+
+       # Remove version info from name if versioning should be avoided
+       if test "$avoid_version" = yes && test "$need_version" = no; then
+         major=
+         versuffix=
+         verstring=""
+       fi
+
+       # Check to see if the archive will have undefined symbols.
+       if test "$allow_undefined" = yes; then
+         if test "$allow_undefined_flag" = unsupported; then
+           $echo "$modename: warning: undefined symbols not allowed in $host shared libraries" 1>&2
+           build_libtool_libs=no
+           build_old_libs=yes
+         fi
+       else
+         # Don't allow undefined symbols.
+         allow_undefined_flag="$no_undefined_flag"
+       fi
+      fi
+
+      if test "$mode" != relink; then
+       # Remove our outputs.
+       $show "${rm}r $output_objdir/$outputname $output_objdir/$libname.* $output_objdir/${libname}${release}.*"
+       $run ${rm}r $output_objdir/$outputname $output_objdir/$libname.* $output_objdir/${libname}${release}.*
+      fi
+
+      # Now set the variables for building old libraries.
+      if test "$build_old_libs" = yes && test "$build_libtool_libs" != convenience ; then
+       oldlibs="$oldlibs $output_objdir/$libname.$libext"
+
+       # Transform .lo files to .o files.
+       oldobjs="$objs "`$echo "X$libobjs" | $SP2NL | $Xsed -e '/\.'${libext}'$/d' -e "$lo2o" | $NL2SP`
+      fi
+
+      # Eliminate all temporary directories.
+      for path in $notinst_path; do
+       lib_search_path=`echo "$lib_search_path " | sed -e 's% $path % %g'`
+       deplibs=`echo "$deplibs " | sed -e 's% -L$path % %g'`
+       dependency_libs=`echo "$dependency_libs " | sed -e 's% -L$path % %g'`
+      done
+
+      if test -n "$xrpath"; then
+       # If the user specified any rpath flags, then add them.
+       temp_xrpath=
+       for libdir in $xrpath; do
+         temp_xrpath="$temp_xrpath -R$libdir"
+         case "$finalize_rpath " in
+         *" $libdir "*) ;;
+         *) finalize_rpath="$finalize_rpath $libdir" ;;
+         esac
+       done
+       if test $hardcode_into_libs != yes || test $build_old_libs = yes; then
+         dependency_libs="$temp_xrpath $dependency_libs"
+       fi
+      fi
+
+      # Make sure dlfiles contains only unique files that won't be dlpreopened
+      old_dlfiles="$dlfiles"
+      dlfiles=
+      for lib in $old_dlfiles; do
+       case " $dlprefiles $dlfiles " in
+       *" $lib "*) ;;
+       *) dlfiles="$dlfiles $lib" ;;
+       esac
+      done
+
+      # Make sure dlprefiles contains only unique files
+      old_dlprefiles="$dlprefiles"
+      dlprefiles=
+      for lib in $old_dlprefiles; do
+       case "$dlprefiles " in
+       *" $lib "*) ;;
+       *) dlprefiles="$dlprefiles $lib" ;;
+       esac
+      done
+
+      if test "$build_libtool_libs" = yes; then
+       if test -n "$rpath"; then
+         case $host in
+         *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-*-beos*)
+           # these systems don't actually have a c library (as such)!
+           ;;
+         *-*-rhapsody* | *-*-darwin1.[012])
+           # Rhapsody C library is in the System framework
+           deplibs="$deplibs -framework System"
+           ;;
+         *-*-netbsd*)
+           # Don't link with libc until the a.out ld.so is fixed.
+           ;;
+         *-*-openbsd*)
+           # Do not include libc due to us having libc/libc_r.
+           ;;
+         *)
+           # Add libc to deplibs on all other systems if necessary.
+           if test $build_libtool_need_lc = "yes"; then
+             deplibs="$deplibs -lc"
+           fi
+           ;;
+         esac
+       fi
+
+       # Transform deplibs into only deplibs that can be linked in shared.
+       name_save=$name
+       libname_save=$libname
+       release_save=$release
+       versuffix_save=$versuffix
+       major_save=$major
+       # I'm not sure if I'm treating the release correctly.  I think
+       # release should show up in the -l (ie -lgmp5) so we don't want to
+       # add it in twice.  Is that correct?
+       release=""
+       versuffix=""
+       major=""
+       newdeplibs=
+       droppeddeps=no
+       case $deplibs_check_method in
+       pass_all)
+         # Don't check for shared/static.  Everything works.
+         # This might be a little naive.  We might want to check
+         # whether the library exists or not.  But this is on
+         # osf3 & osf4 and I'm not really sure... Just
+         # implementing what was already the behaviour.
+         newdeplibs=$deplibs
+         ;;
+       test_compile)
+         # This code stresses the "libraries are programs" paradigm to its
+         # limits. Maybe even breaks it.  We compile a program, linking it
+         # against the deplibs as a proxy for the library.  Then we can check
+         # whether they linked in statically or dynamically with ldd.
+         $rm conftest.c
+         cat > conftest.c <<EOF
+         int main() { return 0; }
+EOF
+         $rm conftest
+         $CC -o conftest conftest.c $deplibs
+         if test $? -eq 0 ; then
+           ldd_output=`ldd conftest`
+           for i in $deplibs; do
+             name="`expr $i : '-l\(.*\)'`"
+             # If $name is empty we are operating on a -L argument.
+             if test -n "$name" && test "$name" != "0"; then
+               libname=`eval \\$echo \"$libname_spec\"`
+               deplib_matches=`eval \\$echo \"$library_names_spec\"`
+               set dummy $deplib_matches
+               deplib_match=$2
+               if test `expr "$ldd_output" : ".*$deplib_match"` -ne 0 ; then
+                 newdeplibs="$newdeplibs $i"
+               else
+                 droppeddeps=yes
+                 echo
+                 echo "*** Warning: This library needs some functionality provided by $i."
+                 echo "*** I have the capability to make that library automatically link in when"
+                 echo "*** you link to this library.  But I can only do this if you have a"
+                 echo "*** shared version of the library, which you do not appear to have."
+               fi
+             else
+               newdeplibs="$newdeplibs $i"
+             fi
+           done
+         else
+           # Error occured in the first compile.  Let's try to salvage the situation:
+           # Compile a seperate program for each library.
+           for i in $deplibs; do
+             name="`expr $i : '-l\(.*\)'`"
+            # If $name is empty we are operating on a -L argument.
+             if test -n "$name" && test "$name" != "0"; then
+               $rm conftest
+               $CC -o conftest conftest.c $i
+               # Did it work?
+               if test $? -eq 0 ; then
+                 ldd_output=`ldd conftest`
+                 libname=`eval \\$echo \"$libname_spec\"`
+                 deplib_matches=`eval \\$echo \"$library_names_spec\"`
+                 set dummy $deplib_matches
+                 deplib_match=$2
+                 if test `expr "$ldd_output" : ".*$deplib_match"` -ne 0 ; then
+                   newdeplibs="$newdeplibs $i"
+                 else
+                   droppeddeps=yes
+                   echo
+                   echo "*** Warning: This library needs some functionality provided by $i."
+                   echo "*** I have the capability to make that library automatically link in when"
+                   echo "*** you link to this library.  But I can only do this if you have a"
+                   echo "*** shared version of the library, which you do not appear to have."
+                 fi
+               else
+                 droppeddeps=yes
+                 echo
+                 echo "*** Warning!  Library $i is needed by this library but I was not able to"
+                 echo "***  make it link in!  You will probably need to install it or some"
+                 echo "*** library that it depends on before this library will be fully"
+                 echo "*** functional.  Installing it before continuing would be even better."
+               fi
+             else
+               newdeplibs="$newdeplibs $i"
+             fi
+           done
+         fi
+         ;;
+       file_magic*)
+         set dummy $deplibs_check_method
+         file_magic_regex=`expr "$deplibs_check_method" : "$2 \(.*\)"`
+         for a_deplib in $deplibs; do
+           name="`expr $a_deplib : '-l\(.*\)'`"
+           # If $name is empty we are operating on a -L argument.
+           if test -n "$name" && test "$name" != "0"; then
+             libname=`eval \\$echo \"$libname_spec\"`
+             for i in $lib_search_path $sys_lib_search_path $shlib_search_path; do
+                   potential_libs=`ls $i/$libname[.-]* 2>/dev/null`
+                   for potent_lib in $potential_libs; do
+                     # Follow soft links.
+                     if ls -lLd "$potent_lib" 2>/dev/null \
+                        | grep " -> " >/dev/null; then
+                       continue
+                     fi
+                     # The statement above tries to avoid entering an
+                     # endless loop below, in case of cyclic links.
+                     # We might still enter an endless loop, since a link
+                     # loop can be closed while we follow links,
+                     # but so what?
+                     potlib="$potent_lib"
+                     while test -h "$potlib" 2>/dev/null; do
+                       potliblink=`ls -ld $potlib | sed 's/.* -> //'`
+                       case $potliblink in
+                       [\\/]* | [A-Za-z]:[\\/]*) potlib="$potliblink";;
+                       *) potlib=`$echo "X$potlib" | $Xsed -e 's,[^/]*$,,'`"$potliblink";;
+                       esac
+                     done
+                     if eval $file_magic_cmd \"\$potlib\" 2>/dev/null \
+                        | sed 10q \
+                        | egrep "$file_magic_regex" > /dev/null; then
+                       newdeplibs="$newdeplibs $a_deplib"
+                       a_deplib=""
+                       break 2
+                     fi
+                   done
+             done
+             if test -n "$a_deplib" ; then
+               droppeddeps=yes
+               echo
+               echo "*** Warning: This library needs some functionality provided by $a_deplib."
+               echo "*** I have the capability to make that library automatically link in when"
+               echo "*** you link to this library.  But I can only do this if you have a"
+               echo "*** shared version of the library, which you do not appear to have."
+             fi
+           else
+             # Add a -L argument.
+             newdeplibs="$newdeplibs $a_deplib"
+           fi
+         done # Gone through all deplibs.
+         ;;
+       match_pattern*)
+         set dummy $deplibs_check_method
+         match_pattern_regex=`expr "$deplibs_check_method" : "$2 \(.*\)"`
+         for a_deplib in $deplibs; do
+           name="`expr $a_deplib : '-l\(.*\)'`"
+           # If $name is empty we are operating on a -L argument.
+           if test -n "$name" && test "$name" != "0"; then
+             libname=`eval \\$echo \"$libname_spec\"`
+             for i in $lib_search_path $sys_lib_search_path $shlib_search_path; do
+               potential_libs=`ls $i/$libname[.-]* 2>/dev/null`
+               for potent_lib in $potential_libs; do
+                 if eval echo \"$potent_lib\" 2>/dev/null \
+                     | sed 10q \
+                     | egrep "$match_pattern_regex" > /dev/null; then
+                   newdeplibs="$newdeplibs $a_deplib"
+                   a_deplib=""
+                   break 2
+                 fi
+               done
+             done
+             if test -n "$a_deplib" ; then
+               droppeddeps=yes
+               echo
+               echo "*** Warning: This library needs some functionality provided by $a_deplib."
+               echo "*** I have the capability to make that library automatically link in when"
+               echo "*** you link to this library.  But I can only do this if you have a"
+               echo "*** shared version of the library, which you do not appear to have."
+             fi
+           else
+             # Add a -L argument.
+             newdeplibs="$newdeplibs $a_deplib"
+           fi
+         done # Gone through all deplibs.
+         ;;
+       none | unknown | *)
+         newdeplibs=""
+         if $echo "X $deplibs" | $Xsed -e 's/ -lc$//' \
+              -e 's/ -[LR][^ ]*//g' -e 's/[    ]//g' |
+            grep . >/dev/null; then
+           echo
+           if test "X$deplibs_check_method" = "Xnone"; then
+             echo "*** Warning: inter-library dependencies are not supported in this platform."
+           else
+             echo "*** Warning: inter-library dependencies are not known to be supported."
+           fi
+           echo "*** All declared inter-library dependencies are being dropped."
+           droppeddeps=yes
+         fi
+         ;;
+       esac
+       versuffix=$versuffix_save
+       major=$major_save
+       release=$release_save
+       libname=$libname_save
+       name=$name_save
+
+       case $host in
+       *-*-rhapsody* | *-*-darwin1.[012])
+         # On Rhapsody replace the C library is the System framework
+         newdeplibs=`$echo "X $newdeplibs" | $Xsed -e 's/ -lc / -framework System /'`
+         ;;
+       esac
+
+       if test "$droppeddeps" = yes; then
+         if test "$module" = yes; then
+           echo
+           echo "*** Warning: libtool could not satisfy all declared inter-library"
+           echo "*** dependencies of module $libname.  Therefore, libtool will create"
+           echo "*** a static module, that should work as long as the dlopening"
+           echo "*** application is linked with the -dlopen flag."
+           if test -z "$global_symbol_pipe"; then
+             echo
+             echo "*** However, this would only work if libtool was able to extract symbol"
+             echo "*** lists from a program, using \`nm' or equivalent, but libtool could"
+             echo "*** not find such a program.  So, this module is probably useless."
+             echo "*** \`nm' from GNU binutils and a full rebuild may help."
+           fi
+           if test "$build_old_libs" = no; then
+             oldlibs="$output_objdir/$libname.$libext"
+             build_libtool_libs=module
+             build_old_libs=yes
+           else
+             build_libtool_libs=no
+           fi
+         else
+           echo "*** The inter-library dependencies that have been dropped here will be"
+           echo "*** automatically added whenever a program is linked with this library"
+           echo "*** or is declared to -dlopen it."
+
+           if test $allow_undefined = no; then
+             echo
+             echo "*** Since this library must not contain undefined symbols,"
+             echo "*** because either the platform does not support them or"
+             echo "*** it was explicitly requested with -no-undefined,"
+             echo "*** libtool will only create a static version of it."
+             if test "$build_old_libs" = no; then
+               oldlibs="$output_objdir/$libname.$libext"
+               build_libtool_libs=module
+               build_old_libs=yes
+             else
+               build_libtool_libs=no
+             fi
+           fi
+         fi
+       fi
+       # Done checking deplibs!
+       deplibs=$newdeplibs
+      fi
+
+      # All the library-specific variables (install_libdir is set above).
+      library_names=
+      old_library=
+      dlname=
+
+      # Test again, we may have decided not to build it any more
+      if test "$build_libtool_libs" = yes; then
+       if test $hardcode_into_libs = yes; then
+         # Hardcode the library paths
+         hardcode_libdirs=
+         dep_rpath=
+         rpath="$finalize_rpath"
+         test "$mode" != relink && rpath="$compile_rpath$rpath"
+         for libdir in $rpath; do
+           if test -n "$hardcode_libdir_flag_spec"; then
+             if test -n "$hardcode_libdir_separator"; then
+               if test -z "$hardcode_libdirs"; then
+                 hardcode_libdirs="$libdir"
+               else
+                 # Just accumulate the unique libdirs.
+                 case $hardcode_libdir_separator$hardcode_libdirs$hardcode_libdir_separator in
+                 *"$hardcode_libdir_separator$libdir$hardcode_libdir_separator"*)
+                   ;;
+                 *)
+                   hardcode_libdirs="$hardcode_libdirs$hardcode_libdir_separator$libdir"
+                   ;;
+                 esac
+               fi
+             else
+               eval flag=\"$hardcode_libdir_flag_spec\"
+               dep_rpath="$dep_rpath $flag"
+             fi
+           elif test -n "$runpath_var"; then
+             case "$perm_rpath " in
+             *" $libdir "*) ;;
+             *) perm_rpath="$perm_rpath $libdir" ;;
+             esac
+           fi
+         done
+         # Substitute the hardcoded libdirs into the rpath.
+         if test -n "$hardcode_libdir_separator" &&
+            test -n "$hardcode_libdirs"; then
+           libdir="$hardcode_libdirs"
+           eval dep_rpath=\"$hardcode_libdir_flag_spec\"
+         fi
+         if test -n "$runpath_var" && test -n "$perm_rpath"; then
+           # We should set the runpath_var.
+           rpath=
+           for dir in $perm_rpath; do
+             rpath="$rpath$dir:"
+           done
+           eval "$runpath_var='$rpath\$$runpath_var'; export $runpath_var"
+         fi
+         test -n "$dep_rpath" && deplibs="$dep_rpath $deplibs"
+       fi
+
+       shlibpath="$finalize_shlibpath"
+       test "$mode" != relink && shlibpath="$compile_shlibpath$shlibpath"
+       if test -n "$shlibpath"; then
+         eval "$shlibpath_var='$shlibpath\$$shlibpath_var'; export $shlibpath_var"
+       fi
+
+       # Get the real and link names of the library.
+       eval library_names=\"$library_names_spec\"
+       set dummy $library_names
+       realname="$2"
+       shift; shift
+
+       if test -n "$soname_spec"; then
+         eval soname=\"$soname_spec\"
+       else
+         soname="$realname"
+       fi
+       test -z "$dlname" && dlname=$soname
+
+       lib="$output_objdir/$realname"
+       for link
+       do
+         linknames="$linknames $link"
+       done
+
+       # Ensure that we have .o objects for linkers which dislike .lo
+       # (e.g. aix) in case we are running --disable-static
+       for obj in $libobjs; do
+         xdir=`$echo "X$obj" | $Xsed -e 's%/[^/]*$%%'`
+         if test "X$xdir" = "X$obj"; then
+           xdir="."
+         else
+           xdir="$xdir"
+         fi
+         baseobj=`$echo "X$obj" | $Xsed -e 's%^.*/%%'`
+         oldobj=`$echo "X$baseobj" | $Xsed -e "$lo2o"`
+         if test ! -f $xdir/$oldobj; then
+           $show "(cd $xdir && ${LN_S} $baseobj $oldobj)"
+           $run eval '(cd $xdir && ${LN_S} $baseobj $oldobj)' || exit $?
+         fi
+       done
+
+       # Use standard objects if they are pic
+       test -z "$pic_flag" && libobjs=`$echo "X$libobjs" | $SP2NL | $Xsed -e "$lo2o" | $NL2SP`
+
+       # Prepare the list of exported symbols
+       if test -z "$export_symbols"; then
+         if test "$always_export_symbols" = yes || test -n "$export_symbols_regex"; then
+           $show "generating symbol list for \`$libname.la'"
+           export_symbols="$output_objdir/$libname.exp"
+           $run $rm $export_symbols
+           eval cmds=\"$export_symbols_cmds\"
+           save_ifs="$IFS"; IFS='~'
+           for cmd in $cmds; do
+             IFS="$save_ifs"
+             $show "$cmd"
+             $run eval "$cmd" || exit $?
+           done
+           IFS="$save_ifs"
+           if test -n "$export_symbols_regex"; then
+             $show "egrep -e \"$export_symbols_regex\" \"$export_symbols\" > \"${export_symbols}T\""
+             $run eval 'egrep -e "$export_symbols_regex" "$export_symbols" > "${export_symbols}T"'
+             $show "$mv \"${export_symbols}T\" \"$export_symbols\""
+             $run eval '$mv "${export_symbols}T" "$export_symbols"'
+           fi
+         fi
+       fi
+
+       if test -n "$export_symbols" && test -n "$include_expsyms"; then
+         $run eval '$echo "X$include_expsyms" | $SP2NL >> "$export_symbols"'
+       fi
+
+       if test -n "$convenience"; then
+         if test -n "$whole_archive_flag_spec"; then
+           eval libobjs=\"\$libobjs $whole_archive_flag_spec\"
+         else
+           gentop="$output_objdir/${outputname}x"
+           $show "${rm}r $gentop"
+           $run ${rm}r "$gentop"
+           $show "mkdir $gentop"
+           $run mkdir "$gentop"
+           status=$?
+           if test $status -ne 0 && test ! -d "$gentop"; then
+             exit $status
+           fi
+           generated="$generated $gentop"
+
+           for xlib in $convenience; do
+             # Extract the objects.
+             case $xlib in
+             [\\/]* | [A-Za-z]:[\\/]*) xabs="$xlib" ;;
+             *) xabs=`pwd`"/$xlib" ;;
+             esac
+             xlib=`$echo "X$xlib" | $Xsed -e 's%^.*/%%'`
+             xdir="$gentop/$xlib"
+
+             $show "${rm}r $xdir"
+             $run ${rm}r "$xdir"
+             $show "mkdir $xdir"
+             $run mkdir "$xdir"
+             status=$?
+             if test $status -ne 0 && test ! -d "$xdir"; then
+               exit $status
+             fi
+             $show "(cd $xdir && $AR x $xabs)"
+             $run eval "(cd \$xdir && $AR x \$xabs)" || exit $?
+
+             libobjs="$libobjs "`find $xdir -name \*.o -print -o -name \*.lo -print | $NL2SP`
+           done
+         fi
+       fi
+
+       if test "$thread_safe" = yes && test -n "$thread_safe_flag_spec"; then
+         eval flag=\"$thread_safe_flag_spec\"
+         linker_flags="$linker_flags $flag"
+       fi
+
+       # Make a backup of the uninstalled library when relinking
+       if test "$mode" = relink; then
+         $run eval '(cd $output_objdir && $rm ${realname}U && $mv $realname ${realname}U)' || exit $?
+       fi
+
+       # Do each of the archive commands.
+       if test -n "$export_symbols" && test -n "$archive_expsym_cmds"; then
+         eval cmds=\"$archive_expsym_cmds\"
+       else
+         eval cmds=\"$archive_cmds\"
+       fi
+       save_ifs="$IFS"; IFS='~'
+       for cmd in $cmds; do
+         IFS="$save_ifs"
+         $show "$cmd"
+         $run eval "$cmd" || exit $?
+       done
+       IFS="$save_ifs"
+
+       # Restore the uninstalled library and exit
+       if test "$mode" = relink; then
+         $run eval '(cd $output_objdir && $rm ${realname}T && $mv $realname ${realname}T && $mv "$realname"U $realname)' || exit $?
+         exit 0
+       fi
+
+       # Create links to the real library.
+       for linkname in $linknames; do
+         if test "$realname" != "$linkname"; then
+           $show "(cd $output_objdir && $rm $linkname && $LN_S $realname $linkname)"
+           $run eval '(cd $output_objdir && $rm $linkname && $LN_S $realname $linkname)' || exit $?
+         fi
+       done
+
+       # If -module or -export-dynamic was specified, set the dlname.
+       if test "$module" = yes || test "$export_dynamic" = yes; then
+         # On all known operating systems, these are identical.
+         dlname="$soname"
+       fi
+      fi
+      ;;
+
+    obj)
+      if test -n "$deplibs"; then
+       $echo "$modename: warning: \`-l' and \`-L' are ignored for objects" 1>&2
+      fi
+
+      if test -n "$dlfiles$dlprefiles" || test "$dlself" != no; then
+       $echo "$modename: warning: \`-dlopen' is ignored for objects" 1>&2
+      fi
+
+      if test -n "$rpath"; then
+       $echo "$modename: warning: \`-rpath' is ignored for objects" 1>&2
+      fi
+
+      if test -n "$xrpath"; then
+       $echo "$modename: warning: \`-R' is ignored for objects" 1>&2
+      fi
+
+      if test -n "$vinfo"; then
+       $echo "$modename: warning: \`-version-info' is ignored for objects" 1>&2
+      fi
+
+      if test -n "$release"; then
+       $echo "$modename: warning: \`-release' is ignored for objects" 1>&2
+      fi
+
+      case $output in
+      *.lo)
+       if test -n "$objs$old_deplibs"; then
+         $echo "$modename: cannot build library object \`$output' from non-libtool objects" 1>&2
+         exit 1
+       fi
+       libobj="$output"
+       obj=`$echo "X$output" | $Xsed -e "$lo2o"`
+       ;;
+      *)
+       libobj=
+       obj="$output"
+       ;;
+      esac
+
+      # Delete the old objects.
+      $run $rm $obj $libobj
+
+      # Objects from convenience libraries.  This assumes
+      # single-version convenience libraries.  Whenever we create
+      # different ones for PIC/non-PIC, this we'll have to duplicate
+      # the extraction.
+      reload_conv_objs=
+      gentop=
+      # reload_cmds runs $LD directly, so let us get rid of
+      # -Wl from whole_archive_flag_spec
+      wl=
+
+      if test -n "$convenience"; then
+       if test -n "$whole_archive_flag_spec"; then
+         eval reload_conv_objs=\"\$reload_objs $whole_archive_flag_spec\"
+       else
+         gentop="$output_objdir/${obj}x"
+         $show "${rm}r $gentop"
+         $run ${rm}r "$gentop"
+         $show "mkdir $gentop"
+         $run mkdir "$gentop"
+         status=$?
+         if test $status -ne 0 && test ! -d "$gentop"; then
+           exit $status
+         fi
+         generated="$generated $gentop"
+
+         for xlib in $convenience; do
+           # Extract the objects.
+           case $xlib in
+           [\\/]* | [A-Za-z]:[\\/]*) xabs="$xlib" ;;
+           *) xabs=`pwd`"/$xlib" ;;
+           esac
+           xlib=`$echo "X$xlib" | $Xsed -e 's%^.*/%%'`
+           xdir="$gentop/$xlib"
+
+           $show "${rm}r $xdir"
+           $run ${rm}r "$xdir"
+           $show "mkdir $xdir"
+           $run mkdir "$xdir"
+           status=$?
+           if test $status -ne 0 && test ! -d "$xdir"; then
+             exit $status
+           fi
+           $show "(cd $xdir && $AR x $xabs)"
+           $run eval "(cd \$xdir && $AR x \$xabs)" || exit $?
+
+           reload_conv_objs="$reload_objs "`find $xdir -name \*.o -print -o -name \*.lo -print | $NL2SP`
+         done
+       fi
+      fi
+
+      # Create the old-style object.
+      reload_objs="$objs$old_deplibs "`$echo "X$libobjs" | $SP2NL | $Xsed -e '/\.'${libext}$'/d' -e '/\.lib$/d' -e "$lo2o" | $NL2SP`" $reload_conv_objs" ### testsuite: skip nested quoting test
+
+      output="$obj"
+      eval cmds=\"$reload_cmds\"
+      save_ifs="$IFS"; IFS='~'
+      for cmd in $cmds; do
+       IFS="$save_ifs"
+       $show "$cmd"
+       $run eval "$cmd" || exit $?
+      done
+      IFS="$save_ifs"
+
+      # Exit if we aren't doing a library object file.
+      if test -z "$libobj"; then
+       if test -n "$gentop"; then
+         $show "${rm}r $gentop"
+         $run ${rm}r $gentop
+       fi
+
+       exit 0
+      fi
+
+      if test "$build_libtool_libs" != yes; then
+       if test -n "$gentop"; then
+         $show "${rm}r $gentop"
+         $run ${rm}r $gentop
+       fi
+
+       # Create an invalid libtool object if no PIC, so that we don't
+       # accidentally link it into a program.
+       $show "echo timestamp > $libobj"
+       $run eval "echo timestamp > $libobj" || exit $?
+       exit 0
+      fi
+
+      if test -n "$pic_flag" || test "$pic_mode" != default; then
+       # Only do commands if we really have different PIC objects.
+       reload_objs="$libobjs $reload_conv_objs"
+       output="$libobj"
+       eval cmds=\"$reload_cmds\"
+       save_ifs="$IFS"; IFS='~'
+       for cmd in $cmds; do
+         IFS="$save_ifs"
+         $show "$cmd"
+         $run eval "$cmd" || exit $?
+       done
+       IFS="$save_ifs"
+      else
+       # Just create a symlink.
+       $show $rm $libobj
+       $run $rm $libobj
+       xdir=`$echo "X$libobj" | $Xsed -e 's%/[^/]*$%%'`
+       if test "X$xdir" = "X$libobj"; then
+         xdir="."
+       else
+         xdir="$xdir"
+       fi
+       baseobj=`$echo "X$libobj" | $Xsed -e 's%^.*/%%'`
+       oldobj=`$echo "X$baseobj" | $Xsed -e "$lo2o"`
+       $show "(cd $xdir && $LN_S $oldobj $baseobj)"
+       $run eval '(cd $xdir && $LN_S $oldobj $baseobj)' || exit $?
+      fi
+
+      if test -n "$gentop"; then
+       $show "${rm}r $gentop"
+       $run ${rm}r $gentop
+      fi
+
+      exit 0
+      ;;
+
+    prog)
+      case $host in
+       *cygwin*) output=`echo $output | sed -e 's,.exe$,,;s,$,.exe,'` ;;
+      esac
+      if test -n "$vinfo"; then
+       $echo "$modename: warning: \`-version-info' is ignored for programs" 1>&2
+      fi
+
+      if test -n "$release"; then
+       $echo "$modename: warning: \`-release' is ignored for programs" 1>&2
+      fi
+
+      if test "$preload" = yes; then
+       if test "$dlopen_support" = unknown && test "$dlopen_self" = unknown &&
+          test "$dlopen_self_static" = unknown; then
+         $echo "$modename: warning: \`AC_LIBTOOL_DLOPEN' not used. Assuming no dlopen support."
+       fi
+      fi
+
+      case $host in
+      *-*-rhapsody* | *-*-darwin1.[012])
+       # On Rhapsody replace the C library is the System framework
+       compile_deplibs=`$echo "X $compile_deplibs" | $Xsed -e 's/ -lc / -framework System /'`
+       finalize_deplibs=`$echo "X $finalize_deplibs" | $Xsed -e 's/ -lc / -framework System /'`
+       ;;
+      esac
+
+      compile_command="$compile_command $compile_deplibs"
+      finalize_command="$finalize_command $finalize_deplibs"
+
+      if test -n "$rpath$xrpath"; then
+       # If the user specified any rpath flags, then add them.
+       for libdir in $rpath $xrpath; do
+         # This is the magic to use -rpath.
+         case "$finalize_rpath " in
+         *" $libdir "*) ;;
+         *) finalize_rpath="$finalize_rpath $libdir" ;;
+         esac
+       done
+      fi
+
+      # Now hardcode the library paths
+      rpath=
+      hardcode_libdirs=
+      for libdir in $compile_rpath $finalize_rpath; do
+       if test -n "$hardcode_libdir_flag_spec"; then
+         if test -n "$hardcode_libdir_separator"; then
+           if test -z "$hardcode_libdirs"; then
+             hardcode_libdirs="$libdir"
+           else
+             # Just accumulate the unique libdirs.
+             case $hardcode_libdir_separator$hardcode_libdirs$hardcode_libdir_separator in
+             *"$hardcode_libdir_separator$libdir$hardcode_libdir_separator"*)
+               ;;
+             *)
+               hardcode_libdirs="$hardcode_libdirs$hardcode_libdir_separator$libdir"
+               ;;
+             esac
+           fi
+         else
+           eval flag=\"$hardcode_libdir_flag_spec\"
+           rpath="$rpath $flag"
+         fi
+       elif test -n "$runpath_var"; then
+         case "$perm_rpath " in
+         *" $libdir "*) ;;
+         *) perm_rpath="$perm_rpath $libdir" ;;
+         esac
+       fi
+       case $host in
+       *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2*)
+         case :$dllsearchpath: in
+         *":$libdir:"*) ;;
+         *) dllsearchpath="$dllsearchpath:$libdir";;
+         esac
+         ;;
+       esac
+      done
+      # Substitute the hardcoded libdirs into the rpath.
+      if test -n "$hardcode_libdir_separator" &&
+        test -n "$hardcode_libdirs"; then
+       libdir="$hardcode_libdirs"
+       eval rpath=\" $hardcode_libdir_flag_spec\"
+      fi
+      compile_rpath="$rpath"
+
+      rpath=
+      hardcode_libdirs=
+      for libdir in $finalize_rpath; do
+       if test -n "$hardcode_libdir_flag_spec"; then
+         if test -n "$hardcode_libdir_separator"; then
+           if test -z "$hardcode_libdirs"; then
+             hardcode_libdirs="$libdir"
+           else
+             # Just accumulate the unique libdirs.
+             case $hardcode_libdir_separator$hardcode_libdirs$hardcode_libdir_separator in
+             *"$hardcode_libdir_separator$libdir$hardcode_libdir_separator"*)
+               ;;
+             *)
+               hardcode_libdirs="$hardcode_libdirs$hardcode_libdir_separator$libdir"
+               ;;
+             esac
+           fi
+         else
+           eval flag=\"$hardcode_libdir_flag_spec\"
+           rpath="$rpath $flag"
+         fi
+       elif test -n "$runpath_var"; then
+         case "$finalize_perm_rpath " in
+         *" $libdir "*) ;;
+         *) finalize_perm_rpath="$finalize_perm_rpath $libdir" ;;
+         esac
+       fi
+      done
+      # Substitute the hardcoded libdirs into the rpath.
+      if test -n "$hardcode_libdir_separator" &&
+        test -n "$hardcode_libdirs"; then
+       libdir="$hardcode_libdirs"
+       eval rpath=\" $hardcode_libdir_flag_spec\"
+      fi
+      finalize_rpath="$rpath"
+
+      if test -n "$libobjs" && test "$build_old_libs" = yes; then
+       # Transform all the library objects into standard objects.
+       compile_command=`$echo "X$compile_command" | $SP2NL | $Xsed -e "$lo2o" | $NL2SP`
+       finalize_command=`$echo "X$finalize_command" | $SP2NL | $Xsed -e "$lo2o" | $NL2SP`
+      fi
+
+      dlsyms=
+      if test -n "$dlfiles$dlprefiles" || test "$dlself" != no; then
+       if test -n "$NM" && test -n "$global_symbol_pipe"; then
+         dlsyms="${outputname}S.c"
+       else
+         $echo "$modename: not configured to extract global symbols from dlpreopened files" 1>&2
+       fi
+      fi
+
+      if test -n "$dlsyms"; then
+       case $dlsyms in
+       "") ;;
+       *.c)
+         # Discover the nlist of each of the dlfiles.
+         nlist="$output_objdir/${outputname}.nm"
+
+         $show "$rm $nlist ${nlist}S ${nlist}T"
+         $run $rm "$nlist" "${nlist}S" "${nlist}T"
+
+         # Parse the name list into a source file.
+         $show "creating $output_objdir/$dlsyms"
+
+         test -z "$run" && $echo > "$output_objdir/$dlsyms" "\
+/* $dlsyms - symbol resolution table for \`$outputname' dlsym emulation. */
+/* Generated by $PROGRAM - GNU $PACKAGE $VERSION$TIMESTAMP */
+
+#ifdef __cplusplus
+extern \"C\" {
+#endif
+
+/* Prevent the only kind of declaration conflicts we can make. */
+#define lt_preloaded_symbols some_other_symbol
+
+/* External symbol declarations for the compiler. */\
+"
+
+         if test "$dlself" = yes; then
+           $show "generating symbol list for \`$output'"
+
+           test -z "$run" && $echo ': @PROGRAM@ ' > "$nlist"
+
+           # Add our own program objects to the symbol list.
+           progfiles=`$echo "X$objs$old_deplibs" | $SP2NL | $Xsed -e "$lo2o" | $NL2SP`
+           for arg in $progfiles; do
+             $show "extracting global C symbols from \`$arg'"
+             $run eval "$NM $arg | $global_symbol_pipe >> '$nlist'"
+           done
+
+           if test -n "$exclude_expsyms"; then
+             $run eval 'egrep -v " ($exclude_expsyms)$" "$nlist" > "$nlist"T'
+             $run eval '$mv "$nlist"T "$nlist"'
+           fi
+
+           if test -n "$export_symbols_regex"; then
+             $run eval 'egrep -e "$export_symbols_regex" "$nlist" > "$nlist"T'
+             $run eval '$mv "$nlist"T "$nlist"'
+           fi
+
+           # Prepare the list of exported symbols
+           if test -z "$export_symbols"; then
+             export_symbols="$output_objdir/$output.exp"
+             $run $rm $export_symbols
+             $run eval "sed -n -e '/^: @PROGRAM@$/d' -e 's/^.* \(.*\)$/\1/p' "'< "$nlist" > "$export_symbols"'
+           else
+             $run eval "sed -e 's/\([][.*^$]\)/\\\1/g' -e 's/^/ /' -e 's/$/$/'"' < "$export_symbols" > "$output_objdir/$output.exp"'
+             $run eval 'grep -f "$output_objdir/$output.exp" < "$nlist" > "$nlist"T'
+             $run eval 'mv "$nlist"T "$nlist"'
+           fi
+         fi
+
+         for arg in $dlprefiles; do
+           $show "extracting global C symbols from \`$arg'"
+           name=`echo "$arg" | sed -e 's%^.*/%%'`
+           $run eval 'echo ": $name " >> "$nlist"'
+           $run eval "$NM $arg | $global_symbol_pipe >> '$nlist'"
+         done
+
+         if test -z "$run"; then
+           # Make sure we have at least an empty file.
+           test -f "$nlist" || : > "$nlist"
+
+           if test -n "$exclude_expsyms"; then
+             egrep -v " ($exclude_expsyms)$" "$nlist" > "$nlist"T
+             $mv "$nlist"T "$nlist"
+           fi
+
+           # Try sorting and uniquifying the output.
+           if grep -v "^: " < "$nlist" | sort +2 | uniq > "$nlist"S; then
+             :
+           else
+             grep -v "^: " < "$nlist" > "$nlist"S
+           fi
+
+           if test -f "$nlist"S; then
+             eval "$global_symbol_to_cdecl"' < "$nlist"S >> "$output_objdir/$dlsyms"'
+           else
+             echo '/* NONE */' >> "$output_objdir/$dlsyms"
+           fi
+
+           $echo >> "$output_objdir/$dlsyms" "\
+
+#undef lt_preloaded_symbols
+
+#if defined (__STDC__) && __STDC__
+# define lt_ptr void *
+#else
+# define lt_ptr char *
+# define const
+#endif
+
+/* The mapping between symbol names and symbols. */
+const struct {
+  const char *name;
+  lt_ptr address;
+}
+lt_preloaded_symbols[] =
+{\
+"
+
+           eval "$global_symbol_to_c_name_address" < "$nlist" >> "$output_objdir/$dlsyms"
+
+           $echo >> "$output_objdir/$dlsyms" "\
+  {0, (lt_ptr) 0}
+};
+
+/* This works around a problem in FreeBSD linker */
+#ifdef FREEBSD_WORKAROUND
+static const void *lt_preloaded_setup() {
+  return lt_preloaded_symbols;
+}
+#endif
+
+#ifdef __cplusplus
+}
+#endif\
+"
+         fi
+
+         pic_flag_for_symtable=
+         case $host in
+         # compiling the symbol table file with pic_flag works around
+         # a FreeBSD bug that causes programs to crash when -lm is
+         # linked before any other PIC object.  But we must not use
+         # pic_flag when linking with -static.  The problem exists in
+         # FreeBSD 2.2.6 and is fixed in FreeBSD 3.1.
+         *-*-freebsd2*|*-*-freebsd3.0*|*-*-freebsdelf3.0*)
+           case "$compile_command " in
+           *" -static "*) ;;
+           *) pic_flag_for_symtable=" $pic_flag -DPIC -DFREEBSD_WORKAROUND";;
+           esac;;
+         *-*-hpux*)
+           case "$compile_command " in
+           *" -static "*) ;;
+           *) pic_flag_for_symtable=" $pic_flag -DPIC";;
+           esac
+         esac
+
+         # Now compile the dynamic symbol file.
+         $show "(cd $output_objdir && $CC -c$no_builtin_flag$pic_flag_for_symtable \"$dlsyms\")"
+         $run eval '(cd $output_objdir && $CC -c$no_builtin_flag$pic_flag_for_symtable "$dlsyms")' || exit $?
+
+         # Clean up the generated files.
+         $show "$rm $output_objdir/$dlsyms $nlist ${nlist}S ${nlist}T"
+         $run $rm "$output_objdir/$dlsyms" "$nlist" "${nlist}S" "${nlist}T"
+
+         # Transform the symbol file into the correct name.
+         compile_command=`$echo "X$compile_command" | $Xsed -e "s%@SYMFILE@%$output_objdir/${outputname}S.${objext}%"`
+         finalize_command=`$echo "X$finalize_command" | $Xsed -e "s%@SYMFILE@%$output_objdir/${outputname}S.${objext}%"`
+         ;;
+       *)
+         $echo "$modename: unknown suffix for \`$dlsyms'" 1>&2
+         exit 1
+         ;;
+       esac
+      else
+       # We keep going just in case the user didn't refer to
+       # lt_preloaded_symbols.  The linker will fail if global_symbol_pipe
+       # really was required.
+
+       # Nullify the symbol file.
+       compile_command=`$echo "X$compile_command" | $Xsed -e "s% @SYMFILE@%%"`
+       finalize_command=`$echo "X$finalize_command" | $Xsed -e "s% @SYMFILE@%%"`
+      fi
+
+      if test $need_relink = no || test "$build_libtool_libs" != yes; then
+       # Replace the output file specification.
+       compile_command=`$echo "X$compile_command" | $Xsed -e 's%@OUTPUT@%'"$output"'%g'`
+       link_command="$compile_command$compile_rpath"
+
+       # We have no uninstalled library dependencies, so finalize right now.
+       $show "$link_command"
+       $run eval "$link_command"
+       status=$?
+
+       # Delete the generated files.
+       if test -n "$dlsyms"; then
+         $show "$rm $output_objdir/${outputname}S.${objext}"
+         $run $rm "$output_objdir/${outputname}S.${objext}"
+       fi
+
+       exit $status
+      fi
+
+      if test -n "$shlibpath_var"; then
+       # We should set the shlibpath_var
+       rpath=
+       for dir in $temp_rpath; do
+         case $dir in
+         [\\/]* | [A-Za-z]:[\\/]*)
+           # Absolute path.
+           rpath="$rpath$dir:"
+           ;;
+         *)
+           # Relative path: add a thisdir entry.
+           rpath="$rpath\$thisdir/$dir:"
+           ;;
+         esac
+       done
+       temp_rpath="$rpath"
+      fi
+
+      if test -n "$compile_shlibpath$finalize_shlibpath"; then
+       compile_command="$shlibpath_var=\"$compile_shlibpath$finalize_shlibpath\$$shlibpath_var\" $compile_command"
+      fi
+      if test -n "$finalize_shlibpath"; then
+       finalize_command="$shlibpath_var=\"$finalize_shlibpath\$$shlibpath_var\" $finalize_command"
+      fi
+
+      compile_var=
+      finalize_var=
+      if test -n "$runpath_var"; then
+       if test -n "$perm_rpath"; then
+         # We should set the runpath_var.
+         rpath=
+         for dir in $perm_rpath; do
+           rpath="$rpath$dir:"
+         done
+         compile_var="$runpath_var=\"$rpath\$$runpath_var\" "
+       fi
+       if test -n "$finalize_perm_rpath"; then
+         # We should set the runpath_var.
+         rpath=
+         for dir in $finalize_perm_rpath; do
+           rpath="$rpath$dir:"
+         done
+         finalize_var="$runpath_var=\"$rpath\$$runpath_var\" "
+       fi
+      fi
+
+      if test "$no_install" = yes; then
+       # We don't need to create a wrapper script.
+       link_command="$compile_var$compile_command$compile_rpath"
+       # Replace the output file specification.
+       link_command=`$echo "X$link_command" | $Xsed -e 's%@OUTPUT@%'"$output"'%g'`
+       # Delete the old output file.
+       $run $rm $output
+       # Link the executable and exit
+       $show "$link_command"
+       $run eval "$link_command" || exit $?
+       exit 0
+      fi
+
+      if test "$hardcode_action" = relink; then
+       # Fast installation is not supported
+       link_command="$compile_var$compile_command$compile_rpath"
+       relink_command="$finalize_var$finalize_command$finalize_rpath"
+
+       $echo "$modename: warning: this platform does not like uninstalled shared libraries" 1>&2
+       $echo "$modename: \`$output' will be relinked during installation" 1>&2
+      else
+       if test "$fast_install" != no; then
+         link_command="$finalize_var$compile_command$finalize_rpath"
+         if test "$fast_install" = yes; then
+           relink_command=`$echo "X$compile_var$compile_command$compile_rpath" | $Xsed -e 's%@OUTPUT@%\$progdir/\$file%g'`
+         else
+           # fast_install is set to needless
+           relink_command=
+         fi
+       else
+         link_command="$compile_var$compile_command$compile_rpath"
+         relink_command="$finalize_var$finalize_command$finalize_rpath"
+       fi
+      fi
+
+      # Replace the output file specification.
+      link_command=`$echo "X$link_command" | $Xsed -e 's%@OUTPUT@%'"$output_objdir/$outputname"'%g'`
+
+      # Delete the old output files.
+      $run $rm $output $output_objdir/$outputname $output_objdir/lt-$outputname
+
+      $show "$link_command"
+      $run eval "$link_command" || exit $?
+
+      # Now create the wrapper script.
+      $show "creating $output"
+
+      # Quote the relink command for shipping.
+      if test -n "$relink_command"; then
+       # Preserve any variables that may affect compiler behavior
+       for var in $variables_saved_for_relink; do
+         if eval test -z \"\${$var+set}\"; then
+           relink_command="{ test -z \"\${$var+set}\" || unset $var || { $var=; export $var; }; }; $relink_command"
+         elif eval var_value=\$$var; test -z "$var_value"; then
+           relink_command="$var=; export $var; $relink_command"
+         else
+           var_value=`$echo "X$var_value" | $Xsed -e "$sed_quote_subst"`
+           relink_command="$var=\"$var_value\"; export $var; $relink_command"
+         fi
+       done
+       relink_command="cd `pwd`; $relink_command"
+       relink_command=`$echo "X$relink_command" | $Xsed -e "$sed_quote_subst"`
+      fi
+
+      # Quote $echo for shipping.
+      if test "X$echo" = "X$SHELL $0 --fallback-echo"; then
+       case $0 in
+       [\\/]* | [A-Za-z]:[\\/]*) qecho="$SHELL $0 --fallback-echo";;
+       *) qecho="$SHELL `pwd`/$0 --fallback-echo";;
+       esac
+       qecho=`$echo "X$qecho" | $Xsed -e "$sed_quote_subst"`
+      else
+       qecho=`$echo "X$echo" | $Xsed -e "$sed_quote_subst"`
+      fi
+
+      # Only actually do things if our run command is non-null.
+      if test -z "$run"; then
+       # win32 will think the script is a binary if it has
+       # a .exe suffix, so we strip it off here.
+       case $output in
+         *.exe) output=`echo $output|sed 's,.exe$,,'` ;;
+       esac
+       # test for cygwin because mv fails w/o .exe extensions
+       case $host in
+         *cygwin*) exeext=.exe ;;
+         *) exeext= ;;
+       esac
+       $rm $output
+       trap "$rm $output; exit 1" 1 2 15
+
+       $echo > $output "\
+#! $SHELL
+
+# $output - temporary wrapper script for $objdir/$outputname
+# Generated by $PROGRAM - GNU $PACKAGE $VERSION$TIMESTAMP
+#
+# The $output program cannot be directly executed until all the libtool
+# libraries that it depends on are installed.
+#
+# This wrapper script should never be moved out of the build directory.
+# If it is, it will not operate correctly.
+
+# Sed substitution that helps us do robust quoting.  It backslashifies
+# metacharacters that are still active within double-quoted strings.
+Xsed='sed -e 1s/^X//'
+sed_quote_subst='$sed_quote_subst'
+
+# The HP-UX ksh and POSIX shell print the target directory to stdout
+# if CDPATH is set.
+if test \"\${CDPATH+set}\" = set; then CDPATH=:; export CDPATH; fi
+
+relink_command=\"$relink_command\"
+
+# This environment variable determines our operation mode.
+if test \"\$libtool_install_magic\" = \"$magic\"; then
+  # install mode needs the following variable:
+  notinst_deplibs='$notinst_deplibs'
+else
+  # When we are sourced in execute mode, \$file and \$echo are already set.
+  if test \"\$libtool_execute_magic\" != \"$magic\"; then
+    echo=\"$qecho\"
+    file=\"\$0\"
+    # Make sure echo works.
+    if test \"X\$1\" = X--no-reexec; then
+      # Discard the --no-reexec flag, and continue.
+      shift
+    elif test \"X\`(\$echo '\t') 2>/dev/null\`\" = 'X\t'; then
+      # Yippee, \$echo works!
+      :
+    else
+      # Restart under the correct shell, and then maybe \$echo will work.
+      exec $SHELL \"\$0\" --no-reexec \${1+\"\$@\"}
+    fi
+  fi\
+"
+       $echo >> $output "\
+
+  # Find the directory that this script lives in.
+  thisdir=\`\$echo \"X\$file\" | \$Xsed -e 's%/[^/]*$%%'\`
+  test \"x\$thisdir\" = \"x\$file\" && thisdir=.
+
+  # Follow symbolic links until we get to the real thisdir.
+  file=\`ls -ld \"\$file\" | sed -n 's/.*-> //p'\`
+  while test -n \"\$file\"; do
+    destdir=\`\$echo \"X\$file\" | \$Xsed -e 's%/[^/]*\$%%'\`
+
+    # If there was a directory component, then change thisdir.
+    if test \"x\$destdir\" != \"x\$file\"; then
+      case \"\$destdir\" in
+      [\\\\/]* | [A-Za-z]:[\\\\/]*) thisdir=\"\$destdir\" ;;
+      *) thisdir=\"\$thisdir/\$destdir\" ;;
+      esac
+    fi
+
+    file=\`\$echo \"X\$file\" | \$Xsed -e 's%^.*/%%'\`
+    file=\`ls -ld \"\$thisdir/\$file\" | sed -n 's/.*-> //p'\`
+  done
+
+  # Try to get the absolute directory name.
+  absdir=\`cd \"\$thisdir\" && pwd\`
+  test -n \"\$absdir\" && thisdir=\"\$absdir\"
+"
+
+       if test "$fast_install" = yes; then
+         echo >> $output "\
+  program=lt-'$outputname'$exeext
+  progdir=\"\$thisdir/$objdir\"
+
+  if test ! -f \"\$progdir/\$program\" || \\
+     { file=\`ls -1dt \"\$progdir/\$program\" \"\$progdir/../\$program\" 2>/dev/null | sed 1q\`; \\
+       test \"X\$file\" != \"X\$progdir/\$program\"; }; then
+
+    file=\"\$\$-\$program\"
+
+    if test ! -d \"\$progdir\"; then
+      $mkdir \"\$progdir\"
+    else
+      $rm \"\$progdir/\$file\"
+    fi"
+
+         echo >> $output "\
+
+    # relink executable if necessary
+    if test -n \"\$relink_command\"; then
+      if relink_command_output=\`eval \$relink_command 2>&1\`; then :
+      else
+       $echo \"\$relink_command_output\" >&2
+       $rm \"\$progdir/\$file\"
+       exit 1
+      fi
+    fi
+
+    $mv \"\$progdir/\$file\" \"\$progdir/\$program\" 2>/dev/null ||
+    { $rm \"\$progdir/\$program\";
+      $mv \"\$progdir/\$file\" \"\$progdir/\$program\"; }
+    $rm \"\$progdir/\$file\"
+  fi"
+       else
+         echo >> $output "\
+  program='$outputname'
+  progdir=\"\$thisdir/$objdir\"
+"
+       fi
+
+       echo >> $output "\
+
+  if test -f \"\$progdir/\$program\"; then"
+
+       # Export our shlibpath_var if we have one.
+       if test "$shlibpath_overrides_runpath" = yes && test -n "$shlibpath_var" && test -n "$temp_rpath"; then
+         $echo >> $output "\
+    # Add our own library path to $shlibpath_var
+    $shlibpath_var=\"$temp_rpath\$$shlibpath_var\"
+
+    # Some systems cannot cope with colon-terminated $shlibpath_var
+    # The second colon is a workaround for a bug in BeOS R4 sed
+    $shlibpath_var=\`\$echo \"X\$$shlibpath_var\" | \$Xsed -e 's/::*\$//'\`
+
+    export $shlibpath_var
+"
+       fi
+
+       # fixup the dll searchpath if we need to.
+       if test -n "$dllsearchpath"; then
+         $echo >> $output "\
+    # Add the dll search path components to the executable PATH
+    PATH=$dllsearchpath:\$PATH
+"
+       fi
+
+       $echo >> $output "\
+    if test \"\$libtool_execute_magic\" != \"$magic\"; then
+      # Run the actual program with our arguments.
+"
+       case $host in
+       # win32 systems need to use the prog path for dll
+       # lookup to work
+       *-*-cygwin* | *-*-pw32*)
+         $echo >> $output "\
+      exec \$progdir/\$program \${1+\"\$@\"}
+"
+         ;;
+
+       # Backslashes separate directories on plain windows
+       *-*-mingw | *-*-os2*)
+         $echo >> $output "\
+      exec \$progdir\\\\\$program \${1+\"\$@\"}
+"
+         ;;
+
+       *)
+         $echo >> $output "\
+      # Export the path to the program.
+      PATH=\"\$progdir:\$PATH\"
+      export PATH
+
+      exec \$program \${1+\"\$@\"}
+"
+         ;;
+       esac
+       $echo >> $output "\
+      \$echo \"\$0: cannot exec \$program \${1+\"\$@\"}\"
+      exit 1
+    fi
+  else
+    # The program doesn't exist.
+    \$echo \"\$0: error: \$progdir/\$program does not exist\" 1>&2
+    \$echo \"This script is just a wrapper for \$program.\" 1>&2
+    echo \"See the $PACKAGE documentation for more information.\" 1>&2
+    exit 1
+  fi
+fi\
+"
+       chmod +x $output
+      fi
+      exit 0
+      ;;
+    esac
+
+    # See if we need to build an old-fashioned archive.
+    for oldlib in $oldlibs; do
+
+      if test "$build_libtool_libs" = convenience; then
+       oldobjs="$libobjs_save"
+       addlibs="$convenience"
+       build_libtool_libs=no
+      else
+       if test "$build_libtool_libs" = module; then
+         oldobjs="$libobjs_save"
+         build_libtool_libs=no
+       else
+         oldobjs="$objs$old_deplibs "`$echo "X$libobjs_save" | $SP2NL | $Xsed -e '/\.'${libext}'$/d' -e '/\.lib$/d' -e "$lo2o" | $NL2SP`
+       fi
+       addlibs="$old_convenience"
+      fi
+
+      if test -n "$addlibs"; then
+       gentop="$output_objdir/${outputname}x"
+       $show "${rm}r $gentop"
+       $run ${rm}r "$gentop"
+       $show "mkdir $gentop"
+       $run mkdir "$gentop"
+       status=$?
+       if test $status -ne 0 && test ! -d "$gentop"; then
+         exit $status
+       fi
+       generated="$generated $gentop"
+
+       # Add in members from convenience archives.
+       for xlib in $addlibs; do
+         # Extract the objects.
+         case $xlib in
+         [\\/]* | [A-Za-z]:[\\/]*) xabs="$xlib" ;;
+         *) xabs=`pwd`"/$xlib" ;;
+         esac
+         xlib=`$echo "X$xlib" | $Xsed -e 's%^.*/%%'`
+         xdir="$gentop/$xlib"
+
+         $show "${rm}r $xdir"
+         $run ${rm}r "$xdir"
+         $show "mkdir $xdir"
+         $run mkdir "$xdir"
+         status=$?
+         if test $status -ne 0 && test ! -d "$xdir"; then
+           exit $status
+         fi
+         $show "(cd $xdir && $AR x $xabs)"
+         $run eval "(cd \$xdir && $AR x \$xabs)" || exit $?
+
+         oldobjs="$oldobjs "`find $xdir -name \*.${objext} -print -o -name \*.lo -print | $NL2SP`
+       done
+      fi
+
+      # Do each command in the archive commands.
+      if test -n "$old_archive_from_new_cmds" && test "$build_libtool_libs" = yes; then
+       eval cmds=\"$old_archive_from_new_cmds\"
+      else
+       # Ensure that we have .o objects in place in case we decided
+       # not to build a shared library, and have fallen back to building
+       # static libs even though --disable-static was passed!
+       for oldobj in $oldobjs; do
+         if test ! -f $oldobj; then
+           xdir=`$echo "X$oldobj" | $Xsed -e 's%/[^/]*$%%'`
+           if test "X$xdir" = "X$oldobj"; then
+             xdir="."
+           else
+             xdir="$xdir"
+           fi
+           baseobj=`$echo "X$oldobj" | $Xsed -e 's%^.*/%%'`
+           obj=`$echo "X$baseobj" | $Xsed -e "$o2lo"`
+           $show "(cd $xdir && ${LN_S} $obj $baseobj)"
+           $run eval '(cd $xdir && ${LN_S} $obj $baseobj)' || exit $?
+         fi
+       done
+
+       eval cmds=\"$old_archive_cmds\"
+      fi
+      save_ifs="$IFS"; IFS='~'
+      for cmd in $cmds; do
+       IFS="$save_ifs"
+       $show "$cmd"
+       $run eval "$cmd" || exit $?
+      done
+      IFS="$save_ifs"
+    done
+
+    if test -n "$generated"; then
+      $show "${rm}r$generated"
+      $run ${rm}r$generated
+    fi
+
+    # Now create the libtool archive.
+    case $output in
+    *.la)
+      old_library=
+      test "$build_old_libs" = yes && old_library="$libname.$libext"
+      $show "creating $output"
+
+      # Preserve any variables that may affect compiler behavior
+      for var in $variables_saved_for_relink; do
+       if eval test -z \"\${$var+set}\"; then
+         relink_command="{ test -z \"\${$var+set}\" || unset $var || { $var=; export $var; }; }; $relink_command"
+       elif eval var_value=\$$var; test -z "$var_value"; then
+         relink_command="$var=; export $var; $relink_command"
+       else
+         var_value=`$echo "X$var_value" | $Xsed -e "$sed_quote_subst"`
+         relink_command="$var=\"$var_value\"; export $var; $relink_command"
+       fi
+      done
+      # Quote the link command for shipping.
+      relink_command="cd `pwd`; $SHELL $0 --mode=relink $libtool_args"
+      relink_command=`$echo "X$relink_command" | $Xsed -e "$sed_quote_subst"`
+
+      # Only create the output if not a dry run.
+      if test -z "$run"; then
+       for installed in no yes; do
+         if test "$installed" = yes; then
+           if test -z "$install_libdir"; then
+             break
+           fi
+           output="$output_objdir/$outputname"i
+           # Replace all uninstalled libtool libraries with the installed ones
+           newdependency_libs=
+           for deplib in $dependency_libs; do
+             case $deplib in
+             *.la)
+               name=`$echo "X$deplib" | $Xsed -e 's%^.*/%%'`
+               eval libdir=`sed -n -e 's/^libdir=\(.*\)$/\1/p' $deplib`
+               if test -z "$libdir"; then
+                 $echo "$modename: \`$deplib' is not a valid libtool archive" 1>&2
+                 exit 1
+               fi
+               newdependency_libs="$newdependency_libs $libdir/$name"
+               ;;
+             *) newdependency_libs="$newdependency_libs $deplib" ;;
+             esac
+           done
+           dependency_libs="$newdependency_libs"
+           newdlfiles=
+           for lib in $dlfiles; do
+             name=`$echo "X$lib" | $Xsed -e 's%^.*/%%'`
+             eval libdir=`sed -n -e 's/^libdir=\(.*\)$/\1/p' $lib`
+             if test -z "$libdir"; then
+               $echo "$modename: \`$lib' is not a valid libtool archive" 1>&2
+               exit 1
+             fi
+             newdlfiles="$newdlfiles $libdir/$name"
+           done
+           dlfiles="$newdlfiles"
+           newdlprefiles=
+           for lib in $dlprefiles; do
+             name=`$echo "X$lib" | $Xsed -e 's%^.*/%%'`
+             eval libdir=`sed -n -e 's/^libdir=\(.*\)$/\1/p' $lib`
+             if test -z "$libdir"; then
+               $echo "$modename: \`$lib' is not a valid libtool archive" 1>&2
+               exit 1
+             fi
+             newdlprefiles="$newdlprefiles $libdir/$name"
+           done
+           dlprefiles="$newdlprefiles"
+         fi
+         $rm $output
+         # place dlname in correct position for cygwin
+         tdlname=$dlname
+         case $host,$output,$installed,$module,$dlname in
+           *cygwin*,*lai,yes,no,*.dll) tdlname=../bin/$dlname ;;
+         esac
+         $echo > $output "\
+# $outputname - a libtool library file
+# Generated by $PROGRAM - GNU $PACKAGE $VERSION$TIMESTAMP
+#
+# Please DO NOT delete this file!
+# It is necessary for linking the library.
+
+# The name that we can dlopen(3).
+dlname='$tdlname'
+
+# Names of this library.
+library_names='$library_names'
+
+# The name of the static archive.
+old_library='$old_library'
+
+# Libraries that this one depends upon.
+dependency_libs='$dependency_libs'
+
+# Version information for $libname.
+current=$current
+age=$age
+revision=$revision
+
+# Is this an already installed library?
+installed=$installed
+
+# Files to dlopen/dlpreopen
+dlopen='$dlfiles'
+dlpreopen='$dlprefiles'
+
+# Directory that this library needs to be installed in:
+libdir='$install_libdir'"
+         if test "$installed" = no && test $need_relink = yes; then
+           $echo >> $output "\
+relink_command=\"$relink_command\""
+         fi
+       done
+      fi
+
+      # Do a symbolic link so that the libtool archive can be found in
+      # LD_LIBRARY_PATH before the program is installed.
+      $show "(cd $output_objdir && $rm $outputname && $LN_S ../$outputname $outputname)"
+      $run eval '(cd $output_objdir && $rm $outputname && $LN_S ../$outputname $outputname)' || exit $?
+      ;;
+    esac
+    exit 0
+    ;;
+
+  # libtool install mode
+  install)
+    modename="$modename: install"
+
+    # There may be an optional sh(1) argument at the beginning of
+    # install_prog (especially on Windows NT).
+    if test "$nonopt" = "$SHELL" || test "$nonopt" = /bin/sh ||
+       # Allow the use of GNU shtool's install command.
+       $echo "X$nonopt" | $Xsed | grep shtool > /dev/null; then
+      # Aesthetically quote it.
+      arg=`$echo "X$nonopt" | $Xsed -e "$sed_quote_subst"`
+      case $arg in
+      *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \    ]*|*]*)
+       arg="\"$arg\""
+       ;;
+      esac
+      install_prog="$arg "
+      arg="$1"
+      shift
+    else
+      install_prog=
+      arg="$nonopt"
+    fi
+
+    # The real first argument should be the name of the installation program.
+    # Aesthetically quote it.
+    arg=`$echo "X$arg" | $Xsed -e "$sed_quote_subst"`
+    case $arg in
+    *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \      ]*|*]*)
+      arg="\"$arg\""
+      ;;
+    esac
+    install_prog="$install_prog$arg"
+
+    # We need to accept at least all the BSD install flags.
+    dest=
+    files=
+    opts=
+    prev=
+    install_type=
+    isdir=no
+    stripme=
+    for arg
+    do
+      if test -n "$dest"; then
+       files="$files $dest"
+       dest="$arg"
+       continue
+      fi
+
+      case $arg in
+      -d) isdir=yes ;;
+      -f) prev="-f" ;;
+      -g) prev="-g" ;;
+      -m) prev="-m" ;;
+      -o) prev="-o" ;;
+      -s)
+       stripme=" -s"
+       continue
+       ;;
+      -*) ;;
+
+      *)
+       # If the previous option needed an argument, then skip it.
+       if test -n "$prev"; then
+         prev=
+       else
+         dest="$arg"
+         continue
+       fi
+       ;;
+      esac
+
+      # Aesthetically quote the argument.
+      arg=`$echo "X$arg" | $Xsed -e "$sed_quote_subst"`
+      case $arg in
+      *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \    ]*|*]*)
+       arg="\"$arg\""
+       ;;
+      esac
+      install_prog="$install_prog $arg"
+    done
+
+    if test -z "$install_prog"; then
+      $echo "$modename: you must specify an install program" 1>&2
+      $echo "$help" 1>&2
+      exit 1
+    fi
+
+    if test -n "$prev"; then
+      $echo "$modename: the \`$prev' option requires an argument" 1>&2
+      $echo "$help" 1>&2
+      exit 1
+    fi
+
+    if test -z "$files"; then
+      if test -z "$dest"; then
+       $echo "$modename: no file or destination specified" 1>&2
+      else
+       $echo "$modename: you must specify a destination" 1>&2
+      fi
+      $echo "$help" 1>&2
+      exit 1
+    fi
+
+    # Strip any trailing slash from the destination.
+    dest=`$echo "X$dest" | $Xsed -e 's%/$%%'`
+
+    # Check to see that the destination is a directory.
+    test -d "$dest" && isdir=yes
+    if test "$isdir" = yes; then
+      destdir="$dest"
+      destname=
+    else
+      destdir=`$echo "X$dest" | $Xsed -e 's%/[^/]*$%%'`
+      test "X$destdir" = "X$dest" && destdir=.
+      destname=`$echo "X$dest" | $Xsed -e 's%^.*/%%'`
+
+      # Not a directory, so check to see that there is only one file specified.
+      set dummy $files
+      if test $# -gt 2; then
+       $echo "$modename: \`$dest' is not a directory" 1>&2
+       $echo "$help" 1>&2
+       exit 1
+      fi
+    fi
+    case $destdir in
+    [\\/]* | [A-Za-z]:[\\/]*) ;;
+    *)
+      for file in $files; do
+       case $file in
+       *.lo) ;;
+       *)
+         $echo "$modename: \`$destdir' must be an absolute directory name" 1>&2
+         $echo "$help" 1>&2
+         exit 1
+         ;;
+       esac
+      done
+      ;;
+    esac
+
+    # This variable tells wrapper scripts just to set variables rather
+    # than running their programs.
+    libtool_install_magic="$magic"
+
+    staticlibs=
+    future_libdirs=
+    current_libdirs=
+    for file in $files; do
+
+      # Do each installation.
+      case $file in
+      *.$libext)
+       # Do the static libraries later.
+       staticlibs="$staticlibs $file"
+       ;;
+
+      *.la)
+       # Check to see that this really is a libtool archive.
+       if (sed -e '2q' $file | egrep "^# Generated by .*$PACKAGE") >/dev/null 2>&1; then :
+       else
+         $echo "$modename: \`$file' is not a valid libtool archive" 1>&2
+         $echo "$help" 1>&2
+         exit 1
+       fi
+
+       library_names=
+       old_library=
+       relink_command=
+       # If there is no directory component, then add one.
+       case $file in
+       */* | *\\*) . $file ;;
+       *) . ./$file ;;
+       esac
+
+       # Add the libdir to current_libdirs if it is the destination.
+       if test "X$destdir" = "X$libdir"; then
+         case "$current_libdirs " in
+         *" $libdir "*) ;;
+         *) current_libdirs="$current_libdirs $libdir" ;;
+         esac
+       else
+         # Note the libdir as a future libdir.
+         case "$future_libdirs " in
+         *" $libdir "*) ;;
+         *) future_libdirs="$future_libdirs $libdir" ;;
+         esac
+       fi
+
+       dir=`$echo "X$file" | $Xsed -e 's%/[^/]*$%%'`/
+       test "X$dir" = "X$file/" && dir=
+       dir="$dir$objdir"
+
+       if test -n "$relink_command"; then
+         $echo "$modename: warning: relinking \`$file'" 1>&2
+         $show "$relink_command"
+         if $run eval "$relink_command"; then :
+         else
+           $echo "$modename: error: relink \`$file' with the above command before installing it" 1>&2
+           continue
+         fi
+       fi
+
+       # See the names of the shared library.
+       set dummy $library_names
+       if test -n "$2"; then
+         realname="$2"
+         shift
+         shift
+
+         srcname="$realname"
+         test -n "$relink_command" && srcname="$realname"T
+
+         # Install the shared library and build the symlinks.
+         $show "$install_prog $dir/$srcname $destdir/$realname"
+         $run eval "$install_prog $dir/$srcname $destdir/$realname" || exit $?
+         if test -n "$stripme" && test -n "$striplib"; then
+           $show "$striplib $destdir/$realname"
+           $run eval "$striplib $destdir/$realname" || exit $?
+         fi
+
+         if test $# -gt 0; then
+           # Delete the old symlinks, and create new ones.
+           for linkname
+           do
+             if test "$linkname" != "$realname"; then
+               $show "(cd $destdir && $rm $linkname && $LN_S $realname $linkname)"
+               $run eval "(cd $destdir && $rm $linkname && $LN_S $realname $linkname)"
+             fi
+           done
+         fi
+
+         # Do each command in the postinstall commands.
+         lib="$destdir/$realname"
+         eval cmds=\"$postinstall_cmds\"
+         save_ifs="$IFS"; IFS='~'
+         for cmd in $cmds; do
+           IFS="$save_ifs"
+           $show "$cmd"
+           $run eval "$cmd" || exit $?
+         done
+         IFS="$save_ifs"
+       fi
+
+       # Install the pseudo-library for information purposes.
+       name=`$echo "X$file" | $Xsed -e 's%^.*/%%'`
+       instname="$dir/$name"i
+       $show "$install_prog $instname $destdir/$name"
+       $run eval "$install_prog $instname $destdir/$name" || exit $?
+
+       # Maybe install the static library, too.
+       test -n "$old_library" && staticlibs="$staticlibs $dir/$old_library"
+       ;;
+
+      *.lo)
+       # Install (i.e. copy) a libtool object.
+
+       # Figure out destination file name, if it wasn't already specified.
+       if test -n "$destname"; then
+         destfile="$destdir/$destname"
+       else
+         destfile=`$echo "X$file" | $Xsed -e 's%^.*/%%'`
+         destfile="$destdir/$destfile"
+       fi
+
+       # Deduce the name of the destination old-style object file.
+       case $destfile in
+       *.lo)
+         staticdest=`$echo "X$destfile" | $Xsed -e "$lo2o"`
+         ;;
+       *.$objext)
+         staticdest="$destfile"
+         destfile=
+         ;;
+       *)
+         $echo "$modename: cannot copy a libtool object to \`$destfile'" 1>&2
+         $echo "$help" 1>&2
+         exit 1
+         ;;
+       esac
+
+       # Install the libtool object if requested.
+       if test -n "$destfile"; then
+         $show "$install_prog $file $destfile"
+         $run eval "$install_prog $file $destfile" || exit $?
+       fi
+
+       # Install the old object if enabled.
+       if test "$build_old_libs" = yes; then
+         # Deduce the name of the old-style object file.
+         staticobj=`$echo "X$file" | $Xsed -e "$lo2o"`
+
+         $show "$install_prog $staticobj $staticdest"
+         $run eval "$install_prog \$staticobj \$staticdest" || exit $?
+       fi
+       exit 0
+       ;;
+
+      *)
+       # Figure out destination file name, if it wasn't already specified.
+       if test -n "$destname"; then
+         destfile="$destdir/$destname"
+       else
+         destfile=`$echo "X$file" | $Xsed -e 's%^.*/%%'`
+         destfile="$destdir/$destfile"
+       fi
+
+       # Do a test to see if this is really a libtool program.
+       if (sed -e '4q' $file | egrep "^# Generated by .*$PACKAGE") >/dev/null 2>&1; then
+         notinst_deplibs=
+         relink_command=
+
+         # If there is no directory component, then add one.
+         case $file in
+         */* | *\\*) . $file ;;
+         *) . ./$file ;;
+         esac
+
+         # Check the variables that should have been set.
+         if test -z "$notinst_deplibs"; then
+           $echo "$modename: invalid libtool wrapper script \`$file'" 1>&2
+           exit 1
+         fi
+
+         finalize=yes
+         for lib in $notinst_deplibs; do
+           # Check to see that each library is installed.
+           libdir=
+           if test -f "$lib"; then
+             # If there is no directory component, then add one.
+             case $lib in
+             */* | *\\*) . $lib ;;
+             *) . ./$lib ;;
+             esac
+           fi
+           libfile="$libdir/"`$echo "X$lib" | $Xsed -e 's%^.*/%%g'` ### testsuite: skip nested quoting test
+           if test -n "$libdir" && test ! -f "$libfile"; then
+             $echo "$modename: warning: \`$lib' has not been installed in \`$libdir'" 1>&2
+             finalize=no
+           fi
+         done
+
+         relink_command=
+         # If there is no directory component, then add one.
+         case $file in
+         */* | *\\*) . $file ;;
+         *) . ./$file ;;
+         esac
+
+         outputname=
+         if test "$fast_install" = no && test -n "$relink_command"; then
+           if test "$finalize" = yes && test -z "$run"; then
+             tmpdir="/tmp"
+             test -n "$TMPDIR" && tmpdir="$TMPDIR"
+             tmpdir="$tmpdir/libtool-$$"
+             if $mkdir -p "$tmpdir" && chmod 700 "$tmpdir"; then :
+             else
+               $echo "$modename: error: cannot create temporary directory \`$tmpdir'" 1>&2
+               continue
+             fi
+             file=`$echo "X$file" | $Xsed -e 's%^.*/%%'`
+             outputname="$tmpdir/$file"
+             # Replace the output file specification.
+             relink_command=`$echo "X$relink_command" | $Xsed -e 's%@OUTPUT@%'"$outputname"'%g'`
+
+             $show "$relink_command"
+             if $run eval "$relink_command"; then :
+             else
+               $echo "$modename: error: relink \`$file' with the above command before installing it" 1>&2
+               ${rm}r "$tmpdir"
+               continue
+             fi
+             file="$outputname"
+           else
+             $echo "$modename: warning: cannot relink \`$file'" 1>&2
+           fi
+         else
+           # Install the binary that we compiled earlier.
+           file=`$echo "X$file" | $Xsed -e "s%\([^/]*\)$%$objdir/\1%"`
+         fi
+       fi
+
+       # remove .exe since cygwin /usr/bin/install will append another
+       # one anyways
+       case $install_prog,$host in
+       /usr/bin/install*,*cygwin*)
+         case $file:$destfile in
+         *.exe:*.exe)
+           # this is ok
+           ;;
+         *.exe:*)
+           destfile=$destfile.exe
+           ;;
+         *:*.exe)
+           destfile=`echo $destfile | sed -e 's,.exe$,,'`
+           ;;
+         esac
+         ;;
+       esac
+       $show "$install_prog$stripme $file $destfile"
+       $run eval "$install_prog\$stripme \$file \$destfile" || exit $?
+       test -n "$outputname" && ${rm}r "$tmpdir"
+       ;;
+      esac
+    done
+
+    for file in $staticlibs; do
+      name=`$echo "X$file" | $Xsed -e 's%^.*/%%'`
+
+      # Set up the ranlib parameters.
+      oldlib="$destdir/$name"
+
+      $show "$install_prog $file $oldlib"
+      $run eval "$install_prog \$file \$oldlib" || exit $?
+
+      if test -n "$stripme" && test -n "$striplib"; then
+       $show "$old_striplib $oldlib"
+       $run eval "$old_striplib $oldlib" || exit $?
+      fi
+
+      # Do each command in the postinstall commands.
+      eval cmds=\"$old_postinstall_cmds\"
+      save_ifs="$IFS"; IFS='~'
+      for cmd in $cmds; do
+       IFS="$save_ifs"
+       $show "$cmd"
+       $run eval "$cmd" || exit $?
+      done
+      IFS="$save_ifs"
+    done
+
+    if test -n "$future_libdirs"; then
+      $echo "$modename: warning: remember to run \`$progname --finish$future_libdirs'" 1>&2
+    fi
+
+    if test -n "$current_libdirs"; then
+      # Maybe just do a dry run.
+      test -n "$run" && current_libdirs=" -n$current_libdirs"
+      exec_cmd='$SHELL $0 --finish$current_libdirs'
+    else
+      exit 0
+    fi
+    ;;
+
+  # libtool finish mode
+  finish)
+    modename="$modename: finish"
+    libdirs="$nonopt"
+    admincmds=
+
+    if test -n "$finish_cmds$finish_eval" && test -n "$libdirs"; then
+      for dir
+      do
+       libdirs="$libdirs $dir"
+      done
+
+      for libdir in $libdirs; do
+       if test -n "$finish_cmds"; then
+         # Do each command in the finish commands.
+         eval cmds=\"$finish_cmds\"
+         save_ifs="$IFS"; IFS='~'
+         for cmd in $cmds; do
+           IFS="$save_ifs"
+           $show "$cmd"
+           $run eval "$cmd" || admincmds="$admincmds
+       $cmd"
+         done
+         IFS="$save_ifs"
+       fi
+       if test -n "$finish_eval"; then
+         # Do the single finish_eval.
+         eval cmds=\"$finish_eval\"
+         $run eval "$cmds" || admincmds="$admincmds
+       $cmds"
+       fi
+      done
+    fi
+
+    # Exit here if they wanted silent mode.
+    test "$show" = ":" && exit 0
+
+    echo "----------------------------------------------------------------------"
+    echo "Libraries have been installed in:"
+    for libdir in $libdirs; do
+      echo "   $libdir"
+    done
+    echo
+    echo "If you ever happen to want to link against installed libraries"
+    echo "in a given directory, LIBDIR, you must either use libtool, and"
+    echo "specify the full pathname of the library, or use the \`-LLIBDIR'"
+    echo "flag during linking and do at least one of the following:"
+    if test -n "$shlibpath_var"; then
+      echo "   - add LIBDIR to the \`$shlibpath_var' environment variable"
+      echo "     during execution"
+    fi
+    if test -n "$runpath_var"; then
+      echo "   - add LIBDIR to the \`$runpath_var' environment variable"
+      echo "     during linking"
+    fi
+    if test -n "$hardcode_libdir_flag_spec"; then
+      libdir=LIBDIR
+      eval flag=\"$hardcode_libdir_flag_spec\"
+
+      echo "   - use the \`$flag' linker flag"
+    fi
+    if test -n "$admincmds"; then
+      echo "   - have your system administrator run these commands:$admincmds"
+    fi
+    if test -f /etc/ld.so.conf; then
+      echo "   - have your system administrator add LIBDIR to \`/etc/ld.so.conf'"
+    fi
+    echo
+    echo "See any operating system documentation about shared libraries for"
+    echo "more information, such as the ld(1) and ld.so(8) manual pages."
+    echo "----------------------------------------------------------------------"
+    exit 0
+    ;;
+
+  # libtool execute mode
+  execute)
+    modename="$modename: execute"
+
+    # The first argument is the command name.
+    cmd="$nonopt"
+    if test -z "$cmd"; then
+      $echo "$modename: you must specify a COMMAND" 1>&2
+      $echo "$help"
+      exit 1
+    fi
+
+    # Handle -dlopen flags immediately.
+    for file in $execute_dlfiles; do
+      if test ! -f "$file"; then
+       $echo "$modename: \`$file' is not a file" 1>&2
+       $echo "$help" 1>&2
+       exit 1
+      fi
+
+      dir=
+      case $file in
+      *.la)
+       # Check to see that this really is a libtool archive.
+       if (sed -e '2q' $file | egrep "^# Generated by .*$PACKAGE") >/dev/null 2>&1; then :
+       else
+         $echo "$modename: \`$lib' is not a valid libtool archive" 1>&2
+         $echo "$help" 1>&2
+         exit 1
+       fi
+
+       # Read the libtool library.
+       dlname=
+       library_names=
+
+       # If there is no directory component, then add one.
+       case $file in
+       */* | *\\*) . $file ;;
+       *) . ./$file ;;
+       esac
+
+       # Skip this library if it cannot be dlopened.
+       if test -z "$dlname"; then
+         # Warn if it was a shared library.
+         test -n "$library_names" && $echo "$modename: warning: \`$file' was not linked with \`-export-dynamic'"
+         continue
+       fi
+
+       dir=`$echo "X$file" | $Xsed -e 's%/[^/]*$%%'`
+       test "X$dir" = "X$file" && dir=.
+
+       if test -f "$dir/$objdir/$dlname"; then
+         dir="$dir/$objdir"
+       else
+         $echo "$modename: cannot find \`$dlname' in \`$dir' or \`$dir/$objdir'" 1>&2
+         exit 1
+       fi
+       ;;
+
+      *.lo)
+       # Just add the directory containing the .lo file.
+       dir=`$echo "X$file" | $Xsed -e 's%/[^/]*$%%'`
+       test "X$dir" = "X$file" && dir=.
+       ;;
+
+      *)
+       $echo "$modename: warning \`-dlopen' is ignored for non-libtool libraries and objects" 1>&2
+       continue
+       ;;
+      esac
+
+      # Get the absolute pathname.
+      absdir=`cd "$dir" && pwd`
+      test -n "$absdir" && dir="$absdir"
+
+      # Now add the directory to shlibpath_var.
+      if eval "test -z \"\$$shlibpath_var\""; then
+       eval "$shlibpath_var=\"\$dir\""
+      else
+       eval "$shlibpath_var=\"\$dir:\$$shlibpath_var\""
+      fi
+    done
+
+    # This variable tells wrapper scripts just to set shlibpath_var
+    # rather than running their programs.
+    libtool_execute_magic="$magic"
+
+    # Check if any of the arguments is a wrapper script.
+    args=
+    for file
+    do
+      case $file in
+      -*) ;;
+      *)
+       # Do a test to see if this is really a libtool program.
+       if (sed -e '4q' $file | egrep "^# Generated by .*$PACKAGE") >/dev/null 2>&1; then
+         # If there is no directory component, then add one.
+         case $file in
+         */* | *\\*) . $file ;;
+         *) . ./$file ;;
+         esac
+
+         # Transform arg to wrapped name.
+         file="$progdir/$program"
+       fi
+       ;;
+      esac
+      # Quote arguments (to preserve shell metacharacters).
+      file=`$echo "X$file" | $Xsed -e "$sed_quote_subst"`
+      args="$args \"$file\""
+    done
+
+    if test -z "$run"; then
+      if test -n "$shlibpath_var"; then
+       # Export the shlibpath_var.
+       eval "export $shlibpath_var"
+      fi
+
+      # Restore saved enviroment variables
+      if test "${save_LC_ALL+set}" = set; then
+       LC_ALL="$save_LC_ALL"; export LC_ALL
+      fi
+      if test "${save_LANG+set}" = set; then
+       LANG="$save_LANG"; export LANG
+      fi
+
+      # Now prepare to actually exec the command.
+      exec_cmd='"$cmd"$args'
+    else
+      # Display what would be done.
+      if test -n "$shlibpath_var"; then
+       eval "\$echo \"\$shlibpath_var=\$$shlibpath_var\""
+       $echo "export $shlibpath_var"
+      fi
+      $echo "$cmd$args"
+      exit 0
+    fi
+    ;;
+
+  # libtool clean and uninstall mode
+  clean | uninstall)
+    modename="$modename: $mode"
+    rm="$nonopt"
+    files=
+    rmforce=
+    exit_status=0
+
+    # This variable tells wrapper scripts just to set variables rather
+    # than running their programs.
+    libtool_install_magic="$magic"
+
+    for arg
+    do
+      case $arg in
+      -f) rm="$rm $arg"; rmforce=yes ;;
+      -*) rm="$rm $arg" ;;
+      *) files="$files $arg" ;;
+      esac
+    done
+
+    if test -z "$rm"; then
+      $echo "$modename: you must specify an RM program" 1>&2
+      $echo "$help" 1>&2
+      exit 1
+    fi
+
+    rmdirs=
+
+    for file in $files; do
+      dir=`$echo "X$file" | $Xsed -e 's%/[^/]*$%%'`
+      if test "X$dir" = "X$file"; then
+       dir=.
+       objdir="$objdir"
+      else
+       objdir="$dir/$objdir"
+      fi
+      name=`$echo "X$file" | $Xsed -e 's%^.*/%%'`
+      test $mode = uninstall && objdir="$dir"
+
+      # Remember objdir for removal later, being careful to avoid duplicates
+      if test $mode = clean; then
+       case " $rmdirs " in
+         *" $objdir "*) ;;
+         *) rmdirs="$rmdirs $objdir" ;;
+       esac
+      fi
+
+      # Don't error if the file doesn't exist and rm -f was used.
+      if (test -L "$file") >/dev/null 2>&1 \
+       || (test -h "$file") >/dev/null 2>&1 \
+       || test -f "$file"; then
+       :
+      elif test -d "$file"; then
+       exit_status=1
+       continue
+      elif test "$rmforce" = yes; then
+       continue
+      fi
+
+      rmfiles="$file"
+
+      case $name in
+      *.la)
+       # Possibly a libtool archive, so verify it.
+       if (sed -e '2q' $file | egrep "^# Generated by .*$PACKAGE") >/dev/null 2>&1; then
+         . $dir/$name
+
+         # Delete the libtool libraries and symlinks.
+         for n in $library_names; do
+           rmfiles="$rmfiles $objdir/$n"
+         done
+         test -n "$old_library" && rmfiles="$rmfiles $objdir/$old_library"
+         test $mode = clean && rmfiles="$rmfiles $objdir/$name $objdir/${name}i"
+
+         if test $mode = uninstall; then
+           if test -n "$library_names"; then
+             # Do each command in the postuninstall commands.
+             eval cmds=\"$postuninstall_cmds\"
+             save_ifs="$IFS"; IFS='~'
+             for cmd in $cmds; do
+               IFS="$save_ifs"
+               $show "$cmd"
+               $run eval "$cmd"
+               if test $? != 0 && test "$rmforce" != yes; then
+                 exit_status=1
+               fi
+             done
+             IFS="$save_ifs"
+           fi
+
+           if test -n "$old_library"; then
+             # Do each command in the old_postuninstall commands.
+             eval cmds=\"$old_postuninstall_cmds\"
+             save_ifs="$IFS"; IFS='~'
+             for cmd in $cmds; do
+               IFS="$save_ifs"
+               $show "$cmd"
+               $run eval "$cmd"
+               if test $? != 0 && test "$rmforce" != yes; then
+                 exit_status=1
+               fi
+             done
+             IFS="$save_ifs"
+           fi
+           # FIXME: should reinstall the best remaining shared library.
+         fi
+       fi
+       ;;
+
+      *.lo)
+       if test "$build_old_libs" = yes; then
+         oldobj=`$echo "X$name" | $Xsed -e "$lo2o"`
+         rmfiles="$rmfiles $dir/$oldobj"
+       fi
+       ;;
+
+      *)
+       # Do a test to see if this is a libtool program.
+       if test $mode = clean &&
+          (sed -e '4q' $file | egrep "^# Generated by .*$PACKAGE") >/dev/null 2>&1; then
+         relink_command=
+         . $dir/$file
+
+         rmfiles="$rmfiles $objdir/$name $objdir/${name}S.${objext}"
+         if test "$fast_install" = yes && test -n "$relink_command"; then
+           rmfiles="$rmfiles $objdir/lt-$name"
+         fi
+       fi
+       ;;
+      esac
+      $show "$rm $rmfiles"
+      $run $rm $rmfiles || exit_status=1
+    done
+
+    # Try to remove the ${objdir}s in the directories where we deleted files
+    for dir in $rmdirs; do
+      if test -d "$dir"; then
+       $show "rmdir $dir"
+       $run rmdir $dir >/dev/null 2>&1
+      fi
+    done
+
+    exit $exit_status
+    ;;
+
+  "")
+    $echo "$modename: you must specify a MODE" 1>&2
+    $echo "$generic_help" 1>&2
+    exit 1
+    ;;
+  esac
+
+  if test -z "$exec_cmd"; then
+    $echo "$modename: invalid operation mode \`$mode'" 1>&2
+    $echo "$generic_help" 1>&2
+    exit 1
+  fi
+fi # test -z "$show_help"
+
+if test -n "$exec_cmd"; then
+  eval exec $exec_cmd
+  exit 1
+fi
+
+# We need to display help for each of the modes.
+case $mode in
+"") $echo \
+"Usage: $modename [OPTION]... [MODE-ARG]...
+
+Provide generalized library-building support services.
+
+    --config          show all configuration variables
+    --debug           enable verbose shell tracing
+-n, --dry-run         display commands without modifying any files
+    --features        display basic configuration information and exit
+    --finish          same as \`--mode=finish'
+    --help            display this help message and exit
+    --mode=MODE       use operation mode MODE [default=inferred from MODE-ARGS]
+    --quiet           same as \`--silent'
+    --silent          don't print informational messages
+    --version         print version information
+
+MODE must be one of the following:
+
+      clean           remove files from the build directory
+      compile         compile a source file into a libtool object
+      execute         automatically set library path, then run a program
+      finish          complete the installation of libtool libraries
+      install         install libraries or executables
+      link            create a library or an executable
+      uninstall       remove libraries from an installed directory
+
+MODE-ARGS vary depending on the MODE.  Try \`$modename --help --mode=MODE' for
+a more detailed description of MODE."
+  exit 0
+  ;;
+
+clean)
+  $echo \
+"Usage: $modename [OPTION]... --mode=clean RM [RM-OPTION]... FILE...
+
+Remove files from the build directory.
+
+RM is the name of the program to use to delete files associated with each FILE
+(typically \`/bin/rm').  RM-OPTIONS are options (such as \`-f') to be passed
+to RM.
+
+If FILE is a libtool library, object or program, all the files associated
+with it are deleted. Otherwise, only FILE itself is deleted using RM."
+  ;;
+
+compile)
+  $echo \
+"Usage: $modename [OPTION]... --mode=compile COMPILE-COMMAND... SOURCEFILE
+
+Compile a source file into a libtool library object.
+
+This mode accepts the following additional options:
+
+  -o OUTPUT-FILE    set the output file name to OUTPUT-FILE
+  -prefer-pic       try to building PIC objects only
+  -prefer-non-pic   try to building non-PIC objects only
+  -static           always build a \`.o' file suitable for static linking
+
+COMPILE-COMMAND is a command to be used in creating a \`standard' object file
+from the given SOURCEFILE.
+
+The output file name is determined by removing the directory component from
+SOURCEFILE, then substituting the C source code suffix \`.c' with the
+library object suffix, \`.lo'."
+  ;;
+
+execute)
+  $echo \
+"Usage: $modename [OPTION]... --mode=execute COMMAND [ARGS]...
+
+Automatically set library path, then run a program.
+
+This mode accepts the following additional options:
+
+  -dlopen FILE      add the directory containing FILE to the library path
+
+This mode sets the library path environment variable according to \`-dlopen'
+flags.
+
+If any of the ARGS are libtool executable wrappers, then they are translated
+into their corresponding uninstalled binary, and any of their required library
+directories are added to the library path.
+
+Then, COMMAND is executed, with ARGS as arguments."
+  ;;
+
+finish)
+  $echo \
+"Usage: $modename [OPTION]... --mode=finish [LIBDIR]...
+
+Complete the installation of libtool libraries.
+
+Each LIBDIR is a directory that contains libtool libraries.
+
+The commands that this mode executes may require superuser privileges.  Use
+the \`--dry-run' option if you just want to see what would be executed."
+  ;;
+
+install)
+  $echo \
+"Usage: $modename [OPTION]... --mode=install INSTALL-COMMAND...
+
+Install executables or libraries.
+
+INSTALL-COMMAND is the installation command.  The first component should be
+either the \`install' or \`cp' program.
+
+The rest of the components are interpreted as arguments to that command (only
+BSD-compatible install options are recognized)."
+  ;;
+
+link)
+  $echo \
+"Usage: $modename [OPTION]... --mode=link LINK-COMMAND...
+
+Link object files or libraries together to form another library, or to
+create an executable program.
+
+LINK-COMMAND is a command using the C compiler that you would use to create
+a program from several object files.
+
+The following components of LINK-COMMAND are treated specially:
+
+  -all-static       do not do any dynamic linking at all
+  -avoid-version    do not add a version suffix if possible
+  -dlopen FILE      \`-dlpreopen' FILE if it cannot be dlopened at runtime
+  -dlpreopen FILE   link in FILE and add its symbols to lt_preloaded_symbols
+  -export-dynamic   allow symbols from OUTPUT-FILE to be resolved with dlsym(3)
+  -export-symbols SYMFILE
+                   try to export only the symbols listed in SYMFILE
+  -export-symbols-regex REGEX
+                   try to export only the symbols matching REGEX
+  -LLIBDIR          search LIBDIR for required installed libraries
+  -lNAME            OUTPUT-FILE requires the installed library libNAME
+  -module           build a library that can dlopened
+  -no-fast-install  disable the fast-install mode
+  -no-install       link a not-installable executable
+  -no-undefined     declare that a library does not refer to external symbols
+  -o OUTPUT-FILE    create OUTPUT-FILE from the specified objects
+  -release RELEASE  specify package release information
+  -rpath LIBDIR     the created library will eventually be installed in LIBDIR
+  -R[ ]LIBDIR       add LIBDIR to the runtime path of programs and libraries
+  -static           do not do any dynamic linking of libtool libraries
+  -version-info CURRENT[:REVISION[:AGE]]
+                   specify library version info [each variable defaults to 0]
+
+All other options (arguments beginning with \`-') are ignored.
+
+Every other argument is treated as a filename.  Files ending in \`.la' are
+treated as uninstalled libtool libraries, other files are standard or library
+object files.
+
+If the OUTPUT-FILE ends in \`.la', then a libtool library is created,
+only library objects (\`.lo' files) may be specified, and \`-rpath' is
+required, except when creating a convenience library.
+
+If OUTPUT-FILE ends in \`.a' or \`.lib', then a standard library is created
+using \`ar' and \`ranlib', or on Windows using \`lib'.
+
+If OUTPUT-FILE ends in \`.lo' or \`.${objext}', then a reloadable object file
+is created, otherwise an executable program is created."
+  ;;
+
+uninstall)
+  $echo \
+"Usage: $modename [OPTION]... --mode=uninstall RM [RM-OPTION]... FILE...
+
+Remove libraries from an installation directory.
+
+RM is the name of the program to use to delete files associated with each FILE
+(typically \`/bin/rm').  RM-OPTIONS are options (such as \`-f') to be passed
+to RM.
+
+If FILE is a libtool library, all the files associated with it are deleted.
+Otherwise, only FILE itself is deleted using RM."
+  ;;
+
+*)
+  $echo "$modename: invalid operation mode \`$mode'" 1>&2
+  $echo "$help" 1>&2
+  exit 1
+  ;;
+esac
+
+echo
+$echo "Try \`$modename --help' for more information about other modes."
+
+exit 0
+
+# Local Variables:
+# mode:shell-script
+# sh-indentation:2
+# End:
diff --git a/missing b/missing
new file mode 100755 (executable)
index 0000000..1570c79
--- /dev/null
+++ b/missing
@@ -0,0 +1,265 @@
+#! /bin/sh
+# Common stub for a few missing GNU programs while installing.
+# Copyright (C) 1996, 1997, 1999, 2000 Free Software Foundation, Inc.
+# Originally by Fran,cois Pinard <pinard@iro.umontreal.ca>, 1996.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+
+# This program 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 General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+# 02111-1307, USA.
+
+if test $# -eq 0; then
+  echo 1>&2 "Try \`$0 --help' for more information"
+  exit 1
+fi
+
+run=:
+
+case "$1" in
+--run)
+  # Try to run requested program, and just exit if it succeeds.
+  run=
+  shift
+  "$@" && exit 0
+  ;;
+esac
+
+# If it does not exist, or fails to run (possibly an outdated version),
+# try to emulate it.
+case "$1" in
+
+  -h|--h|--he|--hel|--help)
+    echo "\
+$0 [OPTION]... PROGRAM [ARGUMENT]...
+
+Handle \`PROGRAM [ARGUMENT]...' for when PROGRAM is missing, or return an
+error status if there is no known handling for PROGRAM.
+
+Options:
+  -h, --help      display this help and exit
+  -v, --version   output version information and exit
+  --run           try to run the given command, and emulate it if it fails
+
+Supported PROGRAM values:
+  aclocal      touch file \`aclocal.m4'
+  autoconf     touch file \`configure'
+  autoheader   touch file \`config.h.in'
+  automake     touch all \`Makefile.in' files
+  bison        create \`y.tab.[ch]', if possible, from existing .[ch]
+  flex         create \`lex.yy.c', if possible, from existing .c
+  help2man     touch the output file
+  lex          create \`lex.yy.c', if possible, from existing .c
+  makeinfo     touch the output file
+  tar          try tar, gnutar, gtar, then tar without non-portable flags
+  yacc         create \`y.tab.[ch]', if possible, from existing .[ch]"
+    ;;
+
+  -v|--v|--ve|--ver|--vers|--versi|--versio|--version)
+    echo "missing 0.3 - GNU automake"
+    ;;
+
+  -*)
+    echo 1>&2 "$0: Unknown \`$1' option"
+    echo 1>&2 "Try \`$0 --help' for more information"
+    exit 1
+    ;;
+
+  aclocal)
+    echo 1>&2 "\
+WARNING: \`$1' is missing on your system.  You should only need it if
+         you modified \`acinclude.m4' or \`configure.in'.  You might want
+         to install the \`Automake' and \`Perl' packages.  Grab them from
+         any GNU archive site."
+    touch aclocal.m4
+    ;;
+
+  autoconf)
+    echo 1>&2 "\
+WARNING: \`$1' is missing on your system.  You should only need it if
+         you modified \`configure.in'.  You might want to install the
+         \`Autoconf' and \`GNU m4' packages.  Grab them from any GNU
+         archive site."
+    touch configure
+    ;;
+
+  autoheader)
+    echo 1>&2 "\
+WARNING: \`$1' is missing on your system.  You should only need it if
+         you modified \`acconfig.h' or \`configure.in'.  You might want
+         to install the \`Autoconf' and \`GNU m4' packages.  Grab them
+         from any GNU archive site."
+    files=`sed -n 's/^[ ]*A[CM]_CONFIG_HEADER(\([^)]*\)).*/\1/p' configure.in`
+    test -z "$files" && files="config.h"
+    touch_files=
+    for f in $files; do
+      case "$f" in
+      *:*) touch_files="$touch_files "`echo "$f" |
+                                      sed -e 's/^[^:]*://' -e 's/:.*//'`;;
+      *) touch_files="$touch_files $f.in";;
+      esac
+    done
+    touch $touch_files
+    ;;
+
+  automake)
+    echo 1>&2 "\
+WARNING: \`$1' is missing on your system.  You should only need it if
+         you modified \`Makefile.am', \`acinclude.m4' or \`configure.in'.
+         You might want to install the \`Automake' and \`Perl' packages.
+         Grab them from any GNU archive site."
+    find . -type f -name Makefile.am -print |
+          sed 's/\.am$/.in/' |
+          while read f; do touch "$f"; done
+    ;;
+
+  bison|yacc)
+    echo 1>&2 "\
+WARNING: \`$1' is missing on your system.  You should only need it if
+         you modified a \`.y' file.  You may need the \`Bison' package
+         in order for those modifications to take effect.  You can get
+         \`Bison' from any GNU archive site."
+    rm -f y.tab.c y.tab.h
+    if [ $# -ne 1 ]; then
+        eval LASTARG="\${$#}"
+       case "$LASTARG" in
+       *.y)
+           SRCFILE=`echo "$LASTARG" | sed 's/y$/c/'`
+           if [ -f "$SRCFILE" ]; then
+                cp "$SRCFILE" y.tab.c
+           fi
+           SRCFILE=`echo "$LASTARG" | sed 's/y$/h/'`
+           if [ -f "$SRCFILE" ]; then
+                cp "$SRCFILE" y.tab.h
+           fi
+         ;;
+       esac
+    fi
+    if [ ! -f y.tab.h ]; then
+       echo >y.tab.h
+    fi
+    if [ ! -f y.tab.c ]; then
+       echo 'main() { return 0; }' >y.tab.c
+    fi
+    ;;
+
+  lex|flex)
+    echo 1>&2 "\
+WARNING: \`$1' is missing on your system.  You should only need it if
+         you modified a \`.l' file.  You may need the \`Flex' package
+         in order for those modifications to take effect.  You can get
+         \`Flex' from any GNU archive site."
+    rm -f lex.yy.c
+    if [ $# -ne 1 ]; then
+        eval LASTARG="\${$#}"
+       case "$LASTARG" in
+       *.l)
+           SRCFILE=`echo "$LASTARG" | sed 's/l$/c/'`
+           if [ -f "$SRCFILE" ]; then
+                cp "$SRCFILE" lex.yy.c
+           fi
+         ;;
+       esac
+    fi
+    if [ ! -f lex.yy.c ]; then
+       echo 'main() { return 0; }' >lex.yy.c
+    fi
+    ;;
+
+  help2man)
+    echo 1>&2 "\
+WARNING: \`$1' is missing on your system.  You should only need it if
+        you modified a dependency of a manual page.  You may need the
+        \`Help2man' package in order for those modifications to take
+        effect.  You can get \`Help2man' from any GNU archive site."
+
+    file=`echo "$*" | sed -n 's/.*-o \([^ ]*\).*/\1/p'`
+    if test -z "$file"; then
+       file=`echo "$*" | sed -n 's/.*--output=\([^ ]*\).*/\1/p'`
+    fi
+    if [ -f "$file" ]; then
+       touch $file
+    else
+       test -z "$file" || exec >$file
+       echo ".ab help2man is required to generate this page"
+       exit 1
+    fi
+    ;;
+
+  makeinfo)
+    echo 1>&2 "\
+WARNING: \`$1' is missing on your system.  You should only need it if
+         you modified a \`.texi' or \`.texinfo' file, or any other file
+         indirectly affecting the aspect of the manual.  The spurious
+         call might also be the consequence of using a buggy \`make' (AIX,
+         DU, IRIX).  You might want to install the \`Texinfo' package or
+         the \`GNU make' package.  Grab either from any GNU archive site."
+    file=`echo "$*" | sed -n 's/.*-o \([^ ]*\).*/\1/p'`
+    if test -z "$file"; then
+      file=`echo "$*" | sed 's/.* \([^ ]*\) *$/\1/'`
+      file=`sed -n '/^@setfilename/ { s/.* \([^ ]*\) *$/\1/; p; q; }' $file`
+    fi
+    touch $file
+    ;;
+
+  tar)
+    shift
+    if test -n "$run"; then
+      echo 1>&2 "ERROR: \`tar' requires --run"
+      exit 1
+    fi
+
+    # We have already tried tar in the generic part.
+    # Look for gnutar/gtar before invocation to avoid ugly error
+    # messages.
+    if (gnutar --version > /dev/null 2>&1); then
+       gnutar ${1+"$@"} && exit 0
+    fi
+    if (gtar --version > /dev/null 2>&1); then
+       gtar ${1+"$@"} && exit 0
+    fi
+    firstarg="$1"
+    if shift; then
+       case "$firstarg" in
+       *o*)
+           firstarg=`echo "$firstarg" | sed s/o//`
+           tar "$firstarg" ${1+"$@"} && exit 0
+           ;;
+       esac
+       case "$firstarg" in
+       *h*)
+           firstarg=`echo "$firstarg" | sed s/h//`
+           tar "$firstarg" ${1+"$@"} && exit 0
+           ;;
+       esac
+    fi
+
+    echo 1>&2 "\
+WARNING: I can't seem to be able to run \`tar' with the given arguments.
+         You may want to install GNU tar or Free paxutils, or check the
+         command line arguments."
+    exit 1
+    ;;
+
+  *)
+    echo 1>&2 "\
+WARNING: \`$1' is needed, and you do not seem to have it handy on your
+         system.  You might have modified some files without having the
+         proper tools for further handling them.  Check the \`README' file,
+         it often tells you about the needed prerequirements for installing
+         this package.  You may also peek at any GNU archive site, in case
+         some other package would contain this missing \`$1' program."
+    exit 1
+    ;;
+esac
+
+exit 0
diff --git a/mkinstalldirs b/mkinstalldirs
new file mode 100755 (executable)
index 0000000..c65f558
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/sh
+# mkinstalldirs --- make directory hierarchy
+# Author: Noah Friedman <friedman@prep.ai.mit.edu>
+# Created: 1993-05-16
+# Last modified: 1994-03-25
+# Public domain
+# @(#)$Id: mkinstalldirs,v 1.1 2000/07/08 20:21:37 petersonm Exp $
+
+errstatus=0
+
+for file in ${1+"$@"} ; do 
+   set fnord `echo ":$file" | sed -ne 's/^:\//#/;s/^://;s/\// /g;s/^#/\//;p'`
+   shift
+
+   pathcomp=
+   for d in ${1+"$@"} ; do
+     pathcomp="$pathcomp$d"
+     case "$pathcomp" in
+       -* ) pathcomp=./$pathcomp ;;
+     esac
+
+     if test ! -d "$pathcomp"; then
+        echo "mkdir $pathcomp" 1>&2
+        mkdir "$pathcomp" || errstatus=$?
+     fi
+
+     pathcomp="$pathcomp/"
+   done
+done
+
+exit $errstatus
+
+# mkinstalldirs ends here
diff --git a/patches/asuka-sethost.diff b/patches/asuka-sethost.diff
new file mode 100644 (file)
index 0000000..a7c4841
--- /dev/null
@@ -0,0 +1,362 @@
+diff -urN srvx_original2/hash.h apples_backup/hash.h
+--- srvx_original2/hash.h      Mon Jun  2 22:05:38 2003
++++ apples_backup/hash.h       Mon Jun  2 21:53:53 2003
+@@ -48,7 +48,11 @@
+ #define FLAGS_DEAF            0x0020 /* deaf +d */
+ #define FLAGS_SERVICE         0x0040 /* cannot be kicked, killed or deoped +k */
+ #define FLAGS_GLOBAL          0x0080 /* receives global messages +g */
+-#define FLAGS_HELPER          0x0100 /* (network?) helper +h */
++
++// apples - sethost 
++// #define FLAGS_HELPER               0x0100 /* (network?) helper +h */
++#define FLAGS_SETHOST           0x0100 /* for +h */
++
+ #define FLAGS_PERSISTENT      0x0200 /* for reserved nicks, this isn't just one-shot */
+ #define FLAGS_GAGGED          0x0400 /* for gagged users */
+ #define FLAGS_AWAY            0x0800 /* for away users */
+@@ -63,7 +67,11 @@
+ #define IsGlobal(x)             ((x)->modes & FLAGS_GLOBAL)
+ #define IsWallOp(x)             ((x)->modes & FLAGS_WALLOP)
+ #define IsServNotice(x)         ((x)->modes & FLAGS_SERVNOTICE)
+-#define IsHelperIrcu(x)         ((x)->modes & FLAGS_HELPER)
++
++// apples - sethost
++// #define IsHelperIrcu(x)         ((x)->modes & FLAGS_HELPER)
++#define IsSetHost(x)            ((x)->modes & FLAGS_SETHOST)
++
+ #define IsGagged(x)             ((x)->modes & FLAGS_GAGGED)
+ #define IsAway(x)               ((x)->modes & FLAGS_AWAY)
+ #define IsStamped(x)            ((x)->modes & FLAGS_STAMPED)
+@@ -103,6 +111,9 @@
+     unsigned int dead : 1;
+     unsigned long ip;
+     long modes;                   /* user flags +isw etc... */
++
++    // apples - sethost
++    char sethost[USERLEN + HOSTLEN + 2]; /* 1 for '\0' and 1 for @ = 2 */
+     time_t timestamp;
+     struct server *uplink;
+diff -urN srvx_original2/opserv.c apples_backup/opserv.c
+--- srvx_original2/opserv.c    Mon Jun  2 22:05:20 2003
++++ apples_backup/opserv.c     Mon Jun  2 21:54:03 2003
+@@ -1229,7 +1229,11 @@
+       if (IsOper(target)) buffer[bpos++] = 'o';
+       if (IsGlobal(target)) buffer[bpos++] = 'g';
+       if (IsServNotice(target)) buffer[bpos++] = 's';
+-      if (IsHelperIrcu(target)) buffer[bpos++] = 'h';
++      
++        // apples - sethost
++        // if (IsHelperIrcu(target)) buffer[bpos++] = 'h';
++        if (IsSetHost(target)) buffer[bpos++] = 'h';
++
+       if (IsService(target)) buffer[bpos++] = 'k';
+       if (IsDeaf(target)) buffer[bpos++] = 'd';
+         if (IsHiddenHost(target)) buffer[bpos++] = 'x';
+diff -urN srvx_original2/proto-common.c apples_backup/proto-common.c
+--- srvx_original2/proto-common.c      Mon Jun  2 22:05:06 2003
++++ apples_backup/proto-common.c       Mon Jun  2 21:53:46 2003
+@@ -537,13 +537,26 @@
+         nickname = "*";
+     }
+     if (options & GENMASK_STRICT_IDENT) {
+-        ident = user->ident;
++        // apples - sethost
++        if (IsSetHost(user)) {
++          ident = alloca(strcspn(user->sethost, "@")+2);
++          safestrncpy(ident, user->sethost, strcspn(user->sethost, "@")+1);
++        }
++        else
++          ident = user->ident;
+     } else if (options & GENMASK_ANY_IDENT) {
+         ident = "*";
+     } else {
+-        ident = alloca(strlen(user->ident)+2);
+-        ident[0] = '*';
+-        strcpy(ident+1, user->ident + ((*user->ident == '~')?1:0));
++        // apples - sethost
++        if (IsSetHost(user)) {
++          ident = alloca(strcspn(user->sethost, "@")+3);
++          ident[0] = '*';
++          safestrncpy(ident+1, user->sethost, strcspn(user->sethost, "@")+1);
++        } else {
++          ident = alloca(strlen(user->ident)+2);
++          ident[0] = '*';
++          strcpy(ident+1, user->ident + ((*user->ident == '~')?1:0));
++        }
+     }
+     hostname = user->hostname;
+     if (IsHiddenHost(user) && user->handle_info && hidden_host_suffix && !(options & GENMASK_NO_HIDING)) {
+@@ -604,15 +617,20 @@
+           sprintf(hostname, "*.%s", user->hostname+ii+2);
+       }
+     }
+-    /* Emit hostmask */
+-    len = strlen(ident) + strlen(hostname) + 2;
+-    if (nickname) {
+-        len += strlen(nickname) + 1;
+-        mask = malloc(len);
+-        sprintf(mask, "%s!%s@%s", nickname, ident, hostname);
+-    } else {
+-        mask = malloc(len);
+-        sprintf(mask, "%s@%s", ident, hostname);
+-    }
++    // apples - sethost
++    if (IsSetHost(user)) 
++      hostname = strchr(user->sethost, '@') + 1;
++
++      /* Emit hostmask */
++      len = strlen(ident) + strlen(hostname) + 2;
++      if (nickname) {
++          len += strlen(nickname) + 1;
++          mask = malloc(len);
++          sprintf(mask, "%s!%s@%s", nickname, ident, hostname);
++      } else {
++          mask = malloc(len);
++          sprintf(mask, "%s@%s", ident, hostname);
++      }
++
+     return mask;
+ }
+diff -urN srvx_original2/proto-p10.c apples_backup/proto-p10.c
+--- srvx_original2/proto-p10.c Mon Jun  2 22:04:54 2003
++++ apples_backup/proto-p10.c  Mon Jun  2 21:53:32 2003
+@@ -293,7 +293,8 @@
+ static unsigned int num_notice_funcs;
+ static struct dict *unbursted_channels;
+-static struct userNode *AddUser(struct server* uplink, const char *nick, const char *ident, const char *hostname, const char *modes, const char *account, const char *numeric, const char *userinfo, time_t timestamp, const char *realip);
++// apples - sethost
++static struct userNode *AddUser(struct server* uplink, const char *nick, const char *ident, const char *hostname, const char *modes, const char *account, const char *numeric, const char *userinfo, time_t timestamp, const char *realip, const char *sethost);
+ /* Numerics can be XYY, XYYY, or XXYYY; with X's identifying the
+  * server and Y's indentifying the client on that server. */
+@@ -397,7 +398,11 @@
+         if (IsServNotice(user)) modes[modelen++] = 's';
+         if (IsDeaf(user)) modes[modelen++] = 'd';
+         if (IsGlobal(user)) modes[modelen++] = 'g';
+-        if (IsHelperIrcu(user)) modes[modelen++] = 'h';
++
++        // apples - set host
++        // if (IsHelperIrcu(user)) modes[modelen++] = 'h';
++        if (IsSetHost(user)) modes[modelen++] = 'h';
++
+         if (IsHiddenHost(user)) modes[modelen++] = 'x';
+         modes[modelen] = 0;
+@@ -948,6 +953,8 @@
+ static CMD_FUNC(cmd_nick)
+ {
+     struct userNode *user;
++    int i; // apples - sethost
++
+     if ((user = GetUserH(origin))) {
+         /* nick change (since the source is a user numeric) */
+         if (argc < 2) return 0;
+@@ -959,14 +966,26 @@
+         serv = GetServerH(origin);
+         if (argv[6][0] == '+') {
+             if (argc < 10) return 0;
+-            if (argc > 10) {
+-                /* The user is +r and has an ACCOUNT stamp in argv[7]. */
+-                AddUser(serv, argv[1], argv[4], argv[5], argv[6], argv[7], argv[9], argv[argc-1], atoi(argv[3]), argv[8]);
+-            } else {
+-                AddUser(serv, argv[1], argv[4], argv[5], argv[6], NULL, argv[8], argv[argc-1], atoi(argv[3]), argv[7]);
++
++            // apples - sethost
++            if ((strchr(argv[6],'h')) && (strchr(argv[6],'r'))) { // +rh
++              i = 0;
++              while ((argv[6][i] != 'r') && (argv[6][i] != 'h')) i++;
++              if (argv[6][i] == 'r') // +r came before +h
++                AddUser(serv, argv[1], argv[4], argv[5], argv[6], argv[7], argv[10], argv[argc-1], atoi(argv[3]), argv[9], argv[8]);
++              else // +h came before +r
++                AddUser(serv, argv[1], argv[4], argv[5], argv[6], argv[8], argv[10], argv[argc-1], atoi(argv[3]), argv[9], argv[7]); 
++            }
++            else {
++              if (strchr(argv[6],'h')) // +h-r
++                AddUser(serv, argv[1], argv[4], argv[5], argv[6], NULL, argv[9], argv[argc-1], atoi(argv[3]), argv[8], argv[7]);
++              else if (strchr(argv[6],'r')) // +r-h
++                AddUser(serv, argv[1], argv[4], argv[5], argv[6], argv[7], argv[9], argv[argc-1], atoi(argv[3]), argv[8], NULL);
++              else // -rh
++                AddUser(serv, argv[1], argv[4], argv[5], argv[6], NULL, argv[8], argv[argc-1], atoi(argv[3]), argv[7], NULL);
+             }
+         } else {
+-            AddUser(serv, argv[1], argv[4], argv[5], "+", NULL, argv[7], argv[argc-1], atoi(argv[3]), argv[6]);
++            AddUser(serv, argv[1], argv[4], argv[5], "+", NULL, argv[7], argv[argc-1], atoi(argv[3]), argv[6], NULL);
+         }
+     }
+     return 1;
+@@ -1057,6 +1076,8 @@
+     struct chanNode *cn;
+     struct userNode *un;
+     const char *modes;
++    char *sethost; // apples - sethost
++    int i; // apples - sethost
+     if (argc < 3) {
+         return 0;
+@@ -1067,7 +1088,19 @@
+             log(MAIN_LOG, LOG_ERROR, "Unable to find user %s whose mode is changing\n", argv[1]);
+             return 0;
+         }
+-        mod_usermode(un, argv[2]);
++        // apples - sethost
++        if (argc == 3)
++          mod_usermode(un, argv[2]);
++        else {
++          sethost = malloc(strlen(argv[2]) + 1 + strlen(argv[3]) + 1);
++          i = 0;
++          while((sethost[i++] = *argv[2]++));
++          i--; 
++          sethost[i++] = ' ';
++          while((sethost[i++] = *argv[3]++)); 
++          mod_usermode(un, sethost); // apples - sethost
++        }       
++
+         return 1;
+     }
+@@ -1733,7 +1766,7 @@
+     }
+     make_numeric(self, local_num, numeric);
+     if (old_user) timestamp = old_user->timestamp - 1;
+-    return AddUser(self, nick, nick, self->name, "+oik", NULL, numeric, desc, now, "AAAAAA");
++    return AddUser(self, nick, nick, self->name, "+oik", NULL, numeric, desc, now, "AAAAAA", NULL); // apples - sethost
+ }
+ struct userNode *
+@@ -1750,7 +1783,7 @@
+     }
+     make_numeric(self, local_num, numeric);
+     if (old_user) timestamp = old_user->timestamp - 1;
+-    return AddUser(self, nick, ident, hostname, "+i", NULL, numeric, desc, timestamp, "AAAAAA");
++    return AddUser(self, nick, ident, hostname, "+i", NULL, numeric, desc, timestamp, "AAAAAA", NULL); // apples - sethost
+ }
+ int
+@@ -1765,12 +1798,14 @@
+     return 1;
+ }
++// apples - sethost
+ static struct userNode*
+-AddUser(struct server* uplink, const char *nick, const char *ident, const char *hostname, const char *modes, const char *account, const char *numeric, const char *userinfo, time_t timestamp, const char *realip)
++AddUser(struct server* uplink, const char *nick, const char *ident, const char *hostname, const char *modes, const char *account, const char *numeric, const char *userinfo, time_t timestamp, const char *realip, const char *sethost)
+ {
+     struct userNode *oldUser, *uNode;
+     struct policer_params *pol_params;
+     unsigned int n, ignore_user;
++    char *newmodes; // apples - sethost
+     if ((strlen(numeric) < 3) || (strlen(numeric) > 5)) {
+         log(MAIN_LOG, LOG_WARNING, "AddUser(%p, %s, ...): numeric %s wrong length!\n", uplink, nick, numeric);
+@@ -1829,7 +1864,22 @@
+     uNode->num_local = base64toint(numeric+strlen(uNode->uplink->numeric), 3) & uNode->uplink->num_mask;
+     uNode->uplink->users[uNode->num_local] = uNode;
+     if (account) call_account_func(uNode, account);
+-    mod_usermode(uNode, modes);
++ 
++    // apples - sethost
++    if (sethost) {
++      newmodes = malloc(strlen(modes) + 1 + strlen(sethost) + 1);
++      n = 0;
++      while ((newmodes[n] = modes[n])) n++;
++      newmodes[n++] = ' ';
++      while ((newmodes[n] = *sethost)) { 
++        n++; 
++        sethost++; 
++      }
++      mod_usermode(uNode, newmodes);
++    }
++    else
++      mod_usermode(uNode, modes);
++
+     if (ignore_user) return uNode;
+     dict_insert(clients, uNode->nick, uNode);
+@@ -1899,8 +1949,10 @@
+     extern struct policer_params *oper_policer_params, *luser_policer_params;
+     static void call_oper_funcs(struct userNode *user);
+     int add = 1;
++    int i = 0;
+     if (!user || !mode_change) return;
++ 
+     while (1) {
+ #define do_user_mode(FLAG) do { if (add) user->modes |= FLAG; else user->modes &= ~FLAG; } while (0)
+       switch (*mode_change++) {
+@@ -1931,7 +1983,18 @@
+       case 'd': do_user_mode(FLAGS_DEAF); break;
+       case 'k': do_user_mode(FLAGS_SERVICE); break;
+       case 'g': do_user_mode(FLAGS_GLOBAL); break;
+-      case 'h': do_user_mode(FLAGS_HELPER); break;
++
++        // apples - sethosti
++        // don't need this anymore vvvvvv
++      // case 'h': do_user_mode(FLAGS_HELPER); break;
++        // so basically, +h is the only mode that requires a parameter for the time
++        // being, so i check if there's an 'h' in the first part, and if there is 
++        // then everything after the space becomes their new host
++        case 'h': do_user_mode(FLAGS_SETHOST); break;
++        case ' ': if (IsSetHost(user))
++                    while ((user->sethost[i++] = *mode_change++)); 
++                  break;  
++                  
+         case 'x': do_user_mode(FLAGS_HIDDEN_HOST); break;
+       }
+ #undef do_user_mode
+diff -urN srvx_original2/tools.c apples_backup/tools.c
+--- srvx_original2/tools.c     Mon Jun  2 22:05:28 2003
++++ apples_backup/tools.c      Mon Jun  2 21:53:21 2003
+@@ -291,6 +291,9 @@
+ user_matches_glob(struct userNode *user, const char *orig_glob, int include_nick)
+ {
+     char *glob, *marker;
++    int usingreal = 0; // apples - sethost
++    int usingset = 0; // apples - sethost
++    char *setident, *sethostname; // apples - sethost
+     /* Make a writable copy of the glob */
+     glob = alloca(strlen(orig_glob)+1);
+@@ -311,22 +314,37 @@
+         return 0;
+     }
+     *marker = 0;
+-    if (!match_ircglob(user->ident, glob)) return 0;
++    
++    // apples - sethost
++    if (match_ircglob(user->ident, glob)) usingreal = 1;
++    if (IsSetHost(user)) {
++          setident = alloca(strcspn(user->sethost, "@")+2);
++          safestrncpy(setident, user->sethost, strcspn(user->sethost, "@")+1);
++          if (match_ircglob(setident, glob)) usingset = 1;
++    }
++
+     glob = marker + 1;
+     /* Now check the host part */
++
++    // apples - sethost
++    if (IsSetHost(user)) {
++      sethostname = strchr(user->sethost, '@') + 1;
++      if (match_ircglob(sethostname, glob) && usingset) return 1;
++    }
++
+     if (isdigit(*glob) && !glob[strspn(glob, "0123456789./*?")]) {
+         /* Looks like an IP-based mask */
+         unsigned char userip[20];
+         sprintf(userip, "%ld.%ld.%ld.%ld", (user->ip >> 24) & 255, (user->ip >> 16) & 255, (user->ip >> 8) & 255, user->ip & 255);
+-        return match_ircglob(userip, glob);
++        return match_ircglob(userip, glob) && usingreal;
+     } else {
+         if (hidden_host_suffix && user->handle_info) {
+             char hidden_host[HOSTLEN+1];
+             snprintf(hidden_host, sizeof(hidden_host), "%s.%s", user->handle_info->handle, hidden_host_suffix);
+-            if (match_ircglob(hidden_host, glob)) return 1;
++            if (match_ircglob(hidden_host, glob) && usingreal) return 1;
+         }
+         /* The host part of the mask isn't IP-based */
+-        return match_ircglob(user->hostname, glob);
++        return match_ircglob(user->hostname, glob) && usingreal;
+     }
+ }
diff --git a/patches/helpserv-pgsql.diff b/patches/helpserv-pgsql.diff
new file mode 100644 (file)
index 0000000..b2602b0
--- /dev/null
@@ -0,0 +1,380 @@
+Index: src/Makefile.am
+===================================================================
+RCS file: /cvsroot/srvx/services/src/Makefile.am,v
+retrieving revision 1.59
+diff -u -r1.59 Makefile.am
+--- src/Makefile.am    9 Sep 2003 01:56:55 -0000       1.59
++++ src/Makefile.am    5 Nov 2003 14:15:46 -0000
+@@ -9,7 +9,7 @@
+       ./expnhelp < $(srcdir)/nickserv.help.m4 > $@
+ EXTRA_srvx_SOURCES = proto-bahamut.c proto-common.c proto-p10.c mod-snoop.c mod-memoserv.c
+-srvx_LDADD = @MODULE_OBJS@
++srvx_LDADD = @MODULE_OBJS@ -lpq
+ srvx_DEPENDENCIES = @MODULE_OBJS@
+ srvx_SOURCES = \
+       chanserv.c chanserv.h \
+Index: src/helpserv.c
+===================================================================
+RCS file: /cvsroot/srvx/services/src/helpserv.c,v
+retrieving revision 1.84
+diff -u -r1.84 helpserv.c
+--- src/helpserv.c     5 Nov 2003 13:52:23 -0000       1.84
++++ src/helpserv.c     5 Nov 2003 14:15:48 -0000
+@@ -46,6 +46,8 @@
+ #include "opserv.h"
+ #include "saxdb.h"
+ #include "timeq.h"
++#include "ioset.h"
++#include <postgresql/libpq-fe.h>
+ #define HELPSERV_CONF_NAME "services/helpserv"
+ #define HELPSERV_HELPFILE_NAME "helpserv.help"
+@@ -94,6 +96,7 @@
+ #define KEY_REQ_ON_JOIN "req_on_join"
+ #define KEY_AUTO_VOICE "auto_voice"
+ #define KEY_AUTO_DEVOICE "auto_devoice"
++#define KEY_LOG_SQL "log_sql"
+ /* General */
+ #define HSMSG_WRITE_SUCCESS      "HelpServ db write completed (in "FMT_TIME_T".%03lu seconds)."
+@@ -401,6 +404,7 @@
+     const char *description;
+     unsigned long db_backup_frequency;
+     const char *reqlogfile;
++    PGconn *sql_log;
+ } helpserv_conf;
+ static int helpserv_enabled;
+@@ -443,6 +447,7 @@
+     unsigned int req_on_join : 1;
+     unsigned int auto_voice : 1;
+     unsigned int auto_devoice : 1;
++    unsigned int log_sql : 1;
+     unsigned int helpchan_empty : 1;
+@@ -608,36 +613,153 @@
+     return dict_find(hs->users, hi->handle, NULL);
+ }
+-static void helpserv_log_request(struct helpserv_request *req, const char *reason) {
+-    char key[27+NICKLEN];
+-    char userhost[USERLEN+HOSTLEN+2];
++static struct string_list sql_queue;
++static struct io_fd *sql_fd;
+-    if (!reqlog_ctx || !req)
++static void pgsql_send_next_query() {
++    int res;
++
++    res = PQsendQuery(helpserv_conf.sql_log, sql_queue.list[0]);
++    if (!res) {
++        log_module(MAIN_LOG, LOG_ERROR, "Error sending query \"%s\": %s", sql_queue.list[0], PQerrorMessage(helpserv_conf.sql_log));
+         return;
+-    if (!reason)
+-        reason = "";
++    }
++    res = PQflush(helpserv_conf.sql_log);
++    if (res == EOF)
++        log_module(MAIN_LOG, LOG_ERROR, "Error flushing PgSql output: %s", PQerrorMessage(helpserv_conf.sql_log));
++}
++
++static void pgsql_readable(UNUSED_ARG(struct io_fd *fd)) {
++    PGconn *conn;
++    PGresult *pgres;
++    unsigned int ii;
++    int res;
++    ExecStatusType st;
++
++    conn = helpserv_conf.sql_log;
++    res = PQconsumeInput(conn);
++    if (!res)
++        log_module(MAIN_LOG, LOG_ERROR, "Error consuming PgSql input: %s", PQerrorMessage(conn));
++    if (PQisBusy(conn))
++        return;
++    while ((pgres = PQgetResult(conn))) {
++        st = PQresultStatus(pgres);
++        if (st != PGRES_COMMAND_OK)
++            log_module(MAIN_LOG, LOG_ERROR, "PgSql error in \"%s\": %s", sql_queue.list[0], PQresultErrorMessage(pgres));
++        PQclear(pgres);
++    }
++    if (sql_queue.used == 1)
++        sql_queue.list[1] = NULL;
++    free(sql_queue.list[0]);
++    sql_queue.used--;
++    for (ii = 0; ii < sql_queue.used; ++ii)
++        sql_queue.list[ii] = sql_queue.list[ii+1];
++    if (sql_queue.used)
++        pgsql_send_next_query();
++}
+-    sprintf(key, "%s-" FMT_TIME_T "-%lu", req->hs->helpserv->nick, req->opened, req->id);
+-    saxdb_start_record(reqlog_ctx, key, 1);
+-    if (req->helper) {
+-        saxdb_write_string(reqlog_ctx, KEY_REQUEST_HELPER, req->helper->handle->handle);
+-        saxdb_write_int(reqlog_ctx, KEY_REQUEST_ASSIGNED, req->assigned);
++static void
++string_buffer_append_quoted(struct string_buffer *dest, const char *src) {
++    if (src) {
++        size_t len = strlen(src);
++        string_buffer_append(dest, '\'');
++        if (dest->size < (dest->used + len * 2)) {
++            if (dest->size < len * 2)
++                dest->size = len * 4;
++            else
++                dest->size = dest->size * 2;
++            dest->list = realloc(dest->list, dest->size);
++        }
++        dest->used += PQescapeString(dest->list + dest->used, src, len);
++        string_buffer_append_string(dest, "', ");
++    } else {
++        string_buffer_append_string(dest, "NULL, ");
+     }
+-    if (req->handle) {
+-        saxdb_write_string(reqlog_ctx, KEY_REQUEST_HANDLE, req->handle->handle);
++}
++
++static void
++string_buffer_append_time(struct string_buffer *dest, time_t when) {
++    struct tm broken_out;
++    if (!when) {
++        string_buffer_append_string(dest, "NULL, ");
++        return;
+     }
+-    if (req->user) {
+-        saxdb_write_string(reqlog_ctx, KEY_REQUEST_NICK, req->user->nick);
+-        sprintf(userhost, "%s@%s", req->user->ident, req->user->hostname);
+-        saxdb_write_string(reqlog_ctx, KEY_REQUEST_USERHOST, userhost);
++    if (dest->size < dest->used + 20) {
++        if (dest->size < 20) {
++            dest->size = 40;
++        } else {
++            dest->size = dest->size * 2;
++        }
++        dest->list = realloc(dest->list, dest->size);
+     }
+-    saxdb_write_int(reqlog_ctx, KEY_REQUEST_OPENED, req->opened);
+-    saxdb_write_int(reqlog_ctx, KEY_REQUEST_CLOSED, now);
+-    saxdb_write_string(reqlog_ctx, KEY_REQUEST_CLOSEREASON, reason);
+-    saxdb_write_string_list(reqlog_ctx, KEY_REQUEST_TEXT, req->text);
+-    saxdb_end_record(reqlog_ctx);
++    dest->used += strftime(dest->list + dest->used, dest->size - dest->used, "'%Y-%m-%d %H:%M:%S', ", localtime_r(&when, &broken_out));
++}
+-    fflush(reqlog_f);
++static void
++pgsql_insert(char *query) {
++    string_list_append(&sql_queue, query);
++    if (sql_queue.used == 1)
++        pgsql_send_next_query();
++}
++
++static void helpserv_log_request(struct helpserv_request *req, const char *reason) {
++    char userhost[USERLEN+HOSTLEN+2];
++
++    if (req->user)
++        sprintf(userhost, "%s@%s", req->user->ident, req->user->hostname);
++    else
++        userhost[0] = 0;
++    if (reqlog_ctx) {
++        char key[27+NICKLEN];
++        sprintf(key, "%s-" FMT_TIME_T "-%lu", req->hs->helpserv->nick, req->opened, req->id);
++        saxdb_start_record(reqlog_ctx, key, 1);
++        if (req->helper) {
++            saxdb_write_string(reqlog_ctx, KEY_REQUEST_HELPER, req->helper->handle->handle);
++            saxdb_write_int(reqlog_ctx, KEY_REQUEST_ASSIGNED, req->assigned);
++        }
++        if (req->handle)
++            saxdb_write_string(reqlog_ctx, KEY_REQUEST_HANDLE, req->handle->handle);
++        if (req->user) {
++            saxdb_write_string(reqlog_ctx, KEY_REQUEST_NICK, req->user->nick);
++            saxdb_write_string(reqlog_ctx, KEY_REQUEST_USERHOST, userhost);
++        }
++        saxdb_write_int(reqlog_ctx, KEY_REQUEST_OPENED, req->opened);
++        saxdb_write_int(reqlog_ctx, KEY_REQUEST_CLOSED, now);
++        saxdb_write_string(reqlog_ctx, KEY_REQUEST_CLOSEREASON, reason);
++        saxdb_write_string_list(reqlog_ctx, KEY_REQUEST_TEXT, req->text);
++        saxdb_end_record(reqlog_ctx);
++        fflush(reqlog_f);
++    }
++    if (helpserv_conf.sql_log && req->hs->log_sql) {
++        struct string_buffer query, sb;
++        unsigned int ii;
++
++        sb.used = query.used = 0;
++        sb.size = query.size = 512;
++        query.list = malloc(query.size);
++        sb.list = malloc(sb.size);
++        string_buffer_append_string(&query, "INSERT INTO srvx_helpserv_reqs (c_bot, t_opened, t_assigned, t_closed, i_id, c_helper, c_user_account, c_user_nick, c_user_host, c_close_reason, c_text) VALUES (");
++        string_buffer_append_quoted(&query, req->hs->helpserv->nick);
++        string_buffer_append_time(&query, req->opened);
++        string_buffer_append_time(&query, req->assigned);
++        string_buffer_append_time(&query, now);
++        string_buffer_append_printf(&query, "%lu, ", req->id);
++        string_buffer_append_quoted(&query, req->helper ? req->helper->handle->handle : NULL);
++        string_buffer_append_quoted(&query, req->handle ? req->handle->handle : NULL);
++        string_buffer_append_quoted(&query, req->user ? req->user->nick : NULL);
++        string_buffer_append_quoted(&query, req->user ? userhost : NULL);
++        string_buffer_append_quoted(&query, reason);
++        for (ii = 0; ii < req->text->used; ++ii) {
++            string_buffer_append_string(&sb, req->text->list[ii]);
++            string_buffer_append(&sb, '\n');
++        }
++        string_buffer_append(&sb, 0);
++        string_buffer_append_quoted(&query, sb.list);
++        query.used -= 2; /* chop off ", " from append_quoted */
++        string_buffer_append_string(&query, ");");
++        pgsql_insert(query.list);
++        free(sb.list);
++    }
+ }
+ /* Searches for a request by number, nick, or account (num|nick|*account).
+@@ -2931,6 +3053,28 @@
+     OPTION_BINARY(hs->auto_devoice, "AutoDeVoice");
+ }
++static HELPSERV_OPTION(opt_log_sql) {
++    int changed = 0;
++    if (argc > 0) {
++        if (!from_opserv) {
++            helpserv_notice(user, HSMSG_SET_NEED_OPER);
++            return 0;
++        }
++        if (enabled_string(argv[0])) {
++            hs->log_sql = 1;
++            changed = 1;
++        } else if (disabled_string(argv[0])) {
++            hs->log_sql = 0;
++            changed = 1;
++        } else {
++            helpserv_notice(user, MSG_INVALID_BINARY, argv[0]);
++            return 0;
++        }
++    }
++    helpserv_notice(user, HSMSG_STRING_VALUE, "LogSql", hs->log_sql ? "Enabled" : "Disabled");
++    return changed;
++}
++
+ static HELPSERV_FUNC(cmd_set) {
+     helpserv_option_func_t *opt;
+@@ -2944,7 +3088,7 @@
+             opt_empty_interval, opt_stale_delay, opt_request_persistence,
+             opt_helper_persistence, opt_notification, opt_id_wrap,
+             opt_req_maxlen, opt_privmsg_only, opt_req_on_join, opt_auto_voice,
+-            opt_auto_devoice
++            opt_auto_devoice, opt_log_sql
+         };
+         helpserv_notice(user, HSMSG_QUEUE_OPTIONS);
+@@ -3267,6 +3411,7 @@
+     saxdb_write_int(ctx, KEY_REQ_ON_JOIN, hs->req_on_join);
+     saxdb_write_int(ctx, KEY_AUTO_VOICE, hs->auto_voice);
+     saxdb_write_int(ctx, KEY_AUTO_DEVOICE, hs->auto_devoice);
++    saxdb_write_int(ctx, KEY_LOG_SQL, hs->log_sql);
+     /* End bot record */
+     saxdb_end_record(ctx);
+@@ -3376,6 +3521,8 @@
+     hs->auto_voice = str ? enabled_string(str) : 0;
+     str = database_get_data(GET_RECORD_OBJECT(br), KEY_AUTO_DEVOICE, RECDB_QSTRING);
+     hs->auto_devoice = str ? enabled_string(str) : 0;
++    str = database_get_data(GET_RECORD_OBJECT(br), KEY_LOG_SQL, RECDB_QSTRING);
++    hs->log_sql = str ? enabled_string(str) : 0;
+     dict_foreach(users, user_read_helper, hs);
+@@ -3422,6 +3569,31 @@
+         helpserv_conf.reqlogfile = NULL;
+     }
++    if (helpserv_conf.sql_log) {
++        PQfinish(helpserv_conf.sql_log);
++        helpserv_conf.sql_log = NULL;
++    }
++    str = database_get_data(conf_node, "sql_log", RECDB_QSTRING);
++    if (str) {
++        PGconn *conn = PQconnectdb(str);
++        if (!conn) {
++            log_module(HS_LOG, LOG_ERROR, "Unable to allocate pgsql connection");
++        } else if (PQstatus(conn) == CONNECTION_BAD) {
++            log_module(HS_LOG, LOG_ERROR, "Pgsql connection failed: %s", PQerrorMessage(conn));
++            PQfinish(conn);
++        } else if (PQsetnonblocking(conn, 1) == -1) {
++            log_module(HS_LOG, LOG_ERROR, "Unable to make pgsql non-blocking");
++            PQfinish(conn);
++        } else {
++            helpserv_conf.sql_log = conn;
++            sql_fd = ioset_add(PQsocket(conn));
++            sql_fd->connected = 1;
++            sql_fd->wants_reads = 1;
++            sql_fd->readable_cb = pgsql_readable;
++            while (PQflush(conn)) ;
++        }
++    }
++
+     if (reqlog_ctx) {
+         saxdb_close_context(reqlog_ctx);
+         reqlog_ctx = NULL;
+@@ -4159,16 +4331,20 @@
+     return mktime(timeinfo);
+ }
+-/* If data != NULL, then don't add to the timeq */
+ static void helpserv_run_stats(time_t when) {
+     struct tm when_s;
++    struct string_buffer query;
+     struct helpserv_bot *hs;
+     struct helpserv_user *hs_user;
+     int i;
+     dict_iterator_t it, it2;
++    char timestamp[64];
+     last_stats_update = when;
+     localtime_r(&when, &when_s);
++    strftime(timestamp, sizeof(timestamp), "'%Y-%m-%d %H:%M:%S', ", &when_s);
++    query.size = 512;
++
+     for (it=dict_first(helpserv_bots_dict); it; it=iter_next(it)) {
+         hs = iter_data(it);
+@@ -4195,6 +4371,18 @@
+                 hs_user->reassigned_to[i] = hs_user->reassigned_to[i-1];
+             }
++            /* Log to SQL */
++            if (helpserv_conf.sql_log && hs->log_sql) {
++                query.list = malloc(query.size);
++                query.used = 0;
++                string_buffer_append_string(&query, "INSERT INTO srvx_helpserv_stats (c_bot, t_weekstart, c_helper, i_time, i_picked_up, i_closed, i_reassigned_from, i_reassigned_to) VALUES(");
++                string_buffer_append_quoted(&query, hs->helpserv->nick);
++                string_buffer_append_quoted(&query, timestamp);
++                string_buffer_append_quoted(&query, hs_user->handle->handle);
++                string_buffer_append_printf(&query, "%d, %d, %d, %d, %d);", hs_user->time_per_week[0], hs_user->picked_up[0], hs_user->closed[0], hs_user->reassigned_from[0], hs_user->reassigned_to[0]);
++                pgsql_insert(query.list);
++            }
++
+             /* Reset it for this week */
+             hs_user->time_per_week[0] = hs_user->picked_up[0] = hs_user->closed[0] = hs_user->reassigned_from[0] = hs_user->reassigned_to[0] = 0;
+         }
+@@ -4228,6 +4416,10 @@
+         saxdb_close_context(reqlog_ctx);
+     if (reqlog_f)
+         fclose(reqlog_f);
++    if (helpserv_conf.sql_log) {
++        PQfinish(helpserv_conf.sql_log);
++        helpserv_conf.sql_log = NULL;
++    }
+ }
+ void init_helpserv() {
+@@ -4293,6 +4485,7 @@
+     helpserv_define_option("REQONJOIN", opt_req_on_join);
+     helpserv_define_option("AUTOVOICE", opt_auto_voice);
+     helpserv_define_option("AUTODEVOICE", opt_auto_devoice);
++    helpserv_define_option("LOGSQL", opt_log_sql);
+     helpserv_bots_dict = dict_new();
+     dict_set_free_data(helpserv_bots_dict, helpserv_free_bot);
diff --git a/patches/helpserv-pgsql.txt b/patches/helpserv-pgsql.txt
new file mode 100644 (file)
index 0000000..418c210
--- /dev/null
@@ -0,0 +1,31 @@
+-- This patch assumes a database prepared with the script below.
+-- Once this is set up, you tell the HelpServ code to log stats
+-- by adding an entry to the "helpserv" section of srvx.conf:
+--   "sql_log" "host=postgres.testnet.com port=5432 dbname=srvx user=srvx password=TeStNeT requiressl=1";
+-- some of those options may be omitted, in which case the PostgreSQL
+-- client library will use defaults.  You may use hostaddr=10.0.0.7
+-- instead of host=postgres.testnet.com, if the database server does
+-- not have a name in DNS or in /etc/hosts.
+
+CREATE TABLE srvx_helpserv_reqs (
+   c_bot VARCHAR(32) NOT NULL,
+   t_opened TIMESTAMP NOT NULL,
+   t_assigned TIMESTAMP,
+   t_closed TIMESTAMP NOT NULL,
+   i_id INTEGER NOT NULL,
+   c_helper VARCHAR(32),
+   c_user_account VARCHAR(32),
+   c_user_nick VARCHAR(32),
+   c_user_host VARCHAR(80),
+   c_close_reason TEXT NOT NULL,
+   c_text TEXT);
+
+CREATE TABLE srvx_helpserv_stats (
+   c_bot VARCHAR(32) NOT NULL,
+   t_weekstart TIMESTAMP NOT NULL,
+   c_helper VARCHAR(32) NOT NULL,
+   i_time INTEGER NOT NULL,
+   i_picked_up INTEGER NOT NULL,
+   i_closed INTEGER NOT NULL,
+   i_reassigned_from INTEGER NOT NULL,
+   i_reassigned_to INTEGER NOT NULL);
diff --git a/patches/log-pgsql.diff b/patches/log-pgsql.diff
new file mode 100644 (file)
index 0000000..f0818b5
--- /dev/null
@@ -0,0 +1,275 @@
+Index: src/Makefile.am
+===================================================================
+RCS file: /cvsroot/srvx/services/src/Makefile.am,v
+retrieving revision 1.59
+diff -u -r1.59 Makefile.am
+--- src/Makefile.am    9 Sep 2003 01:56:55 -0000       1.59
++++ src/Makefile.am    28 Sep 2003 14:16:39 -0000
+@@ -9,7 +9,7 @@
+       ./expnhelp < $(srcdir)/nickserv.help.m4 > $@
+ EXTRA_srvx_SOURCES = proto-bahamut.c proto-common.c proto-p10.c mod-snoop.c mod-memoserv.c
+-srvx_LDADD = @MODULE_OBJS@
++srvx_LDADD = @MODULE_OBJS@ -lpq
+ srvx_DEPENDENCIES = @MODULE_OBJS@
+ srvx_SOURCES = \
+       chanserv.c chanserv.h \
+Index: src/log.c
+===================================================================
+RCS file: /cvsroot/srvx/services/src/log.c,v
+retrieving revision 1.65
+diff -u -r1.65 log.c
+--- src/log.c  22 Aug 2003 00:26:21 -0000      1.65
++++ src/log.c  28 Sep 2003 14:16:40 -0000
+@@ -22,6 +22,8 @@
+ #include "log.h"
+ #include "helpfile.h"
+ #include "nickserv.h"
++#include "ioset.h"
++#include <postgresql/libpq-fe.h>
+ struct logDestination;
+@@ -992,6 +994,234 @@
+     ldIrc_module
+ };
++/* pgsql: log type */
++
++struct logDest_pgsql {
++    struct logDestination base;
++    struct string_list queue;
++    PGconn *conn;
++    struct io_fd *fd;
++    int recurse_level;
++};
++static struct logDest_vtable ldPgsql_vtbl;
++
++static void ldPgsql_send_next_query(struct logDest_pgsql *ld) {
++    int res;
++
++    res = PQsendQuery(ld->conn, ld->queue.list[0]);
++    if (!res) {
++        ld->recurse_level++;
++        log_module(MAIN_LOG, LOG_ERROR, "Error sending query \"%s\": %s", ld->queue.list[0], PQerrorMessage(ld->conn));
++        ld->recurse_level--;
++        return;
++    }
++    res = PQflush(ld->conn);
++    if (res == EOF) {
++        ld->recurse_level++;
++        log_module(MAIN_LOG, LOG_ERROR, "Error flushing PgSql output: %s", PQerrorMessage(ld->conn));
++        ld->recurse_level--;
++    }
++}
++
++static void ldPgsql_readable(struct io_fd *fd) {
++    struct logDest_pgsql *ld = fd->data;
++    PGresult *pgres;
++    unsigned int ii;
++    int res;
++    ExecStatusType st;
++
++    res = PQconsumeInput(ld->conn);
++    if (!res) {
++        ld->recurse_level++;
++        log_module(MAIN_LOG, LOG_ERROR, "Error consuming PgSql input: %s", PQerrorMessage(ld->conn));
++        ld->recurse_level--;
++    }
++    if (PQisBusy(ld->conn))
++        return;
++    while ((pgres = PQgetResult(ld->conn))) {
++        st = PQresultStatus(pgres);
++        if (st != PGRES_COMMAND_OK) {
++            ld->recurse_level++;
++            log_module(MAIN_LOG, LOG_ERROR, "PgSql error in \"%s\": %s", ld->queue.list[0], PQresultErrorMessage(pgres));
++            ld->recurse_level--;
++        }
++        PQclear(pgres);
++    }
++    if (ld->queue.used == 1)
++        ld->queue.list[1] = NULL;
++    free(ld->queue.list[0]);
++    ld->queue.used--;
++    for (ii = 0; ii < ld->queue.used; ++ii)
++        ld->queue.list[ii] = ld->queue.list[ii+1];
++    if (ld->queue.used)
++        ldPgsql_send_next_query(ld);
++}
++
++static struct logDestination *
++ldPgsql_open(const char *args) {
++    struct logDest_pgsql *ld;
++    ld = calloc(1, sizeof(*ld));
++    ld->base.vtbl = &ldPgsql_vtbl;
++    ld->conn = PQconnectdb(args);
++    ld->recurse_level++;
++    if (!ld->conn) {
++        log_module(MAIN_LOG, LOG_ERROR, "Unable to allocate pgsql connection");
++    } else if (PQstatus(ld->conn) == CONNECTION_BAD) {
++        log_module(MAIN_LOG, LOG_ERROR, "Pgsql connection failed: %s", PQerrorMessage(ld->conn));
++    } else {
++        int res;
++        res = PQsetnonblocking(ld->conn, 1);
++        if (res == -1)
++            log_module(MAIN_LOG, LOG_ERROR, "Unable to make pgsql non-blocking");
++        ld->fd = ioset_add(PQsocket(ld->conn));
++        ld->fd->data = ld;
++        ld->fd->connected = 1;
++        ld->fd->wants_reads = 1;
++        ld->fd->readable_cb = ldPgsql_readable;
++        while (PQflush(ld->conn)) ;
++    }
++    ld->recurse_level--;
++    return &ld->base;
++}
++
++static void
++ldPgsql_reopen(struct logDestination *self_) {
++    struct logDest_pgsql *self = (struct logDest_pgsql*)self_;
++    PQreset(self->conn);
++}
++
++static void
++ldPgsql_close(struct logDestination *self_) {
++    struct logDest_pgsql *self = (struct logDest_pgsql*)self_;
++    unsigned int ii;
++
++    PQfinish(self->conn);
++    ioset_close(self->fd->fd, 0);
++    for (ii = 0; ii < self->queue.used; ++ii)
++        free(self->queue.list[ii]);
++    self->recurse_level--;
++    free(self->queue.list);
++    free(self);
++}
++
++static void
++string_buffer_append_quoted(struct string_buffer *dest, const char *src) {
++    size_t len = strlen(src);
++    if (dest->size < dest->used + len * 2) {
++        if (dest->size < len * 2) {
++            dest->size = len * 4;
++        } else {
++            dest->size = dest->size * 2;
++        }
++        dest->list = realloc(dest->list, dest->size);
++    }
++    dest->used += PQescapeString(dest->list+dest->used, src, len);
++}
++
++static void
++string_buffer_append_time(struct string_buffer *dest, time_t when) {
++    struct tm gmt;
++    if (dest->size < dest->used + 20) {
++        if (dest->size < 20) {
++            dest->size = 40;
++        } else {
++            dest->size = dest->size * 2;
++        }
++        dest->list = realloc(dest->list, dest->size);
++    }
++    dest->used += strftime(dest->list + dest->used, dest->size - dest->used, "%Y-%m-%d %H:%M:%S", gmtime_r(&when, &gmt));
++}
++
++static void
++string_buffer_append_quoted2(struct string_buffer *dest, const char *src) {
++    if (src) {
++        string_buffer_append_string(dest, ", '");
++        string_buffer_append_quoted(dest, src);
++        string_buffer_append(dest, '\'');
++    } else {
++        string_buffer_append_string(dest, ", NULL");
++    }
++}
++
++static void
++pgsql_insert(struct logDest_pgsql *self, char *query) {
++    string_list_append(&self->queue, query);
++    if (self->queue.used == 1)
++        ldPgsql_send_next_query(self);
++}
++
++static void
++ldPgsql_audit(struct logDestination *self_, struct log_type *type, struct logEntry *entry) {
++    struct logDest_pgsql *self = (struct logDest_pgsql*)self_;
++    struct string_buffer query;
++
++    if (self->recurse_level)
++        return;
++    query.size = 512;
++    query.list = malloc(query.size);
++    query.used = 0;
++    string_buffer_append_printf(&query, "INSERT INTO srvx_audit (i_module, i_severity, i_bot, t_when, c_channel_name, c_user_nick, c_user_account, c_user_hostmask, c_command) VALUES (srvx_module_id('");
++    string_buffer_append_quoted(&query, type->name);
++    string_buffer_append_printf(&query, "'), %d, srvx_bot_id('", entry->slvl);
++    string_buffer_append_quoted(&query, entry->bot->nick);
++    string_buffer_append_string(&query, "'), '");
++    string_buffer_append_time(&query, entry->time);
++    string_buffer_append_string(&query, "'");
++    string_buffer_append_quoted2(&query, entry->channel_name);
++    string_buffer_append_quoted2(&query, entry->user_nick);
++    string_buffer_append_quoted2(&query, entry->user_account);
++    string_buffer_append_quoted2(&query, entry->user_hostmask);
++    string_buffer_append_quoted2(&query, entry->command);
++    string_buffer_append_string(&query, ");");
++    pgsql_insert(self, query.list);
++}
++
++static void
++ldPgsql_replay(struct logDestination *self_, struct log_type *type, int is_write, const char *line) {
++    struct logDest_pgsql *self = (struct logDest_pgsql*)self_;
++    struct string_buffer query;
++
++    if (self->recurse_level)
++        return;
++    query.size = 512;
++    query.list = malloc(query.size);
++    query.used = 0;
++    string_buffer_append_printf(&query, "INSERT INTO srvx_replay (i_module, b_is_write, c_text) VALUES (srvx_module_id('");
++    string_buffer_append_quoted(&query, type->name);
++    string_buffer_append_printf(&query, "'), %s, '", (is_write ? "true" : "false"));
++    string_buffer_append_quoted(&query, line);
++    string_buffer_append_string(&query, "');");
++    pgsql_insert(self, query.list);
++}
++
++static void
++ldPgsql_module(struct logDestination *self_, struct log_type *type, enum log_severity sev, const char *message) {
++    struct logDest_pgsql *self = (struct logDest_pgsql*)self_;
++    struct string_buffer query;
++
++    if (self->recurse_level)
++        return;
++    query.size = 512;
++    query.list = malloc(query.size);
++    query.used = 0;
++    string_buffer_append_printf(&query, "INSERT INTO srvx_log (i_module, i_severity, c_message) VALUES (srvx_module_id('");
++    string_buffer_append_quoted(&query, type->name);
++    string_buffer_append_printf(&query, "'), %d, '", sev);
++    string_buffer_append_quoted(&query, message);
++    string_buffer_append_string(&query, "');");
++    pgsql_insert(self, query.list);
++}
++
++static struct logDest_vtable ldPgsql_vtbl = {
++    "pgsql",
++    ldPgsql_open,
++    ldPgsql_reopen,
++    ldPgsql_close,
++    ldPgsql_audit,
++    ldPgsql_replay,
++    ldPgsql_module
++};
++
+ void
+ log_init(void)
+ {
+@@ -1003,6 +1233,7 @@
+     dict_insert(log_dest_types, ldFile_vtbl.type_name, &ldFile_vtbl);
+     dict_insert(log_dest_types, ldStd_vtbl.type_name, &ldStd_vtbl);
+     dict_insert(log_dest_types, ldIrc_vtbl.type_name, &ldIrc_vtbl);
++    dict_insert(log_dest_types, ldPgsql_vtbl.type_name, &ldPgsql_vtbl);
+     conf_register_reload(log_conf_read);
+     log_default = log_register_type("*", NULL);
+     reg_exit_func(cleanup_logs);
diff --git a/patches/log-pgsql.txt b/patches/log-pgsql.txt
new file mode 100644 (file)
index 0000000..2d23361
--- /dev/null
@@ -0,0 +1,72 @@
+-- This patch assumes a database prepared with the script below.
+-- Once this is set up, you can create a log destination using the
+-- "pgsql:" schema followed by a normal PostgreSQL connect string.
+-- For example:
+-- "logs" {
+--   "*.*" "pgsql:host=postgres.testnet.com port=5432 dbname=srvx user=srvx password=TeStNeT requiressl=1";
+-- };
+-- some of those options may be omitted, in which case the PostgreSQL
+-- client library will use defaults.  You may use hostaddr=10.0.0.7
+-- instead of host=postgres.testnet.com, if the database server does
+-- not have a name in DNS or in /etc/hosts.
+
+CREATE TABLE srvx_modules (
+  i_id SMALLINT PRIMARY KEY,
+  s_name VARCHAR(32) UNIQUE NOT NULL);
+
+CREATE TABLE srvx_bots (
+  i_id SMALLINT PRIMARY KEY,
+  s_name VARCHAR(32) UNIQUE NOT NULL);
+
+CREATE TABLE srvx_audit (
+  i_module SMALLINT NOT NULL REFERENCES srvx_modules(i_id),
+  i_severity SMALLINT NOT NULL,
+  i_bot SMALLINT NOT NULL REFERENCES srvx_bots(i_id),
+  t_when TIMESTAMP NOT NULL,
+  c_channel_name VARCHAR(200),
+  c_user_nick VARCHAR(30) NOT NULL,
+  c_user_account VARCHAR(30),
+  c_user_hostmask VARCHAR(80),
+  c_command VARCHAR(500) NOT NULL);
+
+CREATE TABLE srvx_replay (
+  i_module SMALLINT NOT NULL REFERENCES srvx_modules(i_id),
+  t_when TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  b_is_write BOOLEAN NOT NULL,
+  c_text VARCHAR(510) NOT NULL);
+
+CREATE TABLE srvx_log (
+  i_module SMALLINT NOT NULL REFERENCES srvx_modules(i_id),
+  i_severity SMALLINT NOT NULL,
+  t_when TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  c_message VARCHAR(1024) NOT NULL);
+
+CREATE SEQUENCE srvx_modules_id_seq MINVALUE 0 MAXVALUE 32767;
+CREATE FUNCTION srvx_module_id (VARCHAR(32)) RETURNS SMALLINT
+  LANGUAGE 'plpgsql' STABLE STRICT AS '
+  DECLARE
+    name ALIAS FOR $1;
+    new_id srvx_modules.i_id%TYPE;
+  BEGIN
+    SELECT INTO new_id i_id FROM srvx_modules WHERE s_name=name;
+    IF NOT FOUND THEN
+      SELECT INTO new_id nextval(''srvx_modules_id_seq'');
+      INSERT INTO srvx_modules (i_id, s_name) VALUES (new_id, name);
+    END IF;
+    RETURN new_id;
+  END;';
+
+CREATE SEQUENCE srvx_bots_id_seq MINVALUE 0 MAXVALUE 32767;
+CREATE FUNCTION srvx_bot_id (VARCHAR(32)) RETURNS SMALLINT
+  LANGUAGE 'plpgsql' STABLE STRICT AS '
+  DECLARE
+    name ALIAS FOR $1;
+    new_id srvx_bots.i_id%TYPE;
+  BEGIN
+    SELECT INTO new_id i_id FROM srvx_bots WHERE s_name=name;
+    IF NOT FOUND THEN
+      SELECT INTO new_id nextval(''srvx_bots_id_seq'');
+      INSERT INTO srvx_bots (i_id, s_name) VALUES (new_id, name);
+    END IF;
+    RETURN new_id;
+  END;';
diff --git a/patches/ns_reclaim-flag102403.diff b/patches/ns_reclaim-flag102403.diff
new file mode 100644 (file)
index 0000000..36905ea
--- /dev/null
@@ -0,0 +1,80 @@
+diff -urN services-dist/src/nickserv.c services/src/nickserv.c
+--- services-dist/src/nickserv.c       Fri Oct 24 20:05:25 2003
++++ services/src/nickserv.c    Fri Oct 24 21:08:01 2003
+@@ -1977,7 +1977,7 @@
+     unsigned int i;
+     char *set_display[] = {
+         "INFO", "WIDTH", "TABLEWIDTH", "COLOR", "PRIVMSG", "STYLE",
+-        "EMAIL", "ANNOUNCEMENTS", "MAXLOGINS"
++        "EMAIL", "ANNOUNCEMENTS", "MAXLOGINS", "RECLAIM"
+     };
+     nickserv_notice(user, NSMSG_SETTING_LIST);
+@@ -2094,6 +2094,27 @@
+     return 1;
+ }
++static OPTION_FUNC(opt_reclaim)
++{
++    /* if nickserv's nick ownership functions are disabled, we don't want this. I wanted to send an error
++     * message to the user, but there appears to be no quick & easy method of doing so, so we'll hide instead -akl */
++    if (nickserv_conf.disable_nicks) return 1; 
++      
++    if (argc > 1) {
++      if (enabled_string(argv[1])) {
++          HANDLE_SET_FLAG(hi, NICKRECLAIM);
++      } else if (disabled_string(argv[1])) {
++          HANDLE_CLEAR_FLAG(hi, NICKRECLAIM);
++      } else {
++          nickserv_notice(user, MSG_INVALID_BINARY, argv[1]);
++          return 0;
++      }
++    }
++
++    nickserv_notice (user, NSMSG_STR_SETTING, "RECLAIM:", HANDLE_FLAGGED(hi, NICKRECLAIM) ? "On - Your nicks are protected." : "Off.");
++    return 1;
++}
++
+ static OPTION_FUNC(opt_privmsg)
+ {
+     if (argc > 1) {
+@@ -3305,6 +3326,9 @@
+     assert(user);
+     assert(ni);
++
++    if (!HANDLE_FLAGGED(ni->owner, NICKRECLAIM)) return;
++    
+     switch (action) {
+     case RECLAIM_NONE:
+         /* do nothing */
+@@ -3341,6 +3365,7 @@
+         irc_regnick(user);
+         return 0;
+     }
++    if (ni && !HANDLE_FLAGGED(ni->owner, NICKRECLAIM)) return 0;
+     if (nickserv_conf.warn_nick_owned) {
+         send_message(user, nickserv, NSMSG_RECLAIM_WARN, ni->nick, ni->owner->handle);
+     }
+@@ -3534,6 +3559,7 @@
+     dict_insert(nickserv_opt_dict, "EPITHET", opt_epithet);
+     dict_insert(nickserv_opt_dict, "ANNOUNCEMENTS", opt_announcements);
+     dict_insert(nickserv_opt_dict, "MAXLOGINS", opt_maxlogins);
++    dict_insert(nickserv_opt_dict, "RECLAIM", opt_reclaim);
+     nickserv_handle_dict = dict_new();
+     dict_set_free_keys(nickserv_handle_dict, free);
+diff -urN services-dist/src/nickserv.h services/src/nickserv.h
+--- services-dist/src/nickserv.h       Fri Oct 24 20:05:25 2003
++++ services/src/nickserv.h    Fri Oct 24 20:29:40 2003
+@@ -37,8 +37,9 @@
+ #define HI_FLAG_FROZEN         0x00000040
+ #define HI_FLAG_NODELETE       0x00000080
+ #define HI_FLAG_NETWORK_HELPER 0x00000100
++#define HI_FLAG_NICKRECLAIM    0x00000200
+ /* Flag characters for the above.  First char is LSB, etc. */
+-#define HANDLE_FLAGS "SphgscfnH"
++#define HANDLE_FLAGS "SphgscfnHR"
+ /* HI_STYLE_* go into handle_info.userlist_style */
+ #define HI_STYLE_DEF         'd'
diff --git a/patches/ns_tried2reg102403.diff b/patches/ns_tried2reg102403.diff
new file mode 100644 (file)
index 0000000..da2c7b9
--- /dev/null
@@ -0,0 +1,65 @@
+diff -urN services-dist/src/hash.h services/src/hash.h
+--- services-dist/src/hash.h   Fri Oct 24 20:05:25 2003
++++ services/src/hash.h        Fri Oct 24 21:35:44 2003
+@@ -56,6 +56,7 @@
+ #define FLAGS_STAMPED           0x1000 /* for users who have been stamped */
+ #define FLAGS_HIDDEN_HOST       0x2000 /* user's host is masked by their account */
+ #define FLAGS_REGNICK           0x4000 /* user owns their current nick */
++#define FLAGS_TRIED2REG               0x8000 /* user has used /msg NickServ@.. register already once this session */
+ #define IsOper(x)               ((x)->modes & FLAGS_OPER)
+ #define IsService(x)            ((x)->modes & FLAGS_SERVICE)
+@@ -71,6 +72,7 @@
+ #define IsStamped(x)            ((x)->modes & FLAGS_STAMPED)
+ #define IsHiddenHost(x)         ((x)->modes & FLAGS_HIDDEN_HOST)
+ #define IsReggedNick(x)         ((x)->modes & FLAGS_REGNICK)
++#define HasTried2Reg(x)               ((x)->modes & FLAGS_TRIED2REG)
+ #define IsLocal(x)              ((x)->uplink == self)
+ #define NICKLEN         30
+diff -urN services-dist/src/nickserv.c services/src/nickserv.c
+--- services-dist/src/nickserv.c       Fri Oct 24 20:05:25 2003
++++ services/src/nickserv.c    Fri Oct 24 21:36:24 2003
+@@ -141,6 +141,7 @@
+ #define NSMSG_BAD_MAX_LOGINS    "MaxLogins must be at most %d."
+ #define NSMSG_MAX_LOGINS        "Your account already has its limit of %d user(s) logged in."
+ #define NSMSG_SETTEE_LOGGED_IN  "%s is already logged in, so you cannot do that."
++#define NSMSG_TRIED2REG               "You have already attempted to use $bREGISTER$b once this session, you will have to reconnect to use it again."
+ #define NSMSG_STAMPED_REGISTER  "You have already authenticated to an account once this session; you may not register a new account."
+ #define NSMSG_STAMPED_AUTH      "You have already authenticated to an account once this session; you may not authenticate to another."
+@@ -1063,6 +1064,14 @@
+         return 0;
+     }
++    if (HasTried2Reg(user)) {
++      /* On networks with email enabled, it is possible to use this cmd repeatedly, making
++       * potential damage to services very, very easy. This only allows a user to try once
++       * per session -akl */ 
++      nickserv_notice(user, NSMSG_TRIED2REG);
++      return 0;
++    }
++    
+     NICKSERV_MIN_PARMS((unsigned)3 + nickserv_conf.email_required);
+     if (!is_valid_handle(argv[1])) {
+@@ -1136,6 +1145,8 @@
+     /* If they need to do email verification, tell them. */
+     if (no_auth)
+         nickserv_make_cookie(user, hi, ACTIVATION, hi->passwd);
++
++    user->modes |= FLAGS_TRIED2REG;
+     return 1;
+ }
+diff -urN services-dist/src/opserv.c services/src/opserv.c
+--- services-dist/src/opserv.c Fri Oct 24 20:05:25 2003
++++ services/src/opserv.c      Fri Oct 24 21:33:03 2003
+@@ -1195,6 +1195,7 @@
+       if (IsDeaf(target)) buffer[bpos++] = 'd';
+         if (IsHiddenHost(target)) buffer[bpos++] = 'x';
+         if (IsGagged(target)) buffer_cat(" (gagged)");
++      if (HasTried2Reg(target)) buffer_cat(" (registered an account)");
+       buffer[bpos] = 0;
+       if (bpos > 11) opserv_notice(user, buffer);
+     }
diff --git a/patches/srvx-bantypes.diff b/patches/srvx-bantypes.diff
new file mode 100644 (file)
index 0000000..348ce41
--- /dev/null
@@ -0,0 +1,240 @@
+diff -upr srvx-1.2rc3/src/chanserv.c Services/src/chanserv.c
+--- src/chanserv.c     Sat Jan  4 07:16:40 2003
++++ src/chanserv.c     Mon Jul  7 18:18:27 2003
+@@ -419,6 +419,8 @@ dict_t note_types;
+ static dict_t sChannels;
+ static dict_t plain_dnrs, mask_dnrs, handle_dnrs;
++extern const char *hidden_host_suffix;
++
+ static struct
+ {
+     struct chanNode   *debug_channel;
+@@ -2859,12 +2861,103 @@ bad_channel_ban(struct chanNode *channel
+     return 0;
+ }
++#define i_isdigit(x) isdigit((int) (unsigned char) (x))\r
++\r
++int is_ipv4_address(const char *host)\r
++{\r
++    while (*host != '\0') {\r
++       if (*host != '.' && !i_isdigit(*host))\r
++       return 0;\r
++       host++;\r
++    }\r
++    return 1;\r
++}\r
++\r
++static char *get_domain_mask(char *host)\r
++{\r
++    char *ptr;\r
++\r
++    if (strchr(host, '.') == NULL) {\r
++       /* no dots - toplevel domain or IPv6 address */\r
++       ptr = strrchr(host, ':');\r
++       if (ptr != NULL) {\r
++          /* IPv6 address, ban the last 64k addresses */\r
++          if (ptr[1] != '\0') strcpy(ptr+1, "*");\r
++       }\r
++       return host;\r
++    }\r
++\r
++    if (is_ipv4_address(host)) {\r
++       /* it's an IP address, change last digit to * */\r
++       ptr = strrchr(host, '.');\r
++       if (ptr != NULL && i_isdigit(ptr[1]))\r
++           strcpy(ptr+1, "*");\r
++    } else {\r
++       /* if more than one dot, skip the first\r
++          (dyn123.blah.net -> *.blah.net) */\r
++          ptr = strchr(host, '.');\r
++          if (ptr != NULL && strchr(ptr+1, '.') != NULL) {\r
++             host = ptr-1;\r
++             host[0] = '*';\r
++          }\r
++    }\r
++    return host;\r
++}\r
++\r
++char *generate_ban_hostmask(struct userNode *user, const char banopt)\r
++{\r
++    char *nickname, *ident = "*", *hostname, *mask, *usemask;\r
++    int len;\r
++\r
++    /* figure out string parts */\r
++    if(IsHiddenHost(user) && user->handle_info && hidden_host_suffix) {\r
++       usemask = alloca(strlen(user->handle_info->handle) + strlen(hidden_host_suffix) + 2);\r
++       sprintf(usemask, "%s.%s", user->handle_info->handle, hidden_host_suffix);\r
++    } \r
++    else {\r
++       usemask = user->hostname;\r
++    } \r
++\r
++    if((banopt == 'f') || (banopt == 'h') || (banopt == 'i') || (banopt == 'j')) {\r
++       nickname = user->nick;\r
++    }\r
++    else {\r
++       nickname = "*";\r
++    }\r
++\r
++    if((banopt == 'd') || (banopt == 'e') || (banopt == 'i') || (banopt == 'j')) {\r
++        hostname = get_domain_mask(usemask);\r
++    } \r
++    else {\r
++      hostname = usemask;\r
++    }\r
++\r
++    if((banopt == 'a') || (banopt == 'f')) {\r
++       ident = user->ident;\r
++    }\r
++    else if((banopt == 'b') || (banopt == 'd') || (banopt == 'i')) {\r
++        ident = malloc(strlen(user->ident)+1);\r
++        sprintf(ident, "*%s", user->ident);\r
++    }\r
++    else {\r
++        ident = "*";\r
++    }\r
++\r
++    /* Put it all together. */\r
++    len = strlen(ident) + strlen(hostname) + strlen(nickname) + 3;\r
++    mask = malloc(len);\r
++    sprintf(mask, "%s!%s@%s", nickname, ident, hostname);\r
++\r
++    return mask;\r
++}\r
++
+ static int
+ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, unsigned char *argv[], struct svccmd *cmd, int action)
+ {
+     struct userNode *victim, **victims;
+     unsigned int offset, n, victimCount = 0, duration = 0, present;
+-    char *reason = CSMSG_KICK_REASON, *ban, *name;
++    char *reason = CSMSG_KICK_REASON, *ban, *name, banopt;
++    struct chanData *cData;
+     offset = (action & ACTION_ADD_TIMED_BAN) ? 3 : 2;
+     REQUIRE_PARAMS(offset); (void)cmd;
+@@ -2910,7 +3003,19 @@ eject_user(struct userNode *user, struct
+       victims = &victim;
+       victimCount = 1;
+-      ban = generate_hostmask(victim, GENMASK_STRICT_HOST|GENMASK_ANY_IDENT);
++    if(!(cData = channel->channel_info)) \r
++    {\r
++       banopt = 'c';\r
++    }\r
++\r
++    if(!(cData->options[optBanType])) {\r
++       banopt = 'c';\r
++    }\r
++    else {\r
++       banopt = cData->options[optBanType];\r
++    }\r
++
++    ban = generate_ban_hostmask(victim, banopt);
+       name = victim->nick;
+     }
+     else
+@@ -5507,6 +5612,30 @@ static CHANNEL_OPTION_FUNC(opt_topicrefr
+     CHANNEL_MULTIPLE_OPTION("TopicRefresh ", "topic refresh", optTopicRefresh, -1);
+ }
++static CHANNEL_OPTION_FUNC(opt_bantype)\r
++{\r
++    struct chanData *cData = channel->channel_info;\r
++\r
++    struct valueData values[] =\r
++    {\r
++       {"*!user@host", 'a'},\r
++       {"*!*user@host", 'b'},\r
++       {"*!*@host", 'c'},\r
++       {"*!*user@*.host", 'd'},\r
++       {"*!*@*.host", 'e'},\r
++       {"nick!user@host", 'f'},\r
++       {"nick!*@host", 'h'},\r
++       {"nick!*user@*.host", 'i'},\r
++       {"nick!*@*.host", 'j'},\r
++    };\r
++\r
++    if(!(cData->options[optBanType])) {\r
++        cData->options[optBanType] = values[2].value;\r
++     }\r
++\r
++     CHANNEL_MULTIPLE_OPTION("BanType ", "bantype", optBanType, -1);\r
++}
++
+ static struct svccmd_list set_shows_list;
+ static void
+@@ -6538,6 +6667,7 @@ chanserv_conf_read(void)
+             "DefaultTopic", "TopicMask", "Greeting", "UserGreeting", "Modes",
+             "PubCmd", "StrictOp", "AutoOp", "EnfModes", "EnfTopic", "Protect",
+             "Toys", "Setters", "TopicRefresh", "CtcpUsers", "CtcpReaction",
++            "BanType",
+             /* binary options */
+             "Voice", "UserInfo", "DynLimit", "TopicSnarf", "PeonInvite", "NoDelete",
+             /* delimiter */
+@@ -7320,6 +7450,7 @@ init_chanserv(const char *nick)
+     DEFINE_CHANNEL_OPTION(ctcpusers);
+     DEFINE_CHANNEL_OPTION(ctcpreaction);
+     DEFINE_CHANNEL_OPTION(peoninvite);
++    DEFINE_CHANNEL_OPTION(bantype);
+     /* Alias set topic to set defaulttopic for compatibility. */
+     modcmd_register(chanserv_module, "set topic", chan_opt_defaulttopic, 1, 0, NULL);
+diff -upr srvx-1.2rc3/src/chanserv.h Services/src/chanserv.h
+--- src/chanserv.h     Mon Oct 21 04:34:58 2002
++++ src/chanserv.h     Mon Jul  7 18:16:21 2003
+@@ -51,10 +51,11 @@ enum opt
+     optTopicRefresh,
+     optCTCPUsers,
+     optCTCPReaction,
+-    optMax
++    optMax,
++    optBanType
+ };
+-#define OPTION_STRING_SIZE    (optMax + 2)
++#define OPTION_STRING_SIZE    (optBanType + 2)
+ #define FLAGS_STRING_LENGTH   5
+ /* Flags are stored in base64, so a length of 5 gives us
+@@ -76,7 +77,7 @@ enum opt
+    will not work because FLAGS_STRING_LENGTH is 5. */
+ #define CHANNEL_DEFAULT_FLAGS (CHANNEL_INFO_LINES)
+-#define CHANNEL_DEFAULT_OPTIONS       "lmoooanpcnat"
++#define CHANNEL_DEFAULT_OPTIONS       "lmoooanpcnatc"
+ #define IsProtected(x)                ((x)->flags & CHANNEL_NODELETE)
+ #define IsSuspended(x)                ((x)->flags & CHANNEL_SUSPENDED)
+diff -upr srvx-1.2rc3/src/chanserv.help Services/src/chanserv.help
+--- src/chanserv.help  Wed Jan  1 00:45:40 2003
++++ src/chanserv.help  Mon Jul  7 17:51:47 2003
+@@ -322,6 +322,7 @@
+         "DYNLIMIT:     Adjusts user limit (+l channel mode) to prevent join floods.",
+         "TOPICSNARF:   Topics set manually (by /TOPIC #channel ...) change default $C topic",
+         "PEONINVITE:   Indicates whether peons may use the $bINVITEME$b command.",
++        "BANTYPE:      Indicated what type of ban $C will set will case of a ban command.",
+         "$bIRCOP ONLY$b:",
+         "NODELETE:  Prevents channel deletion.",
+         "$uSee Also:$u set pubcmd, set strictop, set autoop, set enfmodes, set enftopic, set protect, set toys, set setters, set topicrefresh, set ctcpusers, set ctcpreaction");
+@@ -440,6 +441,17 @@
+         "$b2$b  Short timed ban (defaults to 3 minutes) on disallowed CTCPs.",
+         "$b3$b  Long timed ban (defaults to 1 hour) on disallowed CTCPs.",
+         "$uSee Also:$u set, set ctcpusers");
++"SET BANTYPE" ("/msg $C SET <#channel> BANTYPE <value>",\r
++        "This settings control what banmask $C sets upon a ban command. Valid settings are:",\r
++        "$b0$b *!user@host",\r
++        "$b1$b *!*user@host",\r
++        "$b2$b *!*@host",\r
++        "$b3$b *!*user@*.host",\r
++        "$b4$b *!*@*.host",\r
++        "$b5$b nick!user@host",\r
++        "$b6$b nick!*@host",\r
++        "$b7$b nick!*user@*.host",\r
++        "$b8$b nick!*@*.host");
+ "SETINFO" ("/msg $C SETINFO <#channel> <info>",
+         "This command will set a user defined information message to be displayed when you join the channel. By setting the message to '*', you will clear your message.",
+         "$uSee Also:$u access");
diff --git a/patches/srvx-successor.diff b/patches/srvx-successor.diff
new file mode 100644 (file)
index 0000000..d11e4a1
--- /dev/null
@@ -0,0 +1,217 @@
+diff -upr srvx-1.2rc4/src/chanserv.c Services/src/chanserv.c
+--- src/chanserv.c     Mon Jul  7 18:29:34 2003
++++ src/chanserv.c     Mon Jul  7 19:17:00 2003
+@@ -82,6 +82,7 @@
+ /* Channel data */
+ #define KEY_REGISTERED                "registered"
+ #define KEY_REGISTRAR         "registrar"
++#define KEY_SUCCESSOR        "successor"
+ #define KEY_SUSPENDED           "suspended"
+ #define KEY_PREVIOUS            "previous"
+ #define KEY_SUSPENDER         "suspender"
+@@ -247,6 +248,9 @@
+ #define CSMSG_NO_BANS         "No channel bans found on $b%s$b."
+ #define CSMSG_BANS_REMOVED    "Removed all channel bans from $b%s$b."
++#define CSMSG_SET_SUCCESSOR     "$b%s$b is now the successor of $b%s$b"
++#define CSMSG_ONLY_CO_SUCCESSOR "Only Coowners can be set as a successor."
++
+ /* Channel userlist */
+ #define CSMSG_ACCESS_SEARCH_HDR       "%s $b%ss$b matching %s:"
+ #define CSMSG_ACCESS_HEADER   "%s $b%ss$b:"
+@@ -321,6 +325,7 @@
+ #define CSMSG_CHANNEL_BANS    "$bBan Count:           $b%i"
+ #define CSMSG_CHANNEL_USERS   "$b%s Count:%*s$b%i"
+ #define CSMSG_CHANNEL_REGISTRAR "$bRegistrar:           $b%s"
++#define CSMSG_CHANNEL_SUCCESSOR  "$bSuccessor:            $b%s"
+ #define CSMSG_CHANNEL_SUSPENDED "$b%s$b is suspended:"
+ #define CSMSG_CHANNEL_HISTORY   "Suspension history for $b%s$b:"
+ #define CSMSG_CHANNEL_SUSPENDED_0 " by %s: %s"
+@@ -400,6 +405,8 @@
+ #define CSMSG_KICK_FORMAT     "%s (%s)"
+ #define CSMSG_PROTECT_REASON  "That user is protected."
++#define CSMSG_SUCCESSOR "$bSuccessor:$b %s"
++
+ #define CSFUNC_ARGS           user, channel, argc, argv, cmd
+ static unsigned char *CHANSERV_DEFAULT_MODES = "+nt";
+@@ -1505,8 +1512,9 @@ expire_channels(void *data)
+     struct chanData *channel, *next;
+     struct chanNode *save;
+     struct userData *user;
+-    char delay[INTERVALLEN], reason[INTERVALLEN + 64];
+-    int suspended;
++    struct handle_info *successor_handle;
++    char delay[INTERVALLEN], reason[INTERVALLEN + 64];
++    int suspended, ownerfound = 0;
+     (void)data;
+     intervalString(delay, chanserv_conf.channel_expire_delay);
+@@ -1514,11 +1522,36 @@ expire_channels(void *data)
+     for(channel = channelList; channel; channel = next)
+     {
+-      next = channel->next;
+-
++         next = channel->next;
++
++       /* Check for a successor, see if there's an owner first */
++       for(user = channel->users; user; user=user->next)
++       {
++          if(user->present && (user->access >= ulOwner)) 
++          {
++             ownerfound = 1;
++             break;
++          }
++       }
++       if(channel->successor && !ownerfound) 
++       {
++          if((successor_handle = get_handle_info(channel->successor)))
++          {
++              if((user = GetTrueChannelAccess(channel, successor_handle)))
++              {
++                  channel->userCount[user->access]--;
++                  userCount[user->access]--;
++
++                   user->access = ulOwner;
++
++                   channel->userCount[user->access]++;
++                   userCount[user->access]++;
++               }
++           }
++        }
+         /* See if the channel can be expired. */
+         if((now - channel->visited) <= chanserv_conf.channel_expire_delay) continue;
+-      if(IsProtected(channel)) continue;
++          if(IsProtected(channel)) continue;
+         /* Make sure there are no high-ranking users still in the channel. */
+         for(user=channel->users; user; user=user->next)
+@@ -4322,6 +4355,11 @@ static CHANSERV_FUNC(cmd_info)
+         }
+     }
++    if(cData->successor)
++    {
++       chanserv_notice(user, CSMSG_CHANNEL_SUCCESSOR, cData->successor);
++    }
++
+     if(privileged)
+     {
+       if((dnr = chanserv_is_dnr(chan_name, NULL)))
+@@ -5224,6 +5262,58 @@ static CHANNEL_OPTION_FUNC(opt_topicmask
+     return 1;
+ }
++static CHANNEL_OPTION_FUNC(opt_successor)
++{
++   struct userNode *target;
++   struct chanData *cData;
++   struct userData *uData;
++   struct handle_info *target_handle;
++
++   REQUIRE_PARAMS(2);
++
++   if(!(cData = channel->channel_info)) 
++   {
++      return 0;
++   }
++   if((argv[1][0] == '?') || (argv[1][0] == 0)) {
++      chanserv_notice(user, CSMSG_SUCCESSOR, (cData->successor ? cData->successor : "None."));
++      return 1;
++   }
++   if((target = GetUserH(argv[1])))
++   {
++      target_handle = target->handle_info;
++   }
++   else if(argv[1][0] == '*')
++   {
++      if(!(target_handle = get_handle_info(argv[1]+1)))
++      {
++          chanserv_notice(user, MSG_HANDLE_UNKNOWN, argv[1]+1);
++          return 0;
++      }
++   }
++   else
++   {
++      chanserv_notice(user, MSG_NICK_UNKNOWN, argv[1]);
++      return 0;
++   }
++   if((uData = GetTrueChannelAccess(channel->channel_info, target_handle)))
++   {
++      if(uData->access != ulCoowner) 
++      {
++         chanserv_notice(user, CSMSG_ONLY_CO_SUCCESSOR);
++         return 0;
++      }
++      cData->successor = strdup(uData->handle->handle);
++      chanserv_notice(user, CSMSG_SET_SUCCESSOR, uData->handle->handle, channel->name);
++      return 1;
++   }
++   else
++   {
++      chanserv_notice(user, CSMSG_USER_NO_ACCESS, target_handle->handle, channel->name);
++      return 0;
++   }  
++}
++
+ int opt_greeting_common(struct userNode *user, struct chanNode *channel, int argc, unsigned char *argv[], char *name, char **data)
+ {
+     (void)channel;
+@@ -6934,6 +7024,8 @@ chanserv_channel_read(const char *key, s
+     }
+     if((cData->suspended = suspended)) suspended->cData = cData;
++    str = database_get_data(channel, KEY_SUCCESSOR, RECDB_QSTRING);
++    if(str) cData->successor = strdup(str);
+     str = database_get_data(channel, KEY_REGISTERED, RECDB_QSTRING);
+     cData->registered = str ? (time_t)strtoul(str, NULL, 0) : now;
+     str = database_get_data(channel, KEY_VISITED, RECDB_QSTRING);
+@@ -7127,6 +7219,7 @@ chanserv_write_channel(struct saxdb_cont
+     saxdb_write_int(ctx, KEY_MAX, channel->max);
+     if(channel->topic[0]) saxdb_write_string(ctx, KEY_TOPIC, channel->topic);
+     if(channel->registrar) saxdb_write_string(ctx, KEY_REGISTRAR, channel->registrar);
++    if(channel->successor) saxdb_write_string(ctx, KEY_SUCCESSOR, channel->successor);
+     if(channel->greeting) saxdb_write_string(ctx, KEY_GREETING, channel->greeting);
+     if(channel->user_greeting) saxdb_write_string(ctx, KEY_USER_GREETING, channel->user_greeting);
+     if(channel->topic_mask) saxdb_write_string(ctx, KEY_TOPIC_MASK, channel->topic_mask);
+@@ -7412,6 +7505,7 @@ init_chanserv(const char *nick)
+     DEFINE_CHANNEL_OPTION(ctcpusers);
+     DEFINE_CHANNEL_OPTION(ctcpreaction);
+     DEFINE_CHANNEL_OPTION(peoninvite);
++    DEFINE_CHANNEL_OPTION(successor);
+     /* Alias set topic to set defaulttopic for compatibility. */
+     modcmd_register(chanserv_module, "set topic", chan_opt_defaulttopic, 1, 0, NULL);
+diff -upr srvx-1.2rc4/src/chanserv.h Services/src/chanserv.h
+--- src/chanserv.h     Mon Jul  7 18:29:34 2003
++++ src/chanserv.h     Mon Jul  7 19:16:55 2003
+@@ -95,6 +95,7 @@ struct chanData
+     char              *greeting;
+     char              *user_greeting;
+     char              *registrar;
++    char        *successor;
+     char                *topic_mask;
+     unsigned int      limit;
+diff -upr srvx-1.2rc4/src/chanserv.help Services/src/chanserv.help
+--- src/chanserv.help  Wed Jun 18 01:53:36 2003
++++ src/chanserv.help  Mon Jul  7 19:15:06 2003
+@@ -322,6 +322,7 @@
+         "DYNLIMIT:     Adjusts user limit (+l channel mode) to prevent join floods.",
+         "TOPICSNARF:   Topics set manually (by /TOPIC #channel ...) change default $C topic",
+         "PEONINVITE:   Indicates whether peons may use the $bINVITEME$b command.",
++        "SUCCESSOR:     Determines if a coowner gets ownership of a channel when the owner's handle expires.",
+         "$bIRCOP ONLY$b:",
+         "NODELETE:  Prevents channel deletion.",
+         "$uSee Also:$u set pubcmd, set strictop, set autoop, set enfmodes, set enftopic, set protect, set toys, set setters, set topicrefresh, set ctcpusers, set ctcpreaction");
+@@ -427,6 +428,8 @@
+         "$b2$b  Short timed ban (defaults to 3 minutes) on disallowed CTCPs.",
+         "$b3$b  Long timed ban (defaults to 1 hour) on disallowed CTCPs.",
+         "$uSee Also:$u set, set ctcpusers");
++"SET SUCCESSOR" ("/msg $C SET <#channel> SUCCESSOR <nick|*account>",
++        "This is a setting to determine who will be the next owner of a channel when the current owner's handle expires.");
+ "SETINFO" ("/msg $C SETINFO <#channel> <info>",
+         "This command will set a user defined information message to be displayed when you join the channel. By setting the message to '*', you will clear your message.",
+         "$uSee Also:$u access");
diff --git a/rx/COPYING.LIB b/rx/COPYING.LIB
new file mode 100644 (file)
index 0000000..eb685a5
--- /dev/null
@@ -0,0 +1,481 @@
+                 GNU LIBRARY GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1991 Free Software Foundation, Inc.
+                    675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the library GPL.  It is
+ numbered 2 because it goes with version 2 of the ordinary GPL.]
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Library General Public License, applies to some
+specially designated Free Software Foundation software, and to any
+other libraries whose authors decide to use it.  You can use it for
+your libraries, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if
+you distribute copies of the library, or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link a program with the library, you must provide
+complete object files to the recipients so that they can relink them
+with the library, after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  Our method of protecting your rights has two steps: (1) copyright
+the library, and (2) offer you this license which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  Also, for each distributor's protection, we want to make certain
+that everyone understands that there is no warranty for this free
+library.  If the library is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original
+version, so that any problems introduced by others will not reflect on
+the original authors' reputations.
+\f
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that companies distributing free
+software will individually obtain patent licenses, thus in effect
+transforming the program into proprietary software.  To prevent this,
+we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+  Most GNU software, including some libraries, is covered by the ordinary
+GNU General Public License, which was designed for utility programs.  This
+license, the GNU Library General Public License, applies to certain
+designated libraries.  This license is quite different from the ordinary
+one; be sure to read it in full, and don't assume that anything in it is
+the same as in the ordinary license.
+
+  The reason we have a separate public license for some libraries is that
+they blur the distinction we usually make between modifying or adding to a
+program and simply using it.  Linking a program with a library, without
+changing the library, is in some sense simply using the library, and is
+analogous to running a utility program or application program.  However, in
+a textual and legal sense, the linked executable is a combined work, a
+derivative of the original library, and the ordinary General Public License
+treats it as such.
+
+  Because of this blurred distinction, using the ordinary General
+Public License for libraries did not effectively promote software
+sharing, because most developers did not use the libraries.  We
+concluded that weaker conditions might promote sharing better.
+
+  However, unrestricted linking of non-free programs would deprive the
+users of those programs of all benefit from the free status of the
+libraries themselves.  This Library General Public License is intended to
+permit developers of non-free programs to use free libraries, while
+preserving your freedom as a user of such programs to change the free
+libraries that are incorporated in them.  (We have not seen how to achieve
+this as regards changes in header files, but we have achieved it as regards
+changes in the actual functions of the Library.)  The hope is that this
+will lead to faster development of free libraries.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, while the latter only
+works together with the library.
+
+  Note that it is possible for a library to be covered by the ordinary
+General Public License rather than by this special one.
+\f
+                 GNU LIBRARY GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library which
+contains a notice placed by the copyright holder or other authorized
+party saying it may be distributed under the terms of this Library
+General Public License (also called "this License").  Each licensee is
+addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also compile or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    c) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    d) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the source code distributed need not include anything that is normally
+distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. 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 not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Library 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
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                           NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+     Appendix: How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library 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
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public
+    License along with this library; if not, write to the Free
+    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/rx/ChangeLog b/rx/ChangeLog
new file mode 100644 (file)
index 0000000..f5b002d
--- /dev/null
@@ -0,0 +1,278 @@
+Wed Jan 15 12:00:38 1997  Tom Lord  <lord@rizla.lanminds.com>
+
+       * rxsuper.c (rx_superset_cons): reference count tweak.
+
+       * rxnode.c (rx_rexp_equal): fixed test for equality of 
+       interval expressions.
+
+       * rxgnucomp.h (enum RE_SYNTAX_BITS): turned the syntax
+       bits from "#define" into "enum" to ease debugging.
+
+Mon Jan 13 10:07:39 1997  Tom Lord  <lord@rizla.lanminds.com>
+
+       * rxsuper.c (rx_superset_cons):
+       While hash_store will protect cdr itself it might first allocate hash
+        tables and stuff which might cause it to be garbage collected before
+        it's protected -- (from Greg Stark)
+
+       * rxgnucomp.c (isa_blank): Test for ==, not != '\t'.
+       (from Andreas Schwab)
+
+Tue Dec  3 00:33:27 1996  Tom Lord  <lord@rizla.lanminds.com>
+
+       * rxposix.c (regnexec): When testing to consider freeing REGS,
+       watch out for PMATCH == NULL.
+
+       * rxspencer.c (rx_next_solution): In case r_parens: Before trying
+       to match a parenthesized subexpression, restore the corresponding
+       regs to their value prior to attempting the match.  If the match
+       finally fails, be sure sure to restore the old values then, too.
+
+Mon Dec  2 00:52:06 1996  Tom Lord  <lord@rizla.lanminds.com>
+
+       * rxspencer.c (rx_next_solution): After "star_try_next_left_match:"...
+       Only return yes from a star expression whose subexpression fails if
+       the target string has 0 length.
+
+       * rxposix.c (regnexec, regncomp): reversed the order of
+       the string and string-length arguments to be more like
+       other functions (e.g. strncmp). (Suggested by Mike Haertel)
+
+       * inst-rxposix.h, rxgnucomp.h (REG_E*): moved declarations
+       of POSIX error codes into the posix header file.
+
+       * rxgnucomp.c (rx_parse): Don't permit a backreference to
+       an enclosing subexpression.  This change returns some code that
+       was bogusly deleted somewhere along the line.  This fixes
+       a bug that causes a pattern such as: ((.*)\1)x to core dump.
+       (Reported by Mike Haertel)
+       
+Sun Nov 24 04:24:13 1996  Tom Lord  <lord@rizla.lanminds.com>
+
+       * rxposix.c (rx_regexec): Added a new optimization
+       that generalizes the fastmap.  The new optimization is applied
+       if the length of the string exceeds RX_MANY_CASES.
+
+Fri Nov  8 09:07:14 1996  Tom Lord  <lord@rizla.lanminds.com>
+
+       * rxsuper.h (RX_DEFAULT_DFA_CACHE_SIZE): 
+       * rxbasic.h (RX_DEFAULT_NFA_DELAY): 
+       New macros so these values can be set at compile time.
+
+Tue Nov  5 09:37:03 1996  Tom Lord  <lord@rizla.lanminds.com>
+
+       * rxspencer.c (rx_make_solutions): watch out for solns->exp == NULL.
+
+       Eric Johnson (johnsone@uiuc.edu) detected this bug and
+       also performed useful testing of Rx memory management.
+
+Tue Jun 18 11:44:46 1996  Tom Lord  <lord@beehive>
+
+       * rxanal.c (rx_start_superstate): Don't release an old superstate
+       unless it is known that the new superstate has been successfully
+       constructed.
+
+Thu Jun 13 11:18:25 1996  Tom Lord  <lord@beehive>
+
+       * rxspencer.c etc. (rx_next_solution et al.): remove all traces of rx_maybe
+
+Wed May 22 12:28:22 1996  Tom Lord  <lord@beehive>
+
+       * rxanal.c (rx_start_superstate): Preserve the invariant
+       that a locked superstate is never semifree.
+
+Fri May 17 10:21:26 1996  Tom Lord  <lord@beehive>
+
+       * rgx.c (scm_regexec): added match data support for
+       "#\c" -- the final_tag of the match (for the cut operator).
+
+       * rxspencer.c (rx_next_solution): propogate is_final data
+       up through the tree of solution streams.
+
+       * rxnfa.h (struct rx_nfa_state): 
+       unsigned int is_final:1 => int is_final for the cut
+       operator.
+
+       * rxanal.c (rx_match_here_p): 
+       * rxanal.c (rx_fit_p): 
+       * rxanal.c (rx_longest): When a final state is detected,
+       propogate the value of the is_final flag back to the
+       caller.  It may contain data generated by a "cut" operator.
+
+       * rxsuper.c (superset_allocator): when marking a superset
+       final, mark it with the maximum of the is_final fields
+       of the constituent nfa states (for the "cut" operator which
+       allows users to set that value).
+
+       * rxgnucomp.c (rx_parse): Replace "[[:set...:]]" with
+       "[[:cut n:]]".  cut is regular but set is not, so cut
+       leads to much faster running patterns.
+
+       * rxnfa.c (rx_build_nfa): compile r_cut nodes.   r_cut
+       nodes match the empty string and nothing more.  A parameter
+       to the cut node determines whether the empty match leads
+       to a final state, or to a failure.
+
+       * rx.c (rx_free_rx): 
+       * rxsuper.c (release_superset_low): 
+       * rxanal.c (rx_start_superstate): fixed the test for a cached
+       starting superset to reflect the simplified memory management
+       of `struct rx' (they are now explicitly freed using rx_free_rx)
+       and `struct rx_superset' (they are now ultimately freed using
+       free and not kept on a free-list).   Now the `start_set' field
+       of a `struct rx' is only non-0 if it is valid.
+
+Tue May 14 08:56:22 1996  Tom Lord  <lord@beehive>
+
+       * rxspencer.h (typedef rx_contextfn): take an entire expression
+       tree instead of just a context type since for some context types,
+       parameters in the tree matter ([[:set...:]])
+
+       * rxstr.c (rx_str_contextfn): handle [[:set...:]] operator.
+
+       * rxgnucomp.c (rx_parse): added the [[:set n = x:]] construct
+       to make it easier to lex using regexps.
+
+       * rxposix.c (regnexec): 
+       "This pattern (with 10 subexpressions and 9 backreferences) made no entries
+       in a match array of size 5." (from doug@plan9.att.com)
+
+       * rxgnucomp.c (rx_parse): new compilation state variable:
+       
+       last_non_regular_expression When compiling, keep track of two, not
+       one point in the tree for concatenating new nodes.  The
+       *last_non_regular_expression point is always the same as the
+       *last_expression or is a parent of that node.  Concatenations of
+       regular constructs happen at last_expression, others at
+       last_non_regular_expression.  The resulting trees have
+       "observable" constructs clustered near the root of the tree which
+       allows those optimizations that apply only to regular subtrees to
+       have a greater impact on overall performance.
+       
+
+       * rxspencer.c (rx_next_solution): interval satisfaction test was wrong.
+
+       * rxanal.c (rx_posix_analyze_rexp): An interval is always observed (not truly
+       a regular expression).
+
+       * rxstr.c (rx_str_contextfn): "when you're doing
+       back-reference matching case insensitively (with REG_ICASE set), 
+       you are supposed to also do the BR matching without paying attention
+       to case.
+
+Mon May 13 09:59:48 1996  Tom Lord  <lord@beehive>
+
+       * rxspencer.c (rx_next_solution): Don't construct
+       an NFA when comparing an r_string to some text -- 
+       just do a strcmp-like operation.
+
+       * rxgnucomp.c (rx_parse): new variable: n_members
+       An array keeping track of the size of csets generated 
+       by inverting the translation table.
+
+       (rx_parse): validate_inv_tr and n_members were way to big --
+       each only needs CHAR_SET_SIZE elements.
+
+Mon May 13 09:29:42 1996  Zachary Weinberg <zaw2@rabi.phys.columbia.edu>
+
+       * rxnode.c (rx_init_string): New data structure for strings -- 
+       part of the overall support for constant string optimization.
+
+       * rxnode.c (rx_mk_r_str etc.): a new type of rexp-node --
+       an abbreviation for a concatenation of characters.
+
+       * rxdbug.c (print_rexp): Added support for printing r_str nodes.
+
+       * rxgnucomp.c (rx_parse): initial support for constant strings.
+
+
+
+Wed Jan 31 19:59:46 1996  Preston L. Bannister  <pbannister@ca.mdis.com>
+
+       Changes to compile clean under MSVC 4.0 (w/o warnings).
+       Added makefile for MSVC 4.0 (librx.mak).
+
+       [! Changes marked *** were made differently from the submitted
+          patches -- the descriptions may not apply exactly.]
+
+       hashrexp.c:     Added __STDC__ variant of function definition.
+       ***     rxall.h:        Pull in standard C header files.  
+       ***                     Map bzero() to memset().
+       rxanal.c:       Remove unused variable.
+       rxdbug.c:       Added stdio include.
+       rxhash.c:       Remove unused variable.
+       rxnfa.c:        Remove {re,m}alloc definition.
+       rxposix.c:      Remove unused variable.
+       ***             Cast parameter nmatch declared as size_t to int on use.
+       ***             Perhaps nmatch should be passed as int?
+                       [made related variables size_t]
+       rxspencer.c:    Add rxsimp.h include.
+                       Remove unused variables and labels.
+       rxunfa.c:       Remove unused variable.
+                       
+
+Tue Jan 30 10:29:16 1996  Tom Lord  <lord@beehive>
+
+       * rxsimp.c (rx_simple_rexp): move assignment out of if.
+       ("Preston L. Bannister" <preston@speed.net>)
+
+       * Makefile.in (CFLAGS, ALL_CFLAGS): rearranged to allow user
+       specified CFLAGS.
+
+       * rxposix.h: comment stuff after #endif.
+       (reported by Eric Backus <ericb@lsid.hp.com>)
+
+Mon Jan  1 13:03:28 1996  Jason Molenda  (crash@phydeaux.cygnus.com)
+
+        * rxbasic.c (rx_basic_make_solutions): argument called 'rexp' is
+        now called 'expression'.  Argument 'str' should be unsigned char.
+
+        * rxbasic.h (rx_basic_make_solutions): argument 'str' should be
+        unsigned char.
+
+        * rxsuper.h (rx_handle_cache_miss, rx_superstate_eclosure_union): 
+        syntax error in prototypes. [Actually fixed in rxsuper.c, from which
+       that section of rxsuper.h is derived.]
+
+        * rxnode.c (rx_mk_r_cset): fix function decl.
+
+Tue Jan 30 09:43:28 1996  Tom Lord  <lord@beehive>
+
+       * rxposix.c (regnexec): pass rx_regexec "regs", not "pmatch".
+       "regs" is valid even if "pmatch" is NULL.
+       (Fixes testsuite bug "pragma" reported by John.Szetela@amd.com (John J. Szetela)
+       also fixes bug reported by Jongki Suwandi <jongkis@loc1.tandem.com>)
+
+Fri Jan 26 14:23:20 1996  Tom Lord  <lord@beehive>
+
+       * rxdbug.c (AT): Use the GCC feature only if HAVE_POSITIONAL_ARRAY_INITS
+       is defined.
+
+       * Makefile.in: Fixed depends target to not include system
+       header files.  Use @exec_prefix@. (Derek Clegg <Derek_Clegg@next.com>)
+
+Thu Jan  4 16:13:07 1996  Tom Lord  <lord@beehive>
+
+       * rxposix.c (rx_regexec): Don't bother checking to see if an
+       anchored pattern matches other than at the beginning of a string.
+       
+       (rx_regmatch): Don't bother looking for matches that are the
+       wrong length if the overall length of the expression is known.
+       This duplicates an optimization already in rx_make_solutions and
+       rx_basic_make_solutions, but its worth it.  The make_solutions
+       optimization applies to fixed length subexpressions of a variable
+       length expression.  The regmatch optimization can avoid (in sed,
+       for example) many, many uneeded calls to make_solutions and
+       rx_next_solution.
+
+       * rxspencer.c (rx_make_solutions, rx_basic_make_solutions): If the
+       expression is fixed length and that length doesn't match the
+       buffer, don't bother constructing a new solution stream -- just
+       return the canonical "no solution" stream.
+
+
+Sat Dec 30 21:19:31 1995  Tom Lord  <lord@beehive>
+
+       * *.[ch]: posixification and algorithmic improvement (thanks
+       henry!).
+
diff --git a/rx/Makefile.am b/rx/Makefile.am
new file mode 100644 (file)
index 0000000..5275504
--- /dev/null
@@ -0,0 +1,24 @@
+# this is "dirty" code -- please don't compile with -Wall :)
+AM_CFLAGS = -W
+noinst_LIBRARIES = librx.a
+
+librx_a_SOURCES = \
+       hashrexp.c \
+       _rx.h rx.c rx.h \
+       rxanal.c rxanal.h \
+       rxbasic.c rxbasic.h \
+       rxbitset.c rxbitset.h \
+       rxcontext.h \
+       rxcset.c rxcset.h \
+       rxgnucomp.c rxgnucomp.h \
+       rxhash.c rxhash.h \
+       rxnfa.c rxnfa.h \
+       rxnode.c rxnode.h \
+       rxposix.c inst-rxposix.h rxposix.h \
+       rxproto.h \
+       rxsimp.c rxsimp.h \
+       rxspencer.c rxspencer.h \
+       rxstr.c rxstr.h \
+       rxsuper.c rxsuper.h \
+       rxunfa.c rxunfa.h \
+       rxall.h
diff --git a/rx/Makefile.in b/rx/Makefile.in
new file mode 100644 (file)
index 0000000..0989f33
--- /dev/null
@@ -0,0 +1,431 @@
+# Makefile.in generated by automake 1.7.9 from Makefile.am.
+# @configure_input@
+
+# Copyright 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003
+# Free Software Foundation, Inc.
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+srcdir = @srcdir@
+top_srcdir = @top_srcdir@
+VPATH = @srcdir@
+pkgdatadir = $(datadir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+top_builddir = ..
+
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+INSTALL = @INSTALL@
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+host_triplet = @host@
+ACLOCAL = @ACLOCAL@
+ALLOCA = @ALLOCA@
+AMDEP_FALSE = @AMDEP_FALSE@
+AMDEP_TRUE = @AMDEP_TRUE@
+AMTAR = @AMTAR@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+MAINT = @MAINT@
+MAINTAINER_MODE_FALSE = @MAINTAINER_MODE_FALSE@
+MAINTAINER_MODE_TRUE = @MAINTAINER_MODE_TRUE@
+MAKEINFO = @MAKEINFO@
+MODULE_OBJS = @MODULE_OBJS@
+MY_SUBDIRS = @MY_SUBDIRS@
+OBJEXT = @OBJEXT@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+RANLIB = @RANLIB@
+RX_INCLUDES = @RX_INCLUDES@
+RX_LIBS = @RX_LIBS@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+STRIP = @STRIP@
+VERSION = @VERSION@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_RANLIB = @ac_ct_RANLIB@
+ac_ct_STRIP = @ac_ct_STRIP@
+am__fastdepCC_FALSE = @am__fastdepCC_FALSE@
+am__fastdepCC_TRUE = @am__fastdepCC_TRUE@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+datadir = @datadir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+oldincludedir = @oldincludedir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sysconfdir = @sysconfdir@
+target = @target@
+target_alias = @target_alias@
+target_cpu = @target_cpu@
+target_os = @target_os@
+target_vendor = @target_vendor@
+
+# this is "dirty" code -- please don't compile with -Wall :)
+AM_CFLAGS = -W
+noinst_LIBRARIES = librx.a
+
+librx_a_SOURCES = \
+       hashrexp.c \
+       _rx.h rx.c rx.h \
+       rxanal.c rxanal.h \
+       rxbasic.c rxbasic.h \
+       rxbitset.c rxbitset.h \
+       rxcontext.h \
+       rxcset.c rxcset.h \
+       rxgnucomp.c rxgnucomp.h \
+       rxhash.c rxhash.h \
+       rxnfa.c rxnfa.h \
+       rxnode.c rxnode.h \
+       rxposix.c inst-rxposix.h rxposix.h \
+       rxproto.h \
+       rxsimp.c rxsimp.h \
+       rxspencer.c rxspencer.h \
+       rxstr.c rxstr.h \
+       rxsuper.c rxsuper.h \
+       rxunfa.c rxunfa.h \
+       rxall.h
+
+subdir = rx
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+mkinstalldirs = $(SHELL) $(top_srcdir)/mkinstalldirs
+CONFIG_HEADER = $(top_builddir)/src/config.h
+CONFIG_CLEAN_FILES =
+LIBRARIES = $(noinst_LIBRARIES)
+
+librx_a_AR = $(AR) cru
+librx_a_LIBADD =
+am_librx_a_OBJECTS = hashrexp.$(OBJEXT) rx.$(OBJEXT) rxanal.$(OBJEXT) \
+       rxbasic.$(OBJEXT) rxbitset.$(OBJEXT) rxcset.$(OBJEXT) \
+       rxgnucomp.$(OBJEXT) rxhash.$(OBJEXT) rxnfa.$(OBJEXT) \
+       rxnode.$(OBJEXT) rxposix.$(OBJEXT) rxsimp.$(OBJEXT) \
+       rxspencer.$(OBJEXT) rxstr.$(OBJEXT) rxsuper.$(OBJEXT) \
+       rxunfa.$(OBJEXT)
+librx_a_OBJECTS = $(am_librx_a_OBJECTS)
+
+DEFAULT_INCLUDES =  -I. -I$(srcdir) -I$(top_builddir)/src
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__depfiles_maybe = depfiles
+@AMDEP_TRUE@DEP_FILES = ./$(DEPDIR)/hashrexp.Po ./$(DEPDIR)/rx.Po \
+@AMDEP_TRUE@   ./$(DEPDIR)/rxanal.Po ./$(DEPDIR)/rxbasic.Po \
+@AMDEP_TRUE@   ./$(DEPDIR)/rxbitset.Po ./$(DEPDIR)/rxcset.Po \
+@AMDEP_TRUE@   ./$(DEPDIR)/rxgnucomp.Po ./$(DEPDIR)/rxhash.Po \
+@AMDEP_TRUE@   ./$(DEPDIR)/rxnfa.Po ./$(DEPDIR)/rxnode.Po \
+@AMDEP_TRUE@   ./$(DEPDIR)/rxposix.Po ./$(DEPDIR)/rxsimp.Po \
+@AMDEP_TRUE@   ./$(DEPDIR)/rxspencer.Po ./$(DEPDIR)/rxstr.Po \
+@AMDEP_TRUE@   ./$(DEPDIR)/rxsuper.Po ./$(DEPDIR)/rxunfa.Po
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+       $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+CCLD = $(CC)
+LINK = $(CCLD) $(AM_CFLAGS) $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+DIST_SOURCES = $(librx_a_SOURCES)
+DIST_COMMON = $(srcdir)/Makefile.in COPYING.LIB ChangeLog Makefile.am \
+       compile depcomp
+SOURCES = $(librx_a_SOURCES)
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ Makefile.am  $(top_srcdir)/configure.in $(ACLOCAL_M4)
+       cd $(top_srcdir) && \
+         $(AUTOMAKE) --gnu  rx/Makefile
+Makefile: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.in  $(top_builddir)/config.status
+       cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)
+
+AR = ar
+
+clean-noinstLIBRARIES:
+       -test -z "$(noinst_LIBRARIES)" || rm -f $(noinst_LIBRARIES)
+librx.a: $(librx_a_OBJECTS) $(librx_a_DEPENDENCIES) 
+       -rm -f librx.a
+       $(librx_a_AR) librx.a $(librx_a_OBJECTS) $(librx_a_LIBADD)
+       $(RANLIB) librx.a
+
+mostlyclean-compile:
+       -rm -f *.$(OBJEXT) core *.core
+
+distclean-compile:
+       -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hashrexp.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rx.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxanal.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxbasic.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxbitset.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxcset.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxgnucomp.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxhash.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxnfa.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxnode.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxposix.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxsimp.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxspencer.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxstr.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxsuper.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rxunfa.Po@am__quote@
+
+.c.o:
+@am__fastdepCC_TRUE@   if $(COMPILE) -MT $@ -MD -MP -MF "$(DEPDIR)/$*.Tpo" \
+@am__fastdepCC_TRUE@     -c -o $@ `test -f '$<' || echo '$(srcdir)/'`$<; \
+@am__fastdepCC_TRUE@   then mv -f "$(DEPDIR)/$*.Tpo" "$(DEPDIR)/$*.Po"; \
+@am__fastdepCC_TRUE@   else rm -f "$(DEPDIR)/$*.Tpo"; exit 1; \
+@am__fastdepCC_TRUE@   fi
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      depfile='$(DEPDIR)/$*.Po' tmpdepfile='$(DEPDIR)/$*.TPo' @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@  $(COMPILE) -c `test -f '$<' || echo '$(srcdir)/'`$<
+
+.c.obj:
+@am__fastdepCC_TRUE@   if $(COMPILE) -MT $@ -MD -MP -MF "$(DEPDIR)/$*.Tpo" \
+@am__fastdepCC_TRUE@     -c -o $@ `if test -f '$<'; then $(CYGPATH_W) '$<'; else $(CYGPATH_W) '$(srcdir)/$<'; fi`; \
+@am__fastdepCC_TRUE@   then mv -f "$(DEPDIR)/$*.Tpo" "$(DEPDIR)/$*.Po"; \
+@am__fastdepCC_TRUE@   else rm -f "$(DEPDIR)/$*.Tpo"; exit 1; \
+@am__fastdepCC_TRUE@   fi
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      depfile='$(DEPDIR)/$*.Po' tmpdepfile='$(DEPDIR)/$*.TPo' @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@  $(COMPILE) -c `if test -f '$<'; then $(CYGPATH_W) '$<'; else $(CYGPATH_W) '$(srcdir)/$<'; fi`
+uninstall-info-am:
+
+ETAGS = etags
+ETAGSFLAGS =
+
+CTAGS = ctags
+CTAGSFLAGS =
+
+tags: TAGS
+
+ID: $(HEADERS) $(SOURCES) $(LISP) $(TAGS_FILES)
+       list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \
+       unique=`for i in $$list; do \
+           if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+         done | \
+         $(AWK) '    { files[$$0] = 1; } \
+              END { for (i in files) print i; }'`; \
+       mkid -fID $$unique
+
+TAGS:  $(HEADERS) $(SOURCES)  $(TAGS_DEPENDENCIES) \
+               $(TAGS_FILES) $(LISP)
+       tags=; \
+       here=`pwd`; \
+       list='$(SOURCES) $(HEADERS)  $(LISP) $(TAGS_FILES)'; \
+       unique=`for i in $$list; do \
+           if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+         done | \
+         $(AWK) '    { files[$$0] = 1; } \
+              END { for (i in files) print i; }'`; \
+       test -z "$(ETAGS_ARGS)$$tags$$unique" \
+         || $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+            $$tags $$unique
+
+ctags: CTAGS
+CTAGS:  $(HEADERS) $(SOURCES)  $(TAGS_DEPENDENCIES) \
+               $(TAGS_FILES) $(LISP)
+       tags=; \
+       here=`pwd`; \
+       list='$(SOURCES) $(HEADERS)  $(LISP) $(TAGS_FILES)'; \
+       unique=`for i in $$list; do \
+           if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+         done | \
+         $(AWK) '    { files[$$0] = 1; } \
+              END { for (i in files) print i; }'`; \
+       test -z "$(CTAGS_ARGS)$$tags$$unique" \
+         || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+            $$tags $$unique
+
+GTAGS:
+       here=`$(am__cd) $(top_builddir) && pwd` \
+         && cd $(top_srcdir) \
+         && gtags -i $(GTAGS_ARGS) $$here
+
+distclean-tags:
+       -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+
+top_distdir = ..
+distdir = $(top_distdir)/$(PACKAGE)-$(VERSION)
+
+distdir: $(DISTFILES)
+       @srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; \
+       topsrcdirstrip=`echo "$(top_srcdir)" | sed 's|.|.|g'`; \
+       list='$(DISTFILES)'; for file in $$list; do \
+         case $$file in \
+           $(srcdir)/*) file=`echo "$$file" | sed "s|^$$srcdirstrip/||"`;; \
+           $(top_srcdir)/*) file=`echo "$$file" | sed "s|^$$topsrcdirstrip/|$(top_builddir)/|"`;; \
+         esac; \
+         if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+         dir=`echo "$$file" | sed -e 's,/[^/]*$$,,'`; \
+         if test "$$dir" != "$$file" && test "$$dir" != "."; then \
+           dir="/$$dir"; \
+           $(mkinstalldirs) "$(distdir)$$dir"; \
+         else \
+           dir=''; \
+         fi; \
+         if test -d $$d/$$file; then \
+           if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+             cp -pR $(srcdir)/$$file $(distdir)$$dir || exit 1; \
+           fi; \
+           cp -pR $$d/$$file $(distdir)$$dir || exit 1; \
+         else \
+           test -f $(distdir)/$$file \
+           || cp -p $$d/$$file $(distdir)/$$file \
+           || exit 1; \
+         fi; \
+       done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LIBRARIES)
+
+installdirs:
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+       @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+       $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+         install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+         `test -z '$(STRIP)' || \
+           echo "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'"` install
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+       -rm -f $(CONFIG_CLEAN_FILES)
+
+maintainer-clean-generic:
+       @echo "This command is intended for maintainers to use"
+       @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-noinstLIBRARIES mostlyclean-am
+
+distclean: distclean-am
+       -rm -rf ./$(DEPDIR)
+       -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+       distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-exec-am:
+
+install-info: install-info-am
+
+install-man:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+       -rm -rf ./$(DEPDIR)
+       -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-info-am
+
+.PHONY: CTAGS GTAGS all all-am check check-am clean clean-generic \
+       clean-noinstLIBRARIES ctags distclean distclean-compile \
+       distclean-generic distclean-tags distdir dvi dvi-am info \
+       info-am install install-am install-data install-data-am \
+       install-exec install-exec-am install-info install-info-am \
+       install-man install-strip installcheck installcheck-am \
+       installdirs maintainer-clean maintainer-clean-generic \
+       mostlyclean mostlyclean-compile mostlyclean-generic pdf pdf-am \
+       ps ps-am tags uninstall uninstall-am uninstall-info-am
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/rx/_rx.h b/rx/_rx.h
new file mode 100644 (file)
index 0000000..2c982a2
--- /dev/null
+++ b/rx/_rx.h
@@ -0,0 +1,176 @@
+/* classes: h_files */
+
+#ifndef _RXH
+#define _RXH
+
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include <sys/types.h>
+#include "rxhash.h"
+#include "rxcset.h"
+
+\f
+
+struct rx_cache;
+struct rx_superset;
+struct rx;
+struct rx_se_list;
+
+\f
+
+/* Suppose that from some NFA state and next character, more than one
+ * path through side-effect edges is possible.  In what order should
+ * the paths be tried?  A function of type rx_se_list_order answers
+ * that question.  It compares two lists of side effects, and says
+ * which list comes first.
+ */
+#ifdef __STDC__
+typedef int (*rx_se_list_order) (struct rx *,
+                                struct rx_se_list *, 
+                                struct rx_se_list *);
+#else
+typedef int (*rx_se_list_order) ();
+#endif
+
+
+
+/* Struct RX holds an NFA and cache state for the corresponding super NFA.
+ */
+struct rx
+{
+  /* The compiler assigns a unique id to every pattern.
+   * Like sequence numbers in X, there is a subtle bug here
+   * if you use Rx in a system that runs for a long time.
+   * But, because of the way the caches work out, it is almost
+   * impossible to trigger the Rx version of this bug.
+   *
+   * The id is used to validate superstates found in a cache
+   * of superstates.  It isn't sufficient to let a superstate
+   * point back to the rx for which it was compiled -- the caller
+   * may be re-using a `struct rx' in which case the superstate
+   * is not really valid.  So instead, superstates are validated
+   * by checking the sequence number of the pattern for which
+   * they were built.
+   */
+  int rx_id;
+
+  /* This is memory mgt. state for superstates.  This may be 
+   * shared by more than one struct rx.
+   */
+  struct rx_cache * cache;
+
+  /* Every nfa defines the size of its own character set. 
+   * Each superstate has an array of this size, with each element
+   * a `struct rx_inx'.  So, don't make this number too large.
+   * In particular, don't make it 2^16.
+   */
+  int local_cset_size;
+
+  /* Lists of side effects as stored in the NFA are `hash consed'..meaning
+   * that lists with the same elements are ==.  During compilation, 
+   * this table facilitates hash-consing.
+   */
+  struct rx_hash se_list_memo;
+
+  /* Lists of NFA states are also hashed. 
+   */
+  struct rx_hash set_list_memo;
+
+
+
+  /* The compiler and matcher must build a number of instruction frames.
+   * The format of these frames is fixed (c.f. struct rx_inx).  The values
+   * of the instruction opcodes is not fixed.
+   *
+   * An enumerated type (enum rx_opcode) defines the set of instructions
+   * that the compiler or matcher might generate.  When filling an instruction
+   * frame, the INX field is found by indexing this instruction table
+   * with an opcode:
+   */
+  void ** instruction_table;
+
+  /* The list of all states in an NFA.
+   * The NEXT field of NFA states links this list.
+   */
+  struct rx_nfa_state *nfa_states;
+  struct rx_nfa_state *start_nfa_states;
+  struct rx_superset * start_set;
+
+  /* This orders the search through super-nfa paths.
+   * See the comment near the typedef of rx_se_list_order.
+   */
+  rx_se_list_order se_list_cmp;
+
+  int next_nfa_id;
+};
+
+
+
+/* If `regs_allocated' is REGS_UNALLOCATED in the pattern buffer,
+ * `re_match_2' returns information about at least this many registers
+ * the first time a `regs' structure is passed. 
+ *
+ * Also, this is the greatest number of backreferenced subexpressions
+ * allowed in a pattern being matched without caller-supplied registers.
+ */
+#ifndef RE_NREGS
+#define RE_NREGS 30
+#endif
+
+
+#ifndef emacs
+#define CHARBITS 8
+#define CHAR_SET_SIZE (1 << CHARBITS)
+#define Sword 1
+#define SYNTAX(c) re_syntax_table[c]
+extern char re_syntax_table[CHAR_SET_SIZE];
+#endif
+
+
+/* Should we use malloc or alloca?  If REGEX_MALLOC is not defined, we
+ * use `alloca' instead of `malloc' for the backtracking stack.
+ *
+ * Emacs will die miserably if we don't do this.
+ */
+
+#ifdef REGEX_MALLOC
+#define REGEX_ALLOCATE malloc
+#else /* not REGEX_MALLOC  */
+#define REGEX_ALLOCATE alloca
+#endif /* not REGEX_MALLOC */
+
+#undef MAX
+#undef MIN
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+extern void * rx_id_instruction_table[];
+extern struct rx_cache * rx_default_cache;
+
+\f
+#ifdef __STDC__
+
+#else /* STDC */
+
+#endif /* STDC */
+
+
+#endif  /* _RXH */
diff --git a/rx/compile b/rx/compile
new file mode 100755 (executable)
index 0000000..d4a34aa
--- /dev/null
@@ -0,0 +1,82 @@
+#! /bin/sh
+
+# Wrapper for compilers which do not understand `-c -o'.
+
+# Copyright 1999, 2000 Free Software Foundation, Inc.
+# Written by Tom Tromey <tromey@cygnus.com>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# Usage:
+# compile PROGRAM [ARGS]...
+# `-o FOO.o' is removed from the args passed to the actual compile.
+
+prog=$1
+shift
+
+ofile=
+cfile=
+args=
+while test $# -gt 0; do
+   case "$1" in
+    -o)
+       ofile=$2
+       shift
+       ;;
+    *.c)
+       cfile=$1
+       args="$args $1"
+       ;;
+    *)
+       args="$args $1"
+       ;;
+   esac
+   shift
+done
+
+test -z "$ofile" && {
+   echo "compile: no \`-o' option seen" 1>&2
+   exit 1
+}
+
+test -z "$cfile" && {
+   echo "compile: no \`.c' file seen" 1>&2
+   exit 1
+}
+
+# Name of file we expect compiler to create.
+cofile=`echo $cfile | sed -e 's|^.*/||' -e 's/\.c$/.o/'`
+
+# Create the lock directory.
+lockdir=`echo $ofile | sed -e 's|/|_|g'`
+while true; do
+   if mkdir $lockdir > /dev/null 2>&1; then
+      break
+   fi
+   sleep 1
+done
+# FIXME: race condition here if user kills between mkdir and trap.
+trap "rmdir $lockdir; exit 1" 1 2 15
+
+# Run the compile.
+"$prog" $args
+status=$?
+
+if test -f "$cofile"; then
+   mv "$cofile" "$ofile"
+fi
+
+rmdir $lockdir
+exit $status
diff --git a/rx/depcomp b/rx/depcomp
new file mode 100755 (executable)
index 0000000..3014997
--- /dev/null
@@ -0,0 +1,336 @@
+#! /bin/sh
+
+# depcomp - compile a program generating dependencies as side-effects
+# Copyright 1999, 2000 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+
+# This program 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 General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+# 02111-1307, USA.
+
+# Originally written by Alexandre Oliva <oliva@dcc.unicamp.br>.
+
+if test -z "$depmode" || test -z "$source" || test -z "$object"; then
+  echo "depcomp: Variables source, object and depmode must be set" 1>&2
+  exit 1
+fi
+# `libtool' can also be set to `yes' or `no'.
+
+depfile=${depfile-`echo "$object" | sed 's,\([^/]*\)$,.deps/\1,;s/\.\([^.]*\)$/.P\1/'`}
+tmpdepfile=${tmpdepfile-`echo "$depfile" | sed 's/\.\([^.]*\)$/.T\1/'`}
+
+rm -f "$tmpdepfile"
+
+# Some modes work just like other modes, but use different flags.  We
+# parameterize here, but still list the modes in the big case below,
+# to make depend.m4 easier to write.  Note that we *cannot* use a case
+# here, because this file can only contain one case statement.
+if test "$depmode" = hp; then
+  # HP compiler uses -M and no extra arg.
+  gccflag=-M
+  depmode=gcc
+fi
+
+if test "$depmode" = dashXmstdout; then
+   # This is just like dashmstdout with a different argument.
+   dashmflag=-xM
+   depmode=dashmstdout
+fi
+
+case "$depmode" in
+gcc3)
+## gcc 3 implements dependency tracking that does exactly what
+## we want.  Yay!
+   if "$@" -MT "$object" -MF "$depfile" -MD -MP; then
+      exit $?
+   fi
+   ;;
+
+gcc)
+## There are various ways to get dependency output from gcc.  Here's
+## why we pick this rather obscure method:
+## - Don't want to use -MD because we'd like the dependencies to end
+##   up in a subdir.  Having to rename by hand is ugly.
+##   (We might end up doing this anyway to support other compilers.)
+## - The DEPENDENCIES_OUTPUT environment variable makes gcc act like
+##   -MM, not -M (despite what the docs say).
+## - Using -M directly means running the compiler twice (even worse
+##   than renaming).
+  if test -z "$gccflag"; then
+    gccflag=-MD,
+  fi
+  if "$@" -Wp,"$gccflag$tmpdepfile"; then :
+  else
+    stat=$?
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  alpha=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
+## The second -e expression handles DOS-style file names with drive letters.
+  sed -e 's/^[^:]*: / /' \
+      -e 's/^['$alpha']:\/[^:]*: / /' < "$tmpdepfile" >> "$depfile"
+## This next piece of magic avoids the `deleted header file' problem.
+## The problem is that when a header file which appears in a .P file
+## is deleted, the dependency causes make to die (because there is
+## typically no way to rebuild the header).  We avoid this by adding
+## dummy dependencies for each header file.  Too bad gcc doesn't do
+## this for us directly.
+  tr ' ' '
+' < "$tmpdepfile" |
+## Some versions of gcc put a space before the `:'.  On the theory
+## that the space means something, we add a space to the output as
+## well.
+## Some versions of the HPUX 10.20 sed can't process this invocation
+## correctly.  Breaking it into two sed invocations is a workaround.
+    sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+hp)
+  # This case exists only to let depend.m4 do its work.  It works by
+  # looking at the text of this script.  This case will never be run,
+  # since it is checked for above.
+  exit 1
+  ;;
+
+dashmd)
+  # The Java front end to gcc doesn't run cpp, so we can't use the -Wp
+  # trick.  Instead we must use -M and then rename the resulting .d
+  # file.  This is also the case for older versions of gcc, which
+  # don't implement -Wp.
+  if "$@" -MD; then :
+  else
+    stat=$?
+    rm -f FIXME
+    exit $stat
+  fi
+  FIXME: rewrite the file
+  ;;
+
+sgi)
+  if test "$libtool" = yes; then
+    "$@" "-Wp,-MDupdate,$tmpdepfile"
+  else
+    "$@" -MDupdate "$tmpdepfile"
+  fi
+  stat=$?
+  if test $stat -eq 0; then :
+  else
+    stat=$?
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  rm -f "$depfile"
+
+  if test -f "$tmpdepfile"; then  # yes, the sourcefile depend on other files
+    echo "$object : \\" > "$depfile"
+
+    # Clip off the initial element (the dependent). Don't try to be
+    # clever and replace this with sed code, as IRIX sed won't handle
+    # lines with more than a fixed number of characters (4096 in
+    # IRIX 6.2 sed, 8192 in IRIX 6.5).
+    tr ' ' '
+' < "$tmpdepfile" | sed 's/^[^\.]*\.o://' | tr '
+' ' ' >> $depfile
+
+    tr ' ' '
+' < "$tmpdepfile" | \
+## Some versions of the HPUX 10.20 sed can't process this invocation
+## correctly.  Breaking it into two sed invocations is a workaround.
+      sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' | sed -e 's/$/ :/' >> "$depfile"
+  else
+    # The sourcefile does not contain any dependencies, so just
+    # store a dummy comment line, to avoid errors with the Makefile
+    # "include basename.Plo" scheme.
+    echo "#dummy" > "$depfile"
+  fi
+  rm -f "$tmpdepfile"
+  ;;
+
+#nosideeffect)
+  # This comment above is used by automake to tell side-effect
+  # dependency tracking mechanisms from slower ones.
+
+dashmstdout)
+  # Important note: in order to support this mode, a compiler *must*
+  # always write the proprocessed file to stdout, regardless of -o,
+  # because we must use -o when running libtool.
+  test -z "$dashmflag" && dashmflag=-M
+  ( IFS=" "
+    case " $* " in
+    *" --mode=compile "*) # this is libtool, let us make it quiet
+      for arg
+      do # cycle over the arguments
+        case "$arg" in
+       "--mode=compile")
+         # insert --quiet before "--mode=compile"
+         set fnord "$@" --quiet
+         shift # fnord
+         ;;
+       esac
+       set fnord "$@" "$arg"
+       shift # fnord
+       shift # "$arg"
+      done
+      ;;
+    esac
+    "$@" $dashmflag | sed 's:^[^:]*\:[         ]*:'"$object"'\: :' > "$tmpdepfile"
+  ) &
+  proc=$!
+  "$@"
+  stat=$?
+  wait "$proc"
+  if test "$stat" != 0; then exit $stat; fi
+  rm -f "$depfile"
+  cat < "$tmpdepfile" > "$depfile"
+  tr ' ' '
+' < "$tmpdepfile" | \
+## Some versions of the HPUX 10.20 sed can't process this invocation
+## correctly.  Breaking it into two sed invocations is a workaround.
+    sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+dashXmstdout)
+  # This case only exists to satisfy depend.m4.  It is never actually
+  # run, as this mode is specially recognized in the preamble.
+  exit 1
+  ;;
+
+makedepend)
+  # X makedepend
+  (
+    shift
+    cleared=no
+    for arg in "$@"; do
+      case $cleared in no)
+        set ""; shift
+       cleared=yes
+      esac
+      case "$arg" in
+        -D*|-I*)
+         set fnord "$@" "$arg"; shift;;
+       -*)
+         ;;
+       *)
+         set fnord "$@" "$arg"; shift;;
+      esac
+    done
+    obj_suffix="`echo $object | sed 's/^.*\././'`"
+    touch "$tmpdepfile"
+    ${MAKEDEPEND-makedepend} 2>/dev/null -o"$obj_suffix" -f"$tmpdepfile" "$@"
+  ) &
+  proc=$!
+  "$@"
+  stat=$?
+  wait "$proc"
+  if test "$stat" != 0; then exit $stat; fi
+  rm -f "$depfile"
+  cat < "$tmpdepfile" > "$depfile"
+  tail +3 "$tmpdepfile" | tr ' ' '
+' | \
+## Some versions of the HPUX 10.20 sed can't process this invocation
+## correctly.  Breaking it into two sed invocations is a workaround.
+    sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile" "$tmpdepfile".bak
+  ;;
+
+cpp)
+  # Important note: in order to support this mode, a compiler *must*
+  # always write the proprocessed file to stdout, regardless of -o,
+  # because we must use -o when running libtool.
+  ( IFS=" "
+    case " $* " in
+    *" --mode=compile "*)
+      for arg
+      do # cycle over the arguments
+        case $arg in
+       "--mode=compile")
+         # insert --quiet before "--mode=compile"
+         set fnord "$@" --quiet
+         shift # fnord
+         ;;
+       esac
+       set fnord "$@" "$arg"
+       shift # fnord
+       shift # "$arg"
+      done
+      ;;
+    esac
+    "$@" -E |
+    sed -n '/^# [0-9][0-9]* "\([^"]*\)".*/ s:: \1 \\:p' |
+    sed '$ s: \\$::' > "$tmpdepfile"
+  ) &
+  proc=$!
+  "$@"
+  stat=$?
+  wait "$proc"
+  if test "$stat" != 0; then exit $stat; fi
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  cat < "$tmpdepfile" >> "$depfile"
+  sed < "$tmpdepfile" '/^$/d;s/^ //;s/ \\$//;s/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+msvisualcpp)
+  # Important note: in order to support this mode, a compiler *must*
+  # always write the proprocessed file to stdout, regardless of -o,
+  # because we must use -o when running libtool.
+  ( IFS=" "
+    case " $* " in
+    *" --mode=compile "*)
+      for arg
+      do # cycle over the arguments
+        case $arg in
+       "--mode=compile")
+         # insert --quiet before "--mode=compile"
+         set fnord "$@" --quiet
+         shift # fnord
+         ;;
+       esac
+       set fnord "$@" "$arg"
+       shift # fnord
+       shift # "$arg"
+      done
+      ;;
+    esac
+    "$@" -E |
+    sed -n '/^#line [0-9][0-9]* "\([^"]*\)"/ s::echo "`cygpath -u \\"\1\\"`":p' | sort | uniq > "$tmpdepfile"
+  ) &
+  proc=$!
+  "$@"
+  stat=$?
+  wait "$proc"
+  if test "$stat" != 0; then exit $stat; fi
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  . "$tmpdepfile" | sed 's% %\\ %g' | sed -n '/^\(.*\)$/ s::   \1 \\:p' >> "$depfile"
+  echo "       " >> "$depfile"
+  . "$tmpdepfile" | sed 's% %\\ %g' | sed -n '/^\(.*\)$/ s::\1\::p' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+none)
+  exec "$@"
+  ;;
+
+*)
+  echo "Unknown depmode $depmode" 1>&2
+  exit 1
+  ;;
+esac
+
+exit 0
diff --git a/rx/hashrexp.c b/rx/hashrexp.c
new file mode 100644 (file)
index 0000000..33c22ba
--- /dev/null
@@ -0,0 +1,50 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include "rxall.h"
+#include "rxnode.h"
+#include "rxhash.h"
+
+#ifdef __STDC__
+static int
+rexp_node_equal (void * va, void * vb)
+#else
+static int
+rexp_node_equal (va, vb)
+     void * va;
+     void * vb;
+#endif
+{
+  struct rexp_node * a;
+  struct rexp_node * b;
+
+  a = (struct rexp_node *)va;
+  b = (struct rexp_node *)vb;
+
+  return (   (va == vb)
+         || (   (a->type == b->type)
+             && (a->params.intval == b->params.intval)
+             && (a->params.intval2 == b->params.intval2)
+             && rx_bitset_is_equal (a->params.cset_size, a->params.cset, b->params.cset)
+             && rexp_node_equal (a->params.pair.left, b->params.pair.left)
+             && rexp_node_equal (a->params.pair.right, b->params.pair.right)));
+}
+
+
diff --git a/rx/inst-rxposix.h b/rx/inst-rxposix.h
new file mode 100644 (file)
index 0000000..b6c6746
--- /dev/null
@@ -0,0 +1,155 @@
+/* classes: h_files */
+
+#ifndef INST_RXPOSIXH
+#define INST_RXPOSIXH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+struct rx_posix_regex
+{
+  struct rexp_node * pattern;
+  struct rexp_node ** subexps;
+  size_t re_nsub;
+  unsigned char * translate;
+  unsigned int newline_anchor:1;/* If true, an anchor at a newline matches.*/
+  unsigned int no_sub:1;       /* If set, don't  return register offsets. */
+  unsigned int is_anchored:1;
+  unsigned int is_nullable:1;
+  unsigned char fastmap[256];
+  void * owner_data;
+};
+
+typedef struct rx_posix_regex regex_t;
+
+/* Type for byte offsets within the string.  POSIX mandates this.  */
+typedef int regoff_t;
+
+typedef struct rx_registers
+{
+  regoff_t rm_so;               /* Byte offset from string's start to substring's start.  */
+  regoff_t rm_eo;              /* Byte offset from string's start to substring's end.  */
+  regoff_t final_tag;          /* data from the cut operator (only pmatch[0]) */
+} regmatch_t;
+
+\f
+
+/* If any error codes are removed, changed, or added, update the
+ * `rx_error_msg' table.
+ */
+#define REG_NOERROR    0               /* Success.  */
+#define REG_NOMATCH    1               /* Didn't find a match (for regexec).  */
+
+/* POSIX regcomp return error codes.  
+ * (In the order listed in the standard.)  
+ */
+#define REG_BADPAT     2               /* Invalid pattern.  */
+#define REG_ECOLLATE   3               /* Not implemented.  */
+#define REG_ECTYPE     4               /* Invalid character class name.  */
+#define REG_EESCAPE    5               /* Trailing backslash.  */
+#define REG_ESUBREG    6               /* Invalid back reference.  */
+#define REG_EBRACK     7               /* Unmatched left bracket.  */
+#define REG_EPAREN     8               /* Parenthesis imbalance.  */ 
+#define REG_EBRACE     9               /* Unmatched \{.  */
+#define REG_BADBR      10              /* Invalid contents of \{\}.  */
+#define REG_ERANGE     11              /* Invalid range end.  */
+#define REG_ESPACE     12              /* Ran out of memory.  */
+#define REG_BADRPT     13              /* No preceding re for repetition op.  */
+
+/* Error codes we've added.  
+ */
+#define REG_EEND       14              /* Premature end.  */
+#define REG_ESIZE      15              /* Compiled pattern bigger than 2^16 bytes.  */
+#define REG_ERPAREN    16              /* Unmatched ) or \); not returned from regcomp.  */
+
+\f
+/*
+ * POSIX `cflags' bits (i.e., information for `regcomp').
+ */
+
+/* If this bit is set, then use extended regular expression syntax.
+ * If not set, then use basic regular expression syntax.  
+ */
+#define REG_EXTENDED 1
+
+/* If this bit is set, then ignore case when matching.
+ * If not set, then case is significant.
+ */
+#define REG_ICASE (REG_EXTENDED << 1)
+/* If this bit is set, then anchors do not match at newline
+ *   characters in the string.
+ * If not set, then anchors do match at newlines.  
+ */
+#define REG_NEWLINE (REG_ICASE << 1)
+
+/* If this bit is set, then report only success or fail in regexec.
+ * If not set, then returns differ between not matching and errors.  
+ */
+#define REG_NOSUB (REG_NEWLINE << 1)
+
+
+/*
+ * POSIX `eflags' bits (i.e., information for regexec).  
+ */
+
+/* If this bit is set, then the beginning-of-line operator doesn't match
+ *   the beginning of the string (presumably because it's not the
+ *   beginning of a line).
+ * If not set, then the beginning-of-line operator does match the
+ *   beginning of the string.  
+ */
+#define REG_NOTBOL 1
+
+/* Like REG_NOTBOL, except for the end-of-line.  
+ */
+#define REG_NOTEOL (REG_NOTBOL << 1)
+
+/* For regnexec only.  Allocate register storage and return that. */
+#define REG_ALLOC_REGS (REG_NOTEOL << 1)
+
+\f
+#ifdef __STDC__
+extern int regncomp (regex_t * preg,
+                    const char * pattern, int len,
+                    int cflags);
+extern int regcomp (regex_t * preg, const char * pattern, int cflags);
+extern size_t regerror (int errcode,
+                       const regex_t *preg,
+                       char *errbuf, size_t errbuf_size);
+extern int regnexec (const regex_t *preg,
+                    const char *string, int len,
+                    size_t nmatch, regmatch_t **pmatch,
+                    int eflags);
+extern int regexec (const regex_t *preg,
+                   const char *string,
+                   size_t nmatch, regmatch_t pmatch[],
+                   int eflags);
+extern void regfree (regex_t *preg);
+
+#else /* STDC */
+extern int regncomp ();
+extern int regcomp ();
+extern size_t regerror ();
+extern int regnexec ();
+extern int regexec ();
+extern void regfree ();
+
+#endif /* STDC */
+#endif  /* INST_RXPOSIXH */
diff --git a/rx/rx.c b/rx/rx.c
new file mode 100644 (file)
index 0000000..afa8485
--- /dev/null
+++ b/rx/rx.c
@@ -0,0 +1,85 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include "rx.h"
+#include "rxall.h"
+#include "rxhash.h"
+#include "rxnfa.h"
+#include "rxsuper.h"
+
+\f
+
+const char rx_version_string[] = "GNU Rx version 1.5";
+
+\f
+#ifdef __STDC__
+struct rx *
+rx_make_rx (int cset_size)
+#else
+struct rx *
+rx_make_rx (cset_size)
+     int cset_size;
+#endif
+{
+  static int rx_id = 0;
+  struct rx * new_rx;
+  new_rx = (struct rx *)malloc (sizeof (*new_rx));
+  rx_bzero ((char *)new_rx, sizeof (*new_rx));
+  new_rx->rx_id = rx_id++;
+  new_rx->cache = rx_default_cache;
+  new_rx->local_cset_size = cset_size;
+  new_rx->instruction_table = rx_id_instruction_table;
+  new_rx->next_nfa_id = 0;
+  return new_rx;
+}
+
+#ifdef __STDC__
+void
+rx_free_rx (struct rx * rx)
+#else
+void
+rx_free_rx (rx)
+     struct rx * rx;
+#endif
+{
+  if (rx->start_set)
+    rx->start_set->starts_for = 0;
+  rx_free_nfa (rx);
+  free (rx);
+}
+
+
+#ifdef __STDC__
+void
+rx_bzero (char * mem, int size)
+#else
+void
+rx_bzero (mem, size)
+     char * mem;
+     int size;
+#endif
+{
+  while (size)
+    {
+      *mem = 0;
+      ++mem;
+      --size;
+    }
+}
diff --git a/rx/rx.h b/rx/rx.h
new file mode 100644 (file)
index 0000000..ccca945
--- /dev/null
+++ b/rx/rx.h
@@ -0,0 +1,49 @@
+/* classes: h_files */
+
+#ifndef RXH
+#define RXH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include "rxhash.h"
+
+
+\f
+
+extern const char rx_version_string[];
+
+\f
+#ifdef __STDC__
+extern struct rx * rx_make_rx (int cset_size);
+extern void rx_free_rx (struct rx * rx);
+extern void rx_bzero (char * mem, int size);
+
+#else /* STDC */
+extern struct rx * rx_make_rx ();
+extern void rx_free_rx ();
+extern void rx_bzero ();
+
+#endif /* STDC */
+
+
+
+
+
+#endif  /* RXH */
diff --git a/rx/rxall.h b/rx/rxall.h
new file mode 100644 (file)
index 0000000..adec72d
--- /dev/null
@@ -0,0 +1,36 @@
+/* classes: h_files */
+
+#ifndef RXALLH
+#define RXALLH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+#if 0
+#include <stdlib.h>
+#include "malloc.h"
+#endif
+\f
+#ifdef __STDC__
+
+#else /* STDC */
+
+#endif /* STDC */
+
+
+#endif  /* RXALLH */
diff --git a/rx/rxanal.c b/rx/rxanal.c
new file mode 100644 (file)
index 0000000..431be7a
--- /dev/null
@@ -0,0 +1,732 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include "rxall.h"
+#include "rxanal.h"
+#include "rxbitset.h"
+#include "rxsuper.h"
+
+\f
+
+
+#ifdef __STDC__
+int
+rx_posix_analyze_rexp (struct rexp_node *** subexps,
+                      size_t * re_nsub,
+                      struct rexp_node * node,
+                      int id)
+#else
+int
+rx_posix_analyze_rexp (subexps, re_nsub, node, id)
+     struct rexp_node *** subexps;
+     size_t * re_nsub;
+     struct rexp_node * node;
+     int id;
+#endif
+{
+  if (node)
+    {
+      size_t this_subexp;
+      if (node->type == r_parens)
+       {
+         if (node->params.intval >= 0)
+           {
+             this_subexp = *re_nsub;
+             ++*re_nsub;
+             if (!*subexps)
+               *subexps = (struct rexp_node **)malloc (sizeof (struct rexp_node *) * *re_nsub);
+             else
+               *subexps = (struct rexp_node **)realloc (*subexps,
+                                                        sizeof (struct rexp_node *) * *re_nsub);
+           }
+       }
+      if (node->params.pair.left)
+       id = rx_posix_analyze_rexp (subexps, re_nsub, node->params.pair.left, id);
+      if (node->params.pair.right)
+       id = rx_posix_analyze_rexp (subexps, re_nsub, node->params.pair.right, id);
+      switch (node->type)
+       {
+       case r_cset:
+         node->len = 1;
+         node->observed = 0;
+         break;
+       case r_string:
+         node->len = node->params.cstr.len;
+         node->observed = 0;
+         break;
+       case r_cut:
+         node->len = 0;
+         node->observed = 0;
+         break;
+       case r_concat:
+       case r_alternate:
+         {
+           int lob, rob;
+           int llen, rlen;
+           lob = (!node->params.pair.left ? 0 : node->params.pair.left->observed);
+           rob = (!node->params.pair.right ? 0 : node->params.pair.right->observed);
+           llen = (!node->params.pair.left ? 0 : node->params.pair.left->len);
+           rlen = (!node->params.pair.right ? 0 : node->params.pair.right->len);
+           node->len = ((llen >= 0) && (rlen >= 0)
+                        ? ((node->type == r_concat)
+                           ? llen + rlen
+                           : ((llen == rlen) ? llen : -1))
+                        : -1);
+           node->observed = lob || rob;
+           break;
+         }
+       case r_opt:
+       case r_star:
+       case r_plus:
+         node->len = -1;
+         node->observed = (node->params.pair.left
+                           ? node->params.pair.left->observed
+                           : 0);
+         break;
+
+       case  r_interval:
+         node->len = -1;
+         node->observed = 1;
+         break;
+
+       case r_parens:
+         if (node->params.intval >= 0)
+           {
+             node->observed = 1;
+             (*subexps)[this_subexp] = node;
+           }
+         else
+           node->observed = (node->params.pair.left
+                             ? node->params.pair.left->observed
+                             : 0);
+         node->len = (node->params.pair.left
+                      ? node->params.pair.left->len
+                      : 0);
+         break;
+       case r_context:
+         switch (node->params.intval)
+           {
+           default:
+             node->observed = 1;
+             node->len = -1;
+             break;
+           case '^':
+           case '$':
+           case '=':
+           case '<':
+           case '>':
+           case 'b':
+           case 'B':
+           case '`':
+           case '\'':
+             node->observed = 1;
+             node->len = 0;
+             break;
+           }
+         break;
+       }
+      if (node->observed)
+       node->id = id++;
+      return id;
+    }
+  return id;
+}
+\f
+/* Returns 0 unless the pattern can match the empty string. */
+#ifdef __STDC__
+int
+rx_fill_in_fastmap (int cset_size, unsigned char * map, struct rexp_node * exp)
+#else
+int
+rx_fill_in_fastmap (cset_size, map, exp)
+     int cset_size;
+     unsigned char * map;
+     struct rexp_node * exp;
+#endif
+{
+  if (!exp)
+    {
+    can_match_empty:
+      {
+       int x;
+       for (x = 0; x < cset_size; ++x)
+         map[x] = 1;
+      }
+      return 1;
+    }
+  
+  switch (exp->type)
+    {
+    case r_cset:
+      {
+       int x;
+       int most;
+       
+       most = exp->params.cset_size;
+       for (x = 0; x < most; ++x)
+         if (RX_bitset_member (exp->params.cset, x))
+           map[x] = 1;
+      }
+      return 0;
+
+    case r_string:
+      if (exp->params.cstr.len)
+       {
+         map[exp->params.cstr.contents[0]] = 1;
+         return 0;
+       }
+      else
+       return 1;
+
+    case r_cut:
+      return 1;
+      
+
+    case r_concat:
+      return rx_fill_in_fastmap (cset_size, map, exp->params.pair.left);
+
+      /* Why not the right branch?  If the left branch
+       * can't be empty it doesn't matter.  If it can, then
+       * the fastmap is already saturated, and again, the
+       * right branch doesn't matter.
+       */
+
+
+    case r_alternate:
+      return (  rx_fill_in_fastmap (cset_size, map, exp->params.pair.left)
+             | rx_fill_in_fastmap (cset_size, map, exp->params.pair.right));
+
+    case r_parens:
+    case r_plus:
+      return rx_fill_in_fastmap (cset_size, map, exp->params.pair.left);
+
+    case r_opt:
+    case r_star:
+      goto can_match_empty;
+      /* Why not the subtree?  These operators already saturate
+       * the fastmap. 
+       */
+
+    case r_interval:
+      if (exp->params.intval == 0)
+       goto can_match_empty;
+      else
+       return rx_fill_in_fastmap (cset_size, map, exp->params.pair.left);
+      
+    case r_context:
+      goto can_match_empty;
+    }
+
+  /* this should never happen but gcc seems to like it */
+  return 0;
+  
+}
+\f
+
+#ifdef __STDC__
+int
+rx_is_anchored_p (struct rexp_node * exp)
+#else
+int
+rx_is_anchored_p (exp)
+     struct rexp_node * exp;
+#endif
+{
+  if (!exp)
+    return 0;
+
+  switch (exp->type)
+    {
+    case r_opt:
+    case r_star:
+    case r_cset:
+    case r_string:
+    case r_cut:
+      return 0;
+
+    case r_parens:
+    case r_plus:
+    case r_concat:
+      return rx_is_anchored_p (exp->params.pair.left);
+
+    case r_alternate:
+      return (   rx_is_anchored_p (exp->params.pair.left)
+             && rx_is_anchored_p (exp->params.pair.right));
+
+
+    case r_interval:
+      if (exp->params.intval == 0)
+       return 0;
+      else
+       return rx_is_anchored_p (exp->params.pair.left);
+      
+    case r_context:
+      return (exp->params.intval == '^');
+    }
+
+  /* this should never happen but gcc seems to like it */
+  return 0;
+}
+
+\f
+
+#ifdef __STDC__
+enum rx_answers
+rx_start_superstate (struct rx_classical_system * frame)
+#else
+enum rx_answers
+rx_start_superstate (frame)
+     struct rx_classical_system * frame;
+#endif
+{
+  struct rx_superset * start_contents;
+  struct rx_nfa_state_set * start_nfa_set;
+
+  if (frame->rx->start_set)
+    start_contents = frame->rx->start_set;
+  else
+    {
+      {
+       struct rx_possible_future * futures;
+       futures = rx_state_possible_futures (frame->rx, frame->rx->start_nfa_states);
+
+       if (!futures)
+         return rx_bogus;
+
+       if (futures->next)
+         return rx_start_state_with_too_many_futures;
+
+       start_nfa_set = futures->destset;
+      }
+      
+      start_contents =
+       rx_superstate_eclosure_union (frame->rx,
+                                     rx_superset_cons (frame->rx, 0, 0),
+                                     start_nfa_set);
+      
+      if (!start_contents)
+       return rx_bogus;
+           
+      start_contents->starts_for = frame->rx;
+      frame->rx->start_set = start_contents;
+    }
+
+  if (   start_contents->superstate
+      && (start_contents->superstate->rx_id == frame->rx->rx_id))
+    {
+      if (frame->state)
+       {
+         rx_unlock_superstate (frame->rx, frame->state);
+       }
+      frame->state = start_contents->superstate;
+      /* The cached superstate may be in a semifree state.
+       * We need to lock it and preserve the invariant
+       * that a locked superstate is never semifree.
+       * So refresh it.
+       */
+      rx_refresh_this_superstate (frame->rx->cache, frame->state);
+      rx_lock_superstate (frame->rx, frame->state);
+      return rx_yes;
+    }
+  else
+    {
+      struct rx_superstate * state;
+
+      rx_protect_superset (frame->rx, start_contents);
+      state = rx_superstate (frame->rx, start_contents);
+      rx_release_superset (frame->rx, start_contents);
+      if (!state)
+       return rx_bogus;
+      if (frame->state)
+       {
+         rx_unlock_superstate (frame->rx, frame->state);
+       }
+      frame->state = state;
+      rx_lock_superstate (frame->rx, frame->state);
+      return rx_yes;
+    }
+}
+
+\f
+
+
+#ifdef __STDC__
+enum rx_answers
+rx_fit_p (struct rx_classical_system * frame, unsigned const char * burst, int len)
+#else
+enum rx_answers
+rx_fit_p (frame, burst, len)
+     struct rx_classical_system * frame;
+     unsigned const char * burst;
+     int len;
+#endif
+{
+  struct rx_inx * inx_table;
+  struct rx_inx * inx;
+
+  if (!frame->state)
+    return rx_bogus;
+
+  if (!len)
+    {
+      frame->final_tag = frame->state->contents->is_final;
+      return (frame->state->contents->is_final
+             ? rx_yes
+             : rx_no);
+    }
+
+  inx_table = frame->state->transitions;
+  rx_unlock_superstate (frame->rx, frame->state);
+
+  while (len--)
+    {
+      struct rx_inx * next_table;
+
+      inx = inx_table + *burst;
+      next_table = (struct rx_inx *)inx->data;
+      while (!next_table)
+       {
+         struct rx_superstate * state;
+         state = ((struct rx_superstate *)
+                  ((char *)inx_table
+                   - ((unsigned long)
+                      ((struct rx_superstate *)0)->transitions)));
+
+         switch ((int)inx->inx)
+           {
+           case rx_backtrack:
+             /* RX_BACKTRACK means that we've reached the empty
+              * superstate, indicating that match can't succeed
+              * from this point.
+              */
+             frame->state = 0;
+             return rx_no;
+           
+           case rx_cache_miss:
+             /* Because the superstate NFA is lazily constructed,
+              * and in fact may erode from underneath us, we sometimes
+              * have to construct the next instruction from the hard way.
+              * This invokes one step in the lazy-conversion.
+              */
+             inx = 
+               rx_handle_cache_miss
+                 (frame->rx, state, *burst, inx->data_2);
+
+             if (!inx)
+               {
+                 frame->state = 0;
+                 return rx_bogus;
+               }
+             next_table = (struct rx_inx *)inx->data;
+             continue;
+               
+
+             /* No other instructions are legal here.
+              * (In particular, this function does not handle backtracking
+              * or the related instructions.)
+              */
+           default:
+             frame->state = 0;
+             return rx_bogus;
+         }
+       }
+      inx_table = next_table;
+      ++burst;
+    }
+
+  if (inx->data_2)             /* indicates a final superstate */
+    {
+      frame->final_tag = (int)inx->data_2;
+      frame->state = ((struct rx_superstate *)
+                     ((char *)inx_table
+                      - ((unsigned long)
+                         ((struct rx_superstate *)0)->transitions)));
+      rx_lock_superstate (frame->rx, frame->state);
+      return rx_yes;
+    }
+  frame->state = ((struct rx_superstate *)
+                 ((char *)inx_table
+                  - ((unsigned long)
+                     ((struct rx_superstate *)0)->transitions)));
+  rx_lock_superstate (frame->rx, frame->state);
+  return rx_no;
+}
+
+\f
+
+
+#ifdef __STDC__
+enum rx_answers
+rx_advance (struct rx_classical_system * frame, unsigned const char * burst, int len)
+#else
+enum rx_answers
+rx_advance (frame, burst, len)
+     struct rx_classical_system * frame;
+     unsigned const char * burst;
+     int len;
+#endif
+{
+  struct rx_inx * inx_table;
+
+  if (!frame->state)
+    return rx_bogus;
+
+  if (!len)
+    return rx_yes;
+
+  inx_table = frame->state->transitions;
+  rx_unlock_superstate (frame->rx, frame->state);
+
+  while (len--)
+    {
+      struct rx_inx * inx;
+      struct rx_inx * next_table;
+
+      inx = inx_table + *burst;
+      next_table = (struct rx_inx *)inx->data;
+      while (!next_table)
+       {
+         struct rx_superstate * state;
+         state = ((struct rx_superstate *)
+                  ((char *)inx_table
+                   - ((unsigned long)
+                      ((struct rx_superstate *)0)->transitions)));
+
+         switch ((int)inx->inx)
+           {
+           case rx_backtrack:
+             /* RX_BACKTRACK means that we've reached the empty
+              * superstate, indicating that match can't succeed
+              * from this point.
+              */
+             frame->state = 0;
+             return rx_no;
+           
+           case rx_cache_miss:
+             /* Because the superstate NFA is lazily constructed,
+              * and in fact may erode from underneath us, we sometimes
+              * have to construct the next instruction from the hard way.
+              * This invokes one step in the lazy-conversion.
+              */
+             inx = 
+               rx_handle_cache_miss
+                 (frame->rx, state, *burst, inx->data_2);
+
+             if (!inx)
+               {
+                 frame->state = 0;
+                 return rx_bogus;
+               }
+             next_table = (struct rx_inx *)inx->data;
+             continue;
+               
+
+             /* No other instructions are legal here.
+              * (In particular, this function does not handle backtracking
+              * or the related instructions.)
+              */
+           default:
+             frame->state = 0;
+             return rx_bogus;
+         }
+       }
+      inx_table = next_table;
+      ++burst;
+    }
+  
+  frame->state = ((struct rx_superstate *)
+                 ((char *)inx_table
+                  - ((unsigned long)
+                     ((struct rx_superstate *)0)->transitions)));
+  rx_lock_superstate (frame->rx, frame->state);
+  return rx_yes;
+}
+
+\f
+
+#ifdef __STDC__
+int
+rx_advance_to_final (struct rx_classical_system * frame, unsigned const char * burst, int len)
+#else
+int
+rx_advance_to_final (frame, burst, len)
+     struct rx_classical_system * frame;
+     unsigned const char * burst;
+     int len;
+#endif
+{
+  int initial_len;
+  struct rx_inx * inx_table;
+  struct rx_superstate * this_state;
+
+  if (!frame->state)
+    return 0;
+
+  if (!len)
+    {
+      frame->final_tag = frame->state->contents->is_final;
+      return 0;
+    }
+
+  inx_table = frame->state->transitions;
+
+  initial_len = len;
+
+  this_state = frame->state;
+
+  while (len--)
+    {
+      struct rx_inx * inx;
+      struct rx_inx * next_table;
+
+      /* this_state holds the state for the position we're
+       * leaving.  this_state is locked. 
+       */
+      inx = inx_table + *burst;
+      next_table = (struct rx_inx *)inx->data;
+
+      while (!next_table)
+       {
+         struct rx_superstate * state;
+
+         state = ((struct rx_superstate *)
+                  ((char *)inx_table
+                   - ((unsigned long)
+                      ((struct rx_superstate *)0)->transitions)));
+         
+         switch ((int)inx->inx)
+           {
+           case rx_backtrack:
+             /* RX_BACKTRACK means that we've reached the empty
+              * superstate, indicating that match can't succeed
+              * from this point.
+              *
+              * Return to the state for the position prior to what
+              * we failed at, and return that position.
+              */
+             frame->state = this_state;
+             frame->final_tag = this_state->contents->is_final;
+             return (initial_len - len) - 1;
+           
+           case rx_cache_miss:
+             /* Because the superstate NFA is lazily constructed,
+              * and in fact may erode from underneath us, we sometimes
+              * have to construct the next instruction from the hard way.
+              * This invokes one step in the lazy-conversion.
+              */
+             inx = rx_handle_cache_miss
+               (frame->rx, state, *burst, inx->data_2);
+
+             if (!inx)
+               {
+                 rx_unlock_superstate (frame->rx, this_state);
+                 frame->state = 0;
+                 return -1;
+               }
+             next_table = (struct rx_inx *)inx->data;
+             continue;
+               
+
+             /* No other instructions are legal here.
+              * (In particular, this function does not handle backtracking
+              * or the related instructions.)
+              */
+           default:
+             rx_unlock_superstate (frame->rx, this_state);
+             frame->state = 0;
+             return -1;
+         }
+       }
+
+      /* Release the superstate for the preceeding position: */
+      rx_unlock_superstate (frame->rx, this_state);
+
+      /* Compute the superstate for the new position: */
+      inx_table = next_table;
+      this_state = ((struct rx_superstate *)
+                   ((char *)inx_table
+                    - ((unsigned long)
+                       ((struct rx_superstate *)0)->transitions)));
+      
+      /* Lock it (see top-of-loop invariant): */
+      rx_lock_superstate (frame->rx, this_state);
+      
+      /* Check to see if we should stop: */
+      if (this_state->contents->is_final)
+       {
+         frame->final_tag = this_state->contents->is_final;
+         frame->state = this_state;
+         return (initial_len - len);
+       }
+      
+      ++burst;
+    }
+
+  /* Consumed all of the characters. */
+  frame->state = this_state;
+  frame->final_tag = this_state->contents->is_final;
+
+  /* state already locked (see top-of-loop invariant) */
+  return initial_len;
+}
+
+
+\f
+
+#ifdef __STDC__
+void
+rx_terminate_system (struct rx_classical_system * frame)
+#else
+void
+rx_terminate_system (frame)
+     struct rx_classical_system * frame;
+#endif
+{
+  if (frame->state)
+    {
+      rx_unlock_superstate (frame->rx, frame->state);
+      frame->state = 0;
+    }
+}
+
+\f
+
+
+
+
+
+
+
+#ifdef __STDC__
+void
+rx_init_system (struct rx_classical_system * frame, struct rx * rx)
+#else
+void
+rx_init_system (frame, rx)
+     struct rx_classical_system * frame;
+     struct rx * rx;
+#endif
+{
+  frame->rx = rx;
+  frame->state = 0;
+}
+
+\f
+
diff --git a/rx/rxanal.h b/rx/rxanal.h
new file mode 100644 (file)
index 0000000..53dd576
--- /dev/null
@@ -0,0 +1,79 @@
+/* classes: h_files */
+
+#ifndef RXANALH
+#define RXANALH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include "rxcset.h"
+#include "rxnode.h"
+#include "rxsuper.h"
+
+\f
+
+
+enum rx_answers
+{
+  rx_yes = 0,
+  rx_no = 1,
+  rx_bogus = -1,
+  rx_start_state_with_too_many_futures = rx_bogus - 1
+  /* n < 0 -- error */
+};
+
+struct rx_classical_system
+{
+  struct rx * rx;
+  struct rx_superstate * state;
+  int final_tag;
+};
+
+
+\f
+#ifdef __STDC__
+extern int rx_posix_analyze_rexp (struct rexp_node *** subexps,
+                                 size_t * re_nsub,
+                                 struct rexp_node * node,
+                                 int id);
+extern int rx_fill_in_fastmap (int cset_size, unsigned char * map, struct rexp_node * exp);
+extern int rx_is_anchored_p (struct rexp_node * exp);
+extern enum rx_answers rx_start_superstate (struct rx_classical_system * frame);
+extern enum rx_answers rx_fit_p (struct rx_classical_system * frame, unsigned const char * burst, int len);
+extern enum rx_answers rx_advance (struct rx_classical_system * frame, unsigned const char * burst, int len);
+extern int rx_advance_to_final (struct rx_classical_system * frame, unsigned const char * burst, int len);
+extern void rx_terminate_system (struct rx_classical_system * frame);
+extern void rx_init_system (struct rx_classical_system * frame, struct rx * rx);
+
+#else /* STDC */
+extern int rx_posix_analyze_rexp ();
+extern int rx_fill_in_fastmap ();
+extern int rx_is_anchored_p ();
+extern enum rx_answers rx_start_superstate ();
+extern enum rx_answers rx_fit_p ();
+extern enum rx_answers rx_advance ();
+extern int rx_advance_to_final ();
+extern void rx_terminate_system ();
+extern void rx_init_system ();
+
+#endif /* STDC */
+
+
+
+#endif  /* RXANALH */
diff --git a/rx/rxbasic.c b/rx/rxbasic.c
new file mode 100644 (file)
index 0000000..e6e0e59
--- /dev/null
@@ -0,0 +1,118 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include "rxall.h"
+#include "rxbasic.h"
+#include "rxstr.h"
+
+
+\f
+
+int rx_basic_unfaniverse_delay = RX_DEFAULT_NFA_DELAY;
+static struct rx_unfaniverse * rx_basic_uv = 0;
+
+\f
+
+static int
+init_basic_once ()
+{
+  if (rx_basic_uv)
+    return 0;
+  rx_basic_uv = rx_make_unfaniverse (rx_basic_unfaniverse_delay);
+  return (rx_basic_uv ? 0 : -1);
+}
+
+\f
+#ifdef __STDC__
+struct rx_unfaniverse *
+rx_basic_unfaniverse (void)
+#else
+struct rx_unfaniverse *
+rx_basic_unfaniverse ()
+#endif
+{
+  if (init_basic_once ())
+    return 0;
+  return rx_basic_uv;
+}
+\f
+
+static char * silly_hack = 0;
+
+#ifdef __STDC__
+struct rx_solutions *
+rx_basic_make_solutions (struct rx_registers * regs, struct rexp_node * expression, struct rexp_node ** subexps, int start, int end, struct rx_context_rules * rules, const unsigned char * str)
+#else
+struct rx_solutions *
+rx_basic_make_solutions (regs, expression, subexps, start, end, rules, str)
+     struct rx_registers * regs;
+     struct rexp_node * expression;
+     struct rexp_node ** subexps;
+     int start;
+     int end;
+     struct rx_context_rules * rules;
+     const unsigned char * str;
+#endif
+{
+  struct rx_str_closure * closure;
+  if (init_basic_once ())
+    return 0;                  /* bogus but rare */
+  if (   expression
+      && (expression->len >= 0)
+      && (expression->len != (end - start)))
+    return &rx_no_solutions;
+  if (silly_hack)
+    {
+      closure = (struct rx_str_closure *)silly_hack;
+      silly_hack = 0;
+    }
+  else
+    closure = (struct rx_str_closure *)malloc (sizeof (*closure));
+  if (!closure)
+    return 0;
+  closure->str = str;
+  closure->len = end;
+  closure->rules = *rules;
+  return rx_make_solutions (regs, rx_basic_uv, expression, subexps, 256,
+                           start, end, rx_str_vmfn, rx_str_contextfn,
+                           (void *)closure);
+}
+
+
+
+#ifdef __STDC__
+void
+rx_basic_free_solutions (struct rx_solutions * solns)
+#else
+     void
+     rx_basic_free_solutions (solns)
+     struct rx_solutions * solns;
+#endif
+{
+  if (solns == &rx_no_solutions)
+    return;
+
+  if (!silly_hack)
+    silly_hack = (char *)solns->closure;
+  else
+    free (solns->closure);
+  solns->closure = 0;
+  rx_free_solutions (solns);
+}
diff --git a/rx/rxbasic.h b/rx/rxbasic.h
new file mode 100644 (file)
index 0000000..876381e
--- /dev/null
@@ -0,0 +1,60 @@
+/* classes: h_files */
+
+#ifndef RXBASICH
+#define RXBASICH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include "rxcontext.h"
+#include "rxnode.h"
+#include "rxspencer.h"
+#include "rxunfa.h"
+\f
+
+
+#ifndef RX_DEFAULT_NFA_DELAY
+/* This value bounds the number of NFAs kept in a cache of recent NFAs.
+ * This value is used whenever the rx_basic_* entry points are used,
+ * for example, when the Posix entry points are invoked.
+ */
+#define RX_DEFAULT_NFA_DELAY 64
+#endif
+
+\f
+#ifdef __STDC__
+extern struct rx_unfaniverse * rx_basic_unfaniverse (void);
+extern struct rx_solutions * rx_basic_make_solutions (struct rx_registers * regs, struct rexp_node * expression, struct rexp_node ** subexps, int start, int end, struct rx_context_rules * rules, const unsigned char * str);
+extern void rx_basic_free_solutions (struct rx_solutions * solns);
+
+#else /* STDC */
+extern struct rx_unfaniverse * rx_basic_unfaniverse ();
+extern struct rx_solutions * rx_basic_make_solutions ();
+extern void rx_basic_free_solutions ();
+
+#endif /* STDC */
+
+
+
+
+
+
+
+
+#endif  /* RXBASICH */
diff --git a/rx/rxbitset.c b/rx/rxbitset.c
new file mode 100644 (file)
index 0000000..a34b8b3
--- /dev/null
@@ -0,0 +1,364 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+/*
+ * Tom Lord (lord@cygnus.com, lord@gnu.ai.mit.edu)
+ */
+\f
+
+#include "rxall.h"
+#include "rxbitset.h"
+
+
+#ifdef __STDC__
+int
+rx_bitset_is_equal (int size, rx_Bitset a, rx_Bitset b)
+#else
+int
+rx_bitset_is_equal (size, a, b)
+     int size;
+     rx_Bitset a;
+     rx_Bitset b;
+#endif
+{
+  int x;
+  RX_subset s;
+
+  if (size == 0)
+    return 1;
+
+  s = b[0];
+  b[0] = ~a[0];
+
+  for (x = rx_bitset_numb_subsets(size) - 1; a[x] == b[x]; --x)
+    ;
+
+  b[0] = s;
+  return !x && (s == a[0]);
+}
+
+
+#ifdef __STDC__
+int
+rx_bitset_is_subset (int size, rx_Bitset a, rx_Bitset b)
+#else
+int
+rx_bitset_is_subset (size, a, b)
+     int size;
+     rx_Bitset a;
+     rx_Bitset b;
+#endif
+{
+  int x;
+  x = rx_bitset_numb_subsets(size) - 1;
+  while (x-- && ((a[x] & b[x]) == a[x]));
+  return x == -1;
+}
+
+
+#ifdef __STDC__
+int
+rx_bitset_empty (int size, rx_Bitset set)
+#else
+int
+rx_bitset_empty (size, set)
+     int size;
+     rx_Bitset set;
+#endif
+{
+  int x;
+  RX_subset s;
+  s = set[0];
+  set[0] = 1;
+  for (x = rx_bitset_numb_subsets(size) - 1; !set[x]; --x)
+    ;
+  set[0] = s;
+  return !s;
+}
+
+#ifdef __STDC__
+void
+rx_bitset_null (int size, rx_Bitset b)
+#else
+void
+rx_bitset_null (size, b)
+     int size;
+     rx_Bitset b;
+#endif
+{
+  rx_bzero ((char *)b, rx_sizeof_bitset(size));
+}
+
+
+#ifdef __STDC__
+void
+rx_bitset_universe (int size, rx_Bitset b)
+#else
+void
+rx_bitset_universe (size, b)
+     int size;
+     rx_Bitset b;
+#endif
+{
+  int x = rx_bitset_numb_subsets (size);
+  while (x--)
+    *b++ = ~(RX_subset)0;
+}
+
+
+#ifdef __STDC__
+void
+rx_bitset_complement (int size, rx_Bitset b)
+#else
+void
+rx_bitset_complement (size, b)
+     int size;
+     rx_Bitset b;
+#endif
+{
+  int x = rx_bitset_numb_subsets (size);
+  while (x--)
+    {
+      *b = ~*b;
+      ++b;
+    }
+}
+
+
+#ifdef __STDC__
+void
+rx_bitset_assign (int size, rx_Bitset a, rx_Bitset b)
+#else
+void
+rx_bitset_assign (size, a, b)
+     int size;
+     rx_Bitset a;
+     rx_Bitset b;
+#endif
+{
+  int x;
+  for (x = rx_bitset_numb_subsets(size) - 1; x >=0; --x)
+    a[x] = b[x];
+}
+
+
+#ifdef __STDC__
+void
+rx_bitset_union (int size, rx_Bitset a, rx_Bitset b)
+#else
+void
+rx_bitset_union (size, a, b)
+     int size;
+     rx_Bitset a;
+     rx_Bitset b;
+#endif
+{
+  int x;
+  for (x = rx_bitset_numb_subsets(size) - 1; x >=0; --x)
+    a[x] |= b[x];
+}
+
+
+#ifdef __STDC__
+void
+rx_bitset_intersection (int size,
+                       rx_Bitset a, rx_Bitset b)
+#else
+void
+rx_bitset_intersection (size, a, b)
+     int size;
+     rx_Bitset a;
+     rx_Bitset b;
+#endif
+{
+  int x;
+  for (x = rx_bitset_numb_subsets(size) - 1; x >=0; --x)
+    a[x] &= b[x];
+}
+
+
+#ifdef __STDC__
+void
+rx_bitset_difference (int size, rx_Bitset a, rx_Bitset b)
+#else
+void
+rx_bitset_difference (size, a, b)
+     int size;
+     rx_Bitset a;
+     rx_Bitset b;
+#endif
+{
+  int x;
+  for (x = rx_bitset_numb_subsets(size) - 1; x >=0; --x)
+    a[x] &=  ~ b[x];
+}
+
+
+#ifdef __STDC__
+void
+rx_bitset_revdifference (int size,
+                        rx_Bitset a, rx_Bitset b)
+#else
+void
+rx_bitset_revdifference (size, a, b)
+     int size;
+     rx_Bitset a;
+     rx_Bitset b;
+#endif
+{
+  int x;
+  for (x = rx_bitset_numb_subsets(size) - 1; x >=0; --x)
+    a[x] = ~a[x] & b[x];
+}
+
+#ifdef __STDC__
+void
+rx_bitset_xor (int size, rx_Bitset a, rx_Bitset b)
+#else
+void
+rx_bitset_xor (size, a, b)
+     int size;
+     rx_Bitset a;
+     rx_Bitset b;
+#endif
+{
+  int x;
+  for (x = rx_bitset_numb_subsets(size) - 1; x >=0; --x)
+    a[x] ^= b[x];
+}
+
+
+#ifdef __STDC__
+unsigned long
+rx_bitset_hash (int size, rx_Bitset b)
+#else
+unsigned long
+rx_bitset_hash (size, b)
+     int size;
+     rx_Bitset b;
+#endif
+{
+  int x;
+  unsigned long answer;
+
+  answer = 0;
+
+  for (x = 0; x < size; ++x)
+    {
+      if (RX_bitset_member (b, x))
+       answer += (answer << 3) + x;
+    }
+  return answer;
+}
+
+
+RX_subset rx_subset_singletons [RX_subset_bits] = 
+{
+  0x1,
+  0x2,
+  0x4,
+  0x8,
+  0x10,
+  0x20,
+  0x40,
+  0x80,
+  0x100,
+  0x200,
+  0x400,
+  0x800,
+  0x1000,
+  0x2000,
+  0x4000,
+  0x8000,
+  0x10000,
+  0x20000,
+  0x40000,
+  0x80000,
+  0x100000,
+  0x200000,
+  0x400000,
+  0x800000,
+  0x1000000,
+  0x2000000,
+  0x4000000,
+  0x8000000,
+  0x10000000,
+  0x20000000,
+  0x40000000,
+  0x80000000
+};
+
+
+/* 
+ * (define l (let loop ((x 0) (l '())) (if (eq? x 256) l (loop (+ x 1) (cons x l)))))
+ * (define lb (map (lambda (n) (number->string n 2)) l))
+ * (define lc (map string->list lb))
+ * (define ln (map (lambda (l) (map (lambda (c) (if (eq? c #\1) 1 0)) l)) lc))
+ * (define lt (map (lambda (l) (apply + l)) ln))
+ */
+
+static int char_pops[256] = 
+{
+  0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
+  1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+  1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+  2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+  1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+  2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+  2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+  3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+  1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+  2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+  2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+  3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+  2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+  3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+  3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+  4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8
+};
+
+#define RX_char_population(C) (char_pops[C])
+
+#ifdef __STDC__
+int
+rx_bitset_population (int size, rx_Bitset a)
+#else
+int
+rx_bitset_population (size, a)
+     int size;
+     rx_Bitset a;
+#endif
+{
+  int x;
+  int total;
+  unsigned char s;
+
+  if (size == 0)
+    return 0;
+
+  total = 0;
+  x = sizeof (RX_subset) * rx_bitset_numb_subsets(size) - 1;
+  while (x >= 0)
+    {
+      s = ((unsigned char *)a)[x];
+      --x;
+      total = total + RX_char_population (s);
+    }
+  return total;
+}     
diff --git a/rx/rxbitset.h b/rx/rxbitset.h
new file mode 100644 (file)
index 0000000..9ddb0f0
--- /dev/null
@@ -0,0 +1,109 @@
+/* classes: h_files */
+
+#ifndef RXBITSETH
+#define RXBITSETH
+
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+
+typedef unsigned long RX_subset;
+#define RX_subset_bits (8 * sizeof (RX_subset))
+#define RX_subset_mask (RX_subset_bits - 1)
+typedef RX_subset * rx_Bitset;
+
+#ifdef __STDC__
+typedef void (*rx_bitset_iterator) (rx_Bitset, int member_index);
+#else
+typedef void (*rx_bitset_iterator) ();
+#endif
+
+/* Return the index of the word containing the Nth bit.
+ */
+#define rx_bitset_subset(N)  ((N) / RX_subset_bits)
+
+/* Return the conveniently-sized supset containing the Nth bit.
+ */
+#define rx_bitset_subset_val(B,N)  ((B)[rx_bitset_subset(N)])
+
+
+/* Genericly combine the word containing the Nth bit with a 1 bit mask
+ * of the Nth bit position within that word.
+ */
+#define RX_bitset_access(B,N,OP) \
+  ((B)[rx_bitset_subset(N)] OP rx_subset_singletons[(N) & RX_subset_mask])
+
+#define RX_bitset_member(B,N)   RX_bitset_access(B, N, &)
+#define RX_bitset_enjoin(B,N)   RX_bitset_access(B, N, |=)
+#define RX_bitset_remove(B,N)   RX_bitset_access(B, N, &= ~)
+#define RX_bitset_toggle(B,N)   RX_bitset_access(B, N, ^= )
+
+/* How many words are needed for N bits?
+ */
+#define rx_bitset_numb_subsets(N) (((N) + RX_subset_bits - 1) / RX_subset_bits)
+
+/* How much memory should be allocated for a bitset with N bits?
+ */
+#define rx_sizeof_bitset(N)    (rx_bitset_numb_subsets(N) * sizeof(RX_subset))
+\f
+
+extern RX_subset rx_subset_singletons[];
+
+
+\f
+#ifdef __STDC__
+extern int rx_bitset_is_equal (int size, rx_Bitset a, rx_Bitset b);
+extern int rx_bitset_is_subset (int size, rx_Bitset a, rx_Bitset b);
+extern int rx_bitset_empty (int size, rx_Bitset set);
+extern void rx_bitset_null (int size, rx_Bitset b);
+extern void rx_bitset_universe (int size, rx_Bitset b);
+extern void rx_bitset_complement (int size, rx_Bitset b);
+extern void rx_bitset_assign (int size, rx_Bitset a, rx_Bitset b);
+extern void rx_bitset_union (int size, rx_Bitset a, rx_Bitset b);
+extern void rx_bitset_intersection (int size,
+                                   rx_Bitset a, rx_Bitset b);
+extern void rx_bitset_difference (int size, rx_Bitset a, rx_Bitset b);
+extern void rx_bitset_revdifference (int size,
+                                    rx_Bitset a, rx_Bitset b);
+extern void rx_bitset_xor (int size, rx_Bitset a, rx_Bitset b);
+extern unsigned long rx_bitset_hash (int size, rx_Bitset b);
+extern int rx_bitset_population (int size, rx_Bitset a);
+
+#else /* STDC */
+extern int rx_bitset_is_equal ();
+extern int rx_bitset_is_subset ();
+extern int rx_bitset_empty ();
+extern void rx_bitset_null ();
+extern void rx_bitset_universe ();
+extern void rx_bitset_complement ();
+extern void rx_bitset_assign ();
+extern void rx_bitset_union ();
+extern void rx_bitset_intersection ();
+extern void rx_bitset_difference ();
+extern void rx_bitset_revdifference ();
+extern void rx_bitset_xor ();
+extern unsigned long rx_bitset_hash ();
+extern int rx_bitset_population ();
+
+#endif /* STDC */
+
+
+
+#endif  /* RXBITSETH */
diff --git a/rx/rxcontext.h b/rx/rxcontext.h
new file mode 100644 (file)
index 0000000..b72cdfc
--- /dev/null
@@ -0,0 +1,41 @@
+/* classes: h_files */
+
+#ifndef RXCONTEXTH
+#define RXCONTEXTH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+struct rx_context_rules
+{
+  unsigned int newline_anchor:1;/* If true, an anchor at a newline matches.*/
+  unsigned int not_bol:1;      /* If set, the anchors ('^' and '$') don't */
+  unsigned int not_eol:1;      /*     match at the ends of the string.  */
+  unsigned int case_indep:1;
+};
+
+\f
+#ifdef __STDC__
+
+#else /* STDC */
+
+#endif /* STDC */
+
+
+#endif  /* RXCONTEXTH */
diff --git a/rx/rxcset.c b/rx/rxcset.c
new file mode 100644 (file)
index 0000000..e8f21df
--- /dev/null
@@ -0,0 +1,79 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+/*
+ * Tom Lord (lord@cygnus.com, lord@gnu.ai.mit.edu)
+ */
+\f
+
+
+#include "rxall.h"
+#include "rxcset.h"
+/* Utilities for manipulating bitset represntations of characters sets. */
+
+#ifdef __STDC__
+rx_Bitset
+rx_cset (int size)
+#else
+rx_Bitset
+rx_cset (size)
+     int size;
+#endif
+{
+  rx_Bitset b;
+  b = (rx_Bitset) malloc (rx_sizeof_bitset (size));
+  if (b)
+    rx_bitset_null (size, b);
+  return b;
+}
+
+
+#ifdef __STDC__
+rx_Bitset
+rx_copy_cset (int size, rx_Bitset a)
+#else
+rx_Bitset
+rx_copy_cset (size, a)
+     int size;
+     rx_Bitset a;
+#endif
+{
+  rx_Bitset cs;
+  cs = rx_cset (size);
+
+  if (cs)
+    rx_bitset_union (size, cs, a);
+
+  return cs;
+}
+
+
+#ifdef __STDC__
+void
+rx_free_cset (rx_Bitset c)
+#else
+void
+rx_free_cset (c)
+     rx_Bitset c;
+#endif
+{
+  if (c)
+    free ((char *)c);
+}
+
diff --git a/rx/rxcset.h b/rx/rxcset.h
new file mode 100644 (file)
index 0000000..d130189
--- /dev/null
@@ -0,0 +1,44 @@
+/* classes: h_files */
+
+#ifndef RXCSETH
+#define RXCSETH
+
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+/*  lord       Sun May  7 12:34:11 1995        */
+
+\f
+#include "rxbitset.h"
+
+\f
+#ifdef __STDC__
+extern rx_Bitset rx_cset (int size);
+extern rx_Bitset rx_copy_cset (int size, rx_Bitset a);
+extern void rx_free_cset (rx_Bitset c);
+
+#else /* STDC */
+extern rx_Bitset rx_cset ();
+extern rx_Bitset rx_copy_cset ();
+extern void rx_free_cset ();
+
+#endif /* STDC */
+
+
+
+#endif  /* RXCSETH */
diff --git a/rx/rxdbug.c b/rx/rxdbug.c
new file mode 100644 (file)
index 0000000..3c30506
--- /dev/null
@@ -0,0 +1,251 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include <stdio.h>
+#include "rxall.h"
+#include "rxgnucomp.h"
+#include "rxnfa.h"
+\f
+
+#ifdef HAVE_POSITIONAL_ARRAY_INITS
+#define AT(X) [X] =
+#else
+#define AT(X)
+#endif
+
+
+char *node_type_names[] =
+{
+  AT(r_cset) "r_cset",
+  AT(r_concat) "r_concat",
+  AT(r_alternate) "r_alternate",
+  AT(r_opt) "r_opt",
+  AT(r_star) "r_star",
+  AT(r_plus) "r_plus",
+  AT(r_string) "r_string",
+  AT(r_cut) "r_cut",
+
+  AT(r_interval) "r_interval",
+  AT(r_parens) "r_parens",
+  AT(r_context) "r_context"
+};
+
+void
+print_cset (cset_size, cs)
+     int cset_size;
+     rx_Bitset cs;
+{
+  int x;
+  if (!cs)
+      printf ("nil");
+  else
+    {
+      putchar ('[');
+      for (x = 0; x < cset_size; ++x)
+       if (RX_bitset_member (cs, x))
+         {
+           if (isprint(x))
+             putchar (x);
+           else
+             printf ("\\0%o ", x);
+         }
+      putchar (']');
+    }
+}
+
+void
+print_string(struct rx_string *s, char bracket)
+{
+  int x;
+  if (!s && bracket)
+    printf ("nil");
+  else
+    {
+      if (bracket)
+       putchar ('\"');
+      for (x = 0; x < s->len; ++x)
+       if (isprint(s->contents[x]))
+         putchar (s->contents[x]);
+       else
+         printf ("\\0%o ", x);
+      if (bracket)
+       putchar ('\"');
+    }
+}    
+
+void
+spaces (n)
+     int n;
+{
+  while (n--)
+    putchar (' ');
+}
+
+void
+print_rexp (cset_size, indent, rexp)
+     int cset_size;
+     int indent;
+     struct rexp_node * rexp;
+{
+  spaces (indent);
+  if (!rexp)
+    printf ("nil\n");
+  else
+    {
+      printf ("Node %d type %d (%s), iv=%d(%c), iv2=%d, len=%d obs=%d cs=",
+             rexp->id, rexp->type, node_type_names[rexp->type],
+             rexp->params.intval,
+             (isprint (rexp->params.intval)
+              ? rexp->params.intval
+              : ' '),
+             rexp->params.intval2,
+             rexp->len,
+             rexp->observed);
+      print_cset (cset_size, rexp->params.cset);
+      printf (" s=");
+      print_string (&(rexp->params.cstr), 1);
+      putchar ('\n');
+      if (rexp->params.pair.left || rexp->params.pair.right)
+       {
+         print_rexp (cset_size, indent + 2, rexp->params.pair.left);
+         print_rexp (cset_size, indent + 2, rexp->params.pair.right);
+       }
+    }
+}
+
+
+
+
+void
+unparse_print_rexp (cset_size, rexp)
+     int cset_size;
+     struct rexp_node * rexp;
+{
+  if (!rexp)
+    return;
+  else
+    switch (rexp->type)
+      {
+      case r_cset:
+       if (1 != rx_bitset_population (cset_size, rexp->params.cset))
+         print_cset (cset_size, rexp->params.cset);
+       else
+         {
+           int x;
+           rx_Bitset cs;
+           
+           cs = rexp->params.cset;
+           for (x = 0; x < cset_size; ++x)
+             if (RX_bitset_member (cs, x))
+               {
+                 if (isprint(x))
+                   putchar (x);
+                 else
+                   printf ("\\0%o ", x);
+               }
+         }
+       break;
+
+      case r_string:
+       print_string (&(rexp->params.cstr), 0);
+       break;
+
+      case r_parens:
+       putchar ('(');
+       unparse_print_rexp (cset_size, rexp->params.pair.left);
+       putchar (')');
+       break;
+
+      case r_context:
+       putchar ('\\');
+       putchar (rexp->params.intval);
+       break;
+
+      case r_cut:
+       printf ("[[:cut %d:]]", rexp->params.intval);
+       break;
+
+      case r_concat:
+       unparse_print_rexp (cset_size, rexp->params.pair.left);
+       unparse_print_rexp (cset_size, rexp->params.pair.right);
+       break;
+
+      case r_alternate:
+       unparse_print_rexp (cset_size, rexp->params.pair.left);
+       putchar ('|');
+       unparse_print_rexp (cset_size, rexp->params.pair.right);
+       break;
+
+      case r_opt:
+       unparse_print_rexp (cset_size, rexp->params.pair.left);
+       putchar ('?');
+       break;
+
+      case r_star:
+       unparse_print_rexp (cset_size, rexp->params.pair.left);
+       putchar ('*');
+       break;
+
+      case r_plus:
+       unparse_print_rexp (cset_size, rexp->params.pair.left);
+       putchar ('+');
+       break;
+
+      case r_interval:
+       unparse_print_rexp (cset_size, rexp->params.pair.left);
+       printf ("{%d,%d}", rexp->params.intval, rexp->params.intval2);
+       break;
+      }
+}
+
+
+void
+print_nfa_state (rx, state)
+     struct rx * rx;
+     struct rx_nfa_state * state;
+{
+  struct rx_nfa_edge * e;
+  printf ("state %d, is_final %d, is_start %d\n",
+         state->id, state->is_final, state->is_start);
+  for (e = state->edges; e; e = e->next)
+    {
+      printf ("\tEdge %s to %d ",
+             (e->type == ne_cset
+              ? "cset"
+              : (e->type == ne_epsilon
+                 ? "epsilon"
+                 : "side effect")),
+             e->dest->id);
+      if (e->type == ne_cset)
+       print_cset (rx->local_cset_size, e->params.cset);
+      else
+       printf ("%d", (int)e->params.side_effect);
+      putchar ('\n');
+    }
+}
+
+void
+print_nfa (rx)
+     struct rx * rx;
+{
+  struct rx_nfa_state * state;
+  for (state = rx->nfa_states; state; state = state->next)
+    print_nfa_state (rx, state);
+}
diff --git a/rx/rxgnucomp.c b/rx/rxgnucomp.c
new file mode 100644 (file)
index 0000000..18d4008
--- /dev/null
@@ -0,0 +1,1665 @@
+/*     Copyright (C) 1992, 1993, 1994, 1995 Free Software Foundation, Inc.
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+#include <sys/types.h>
+#include "rxall.h"
+#include "rxgnucomp.h"
+#include "inst-rxposix.h"
+
+\f
+/* {A Syntax Table} 
+ */
+
+/* Define the syntax basics for \<, \>, etc.
+ */
+
+#ifndef emacs
+#define CHARBITS 8
+#define CHAR_SET_SIZE (1 << CHARBITS)
+#define Sword 1
+#define SYNTAX(c) re_syntax_table[c]
+char re_syntax_table[CHAR_SET_SIZE];
+
+#ifdef __STDC__
+static void
+init_syntax_once (void)
+#else
+static void
+init_syntax_once ()
+#endif
+{
+   register int c;
+   static int done = 0;
+
+   if (done)
+     return;
+
+   rx_bzero ((char *)re_syntax_table, sizeof re_syntax_table);
+
+   for (c = 'a'; c <= 'z'; c++)
+     re_syntax_table[c] = Sword;
+
+   for (c = 'A'; c <= 'Z'; c++)
+     re_syntax_table[c] = Sword;
+
+   for (c = '0'; c <= '9'; c++)
+     re_syntax_table[c] = Sword;
+
+   re_syntax_table['_'] = Sword;
+
+   done = 1;
+}
+#endif /* not emacs */
+
+\f
+
+
+
+const char *rx_error_msg[] =
+{
+  0,                                           /* REG_NOUT */
+  "No match",                                  /* REG_NOMATCH */
+  "Invalid regular expression",                        /* REG_BADPAT */
+  "Invalid collation character",               /* REG_ECOLLATE */
+  "Invalid character class name",              /* REG_ECTYPE */
+  "Trailing backslash",                                /* REG_EESCAPE */
+  "Invalid back reference",                    /* REG_ESUBREG */
+  "Unmatched [ or [^",                         /* REG_EBRACK */
+  "Unmatched ( or \\(",                                /* REG_EPAREN */
+  "Unmatched \\{",                             /* REG_EBRACE */
+  "Invalid content of \\{\\}",                 /* REG_BADBR */
+  "Invalid range end",                         /* REG_ERANGE */
+  "Memory exhausted",                          /* REG_ESPACE */
+  "Invalid preceding regular expression",      /* REG_BADRPT */
+  "Premature end of regular expression",       /* REG_EEND */
+  "Regular expression too big",                        /* REG_ESIZE */
+  "Unmatched ) or \\)",                                /* REG_ERPAREN */
+};
+
+
+\f
+/* 
+ * Macros used while compiling patterns.
+ *
+ * By convention, PEND points just past the end of the uncompiled pattern,
+ * P points to the read position in the pattern.  `translate' is the name
+ * of the translation table (`TRANSLATE' is the name of a macro that looks
+ * things up in `translate').
+ */
+
+
+/*
+ * Fetch the next character in the uncompiled pattern---translating it 
+ * if necessary. *Also cast from a signed character in the constant
+ * string passed to us by the user to an unsigned char that we can use
+ * as an array index (in, e.g., `translate').
+ */
+#define PATFETCH(c)                                                    \
+ do {if (p == pend) return REG_EEND;                                   \
+    c = (unsigned char) *p++;                                          \
+    c = translate[c];                                                  \
+ } while (0)
+
+/* 
+ * Fetch the next character in the uncompiled pattern, with no
+ * translation.
+ */
+#define PATFETCH_RAW(c)                                                        \
+  do {if (p == pend) return REG_EEND;                                  \
+    c = (unsigned char) *p++;                                          \
+  } while (0)
+
+/* Go backwards one character in the pattern.  */
+#define PATUNFETCH p--
+
+
+#define TRANSLATE(d) translate[(unsigned char) (d)]
+
+typedef int regnum_t;
+
+/* Since offsets can go either forwards or backwards, this type needs to
+ * be able to hold values from -(MAX_BUF_SIZE - 1) to MAX_BUF_SIZE - 1.
+ */
+typedef int pattern_offset_t;
+
+typedef struct
+{
+  struct rexp_node ** top_expression;
+  struct rexp_node ** last_expression;
+  struct rexp_node ** last_non_regular_expression;
+  pattern_offset_t inner_group_offset;
+  regnum_t regnum;
+} compile_stack_elt_t;
+
+typedef struct
+{
+  compile_stack_elt_t *stack;
+  unsigned size;
+  unsigned avail;                      /* Offset of next open position.  */
+} compile_stack_type;
+
+
+#define INIT_COMPILE_STACK_SIZE 32
+
+#define COMPILE_STACK_EMPTY  (compile_stack.avail == 0)
+#define COMPILE_STACK_FULL  (compile_stack.avail == compile_stack.size)
+
+/* The next available element.  */
+#define COMPILE_STACK_TOP (compile_stack.stack[compile_stack.avail])
+
+
+/* Set the bit for character C in a list.  */
+#define SET_LIST_BIT(c)                               \
+  (b[((unsigned char) (c)) / CHARBITS]               \
+   |= 1 << (((unsigned char) c) % CHARBITS))
+
+/* Get the next unsigned number in the uncompiled pattern.  */
+#define GET_UNSIGNED_NUMBER(num)                                       \
+  { if (p != pend)                                                     \
+     {                                                                 \
+       PATFETCH (c);                                                   \
+       while (isdigit (c))                                             \
+         {                                                             \
+           if (num < 0)                                                        \
+              num = 0;                                                 \
+           num = num * 10 + c - '0';                                   \
+           if (p == pend)                                              \
+              break;                                                   \
+           PATFETCH (c);                                               \
+         }                                                             \
+       }                                                               \
+    }          
+
+#define CHAR_CLASS_MAX_LENGTH  64
+
+#define IS_CHAR_CLASS(string)                                          \
+   (!strcmp (string, "alpha") || !strcmp (string, "upper")             \
+    || !strcmp (string, "lower") || !strcmp (string, "digit")          \
+    || !strcmp (string, "alnum") || !strcmp (string, "xdigit")         \
+    || !strcmp (string, "space") || !strcmp (string, "print")          \
+    || !strcmp (string, "punct") || !strcmp (string, "graph")          \
+    || !strcmp (string, "cntrl") || !strcmp (string, "blank"))
+
+\f
+/* These predicates are used in regex_compile. */
+
+/* P points to just after a ^ in PATTERN.  Return true if that ^ comes
+ * after an alternative or a begin-subexpression.  We assume there is at
+ * least one character before the ^.  
+ */
+
+#ifdef __STDC__
+static int
+at_begline_loc_p (const char *pattern, const char * p, unsigned long syntax)
+#else
+static int
+at_begline_loc_p (pattern, p, syntax)
+     const char *pattern;
+     const char * p;
+     unsigned long syntax;
+#endif
+{
+  const char *prev = p - 2;
+  int prev_prev_backslash = ((prev > pattern) && (prev[-1] == '\\'));
+  
+    return
+      
+      (/* After a subexpression?  */
+       ((*prev == '(') && ((syntax & RE_NO_BK_PARENS) || prev_prev_backslash))
+       ||
+       /* After an alternative?  */
+       ((*prev == '|') && ((syntax & RE_NO_BK_VBAR) || prev_prev_backslash))
+       );
+}
+
+/* The dual of at_begline_loc_p.  This one is for $.  We assume there is
+ * at least one character after the $, i.e., `P < PEND'.
+ */
+
+#ifdef __STDC__
+static int
+at_endline_loc_p (const char *p, const char *pend, int syntax)
+#else
+static int
+at_endline_loc_p (p, pend, syntax)
+     const char *p;
+     const char *pend;
+     int syntax;
+#endif
+{
+  const char *next = p;
+  int next_backslash = (*next == '\\');
+  const char *next_next = (p + 1 < pend) ? (p + 1) : 0;
+  
+  return
+    (
+     /* Before a subexpression?  */
+     ((syntax & RE_NO_BK_PARENS)
+      ? (*next == ')')
+      : (next_backslash && next_next && (*next_next == ')')))
+    ||
+     /* Before an alternative?  */
+     ((syntax & RE_NO_BK_VBAR)
+      ? (*next == '|')
+      : (next_backslash && next_next && (*next_next == '|')))
+     );
+}
+\f
+
+unsigned char rx_id_translation[256] =
+{
+  0,  1,  2,  3,  4,  5,  6,  7,  8,  9,
+ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
+ 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+ 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
+ 60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
+ 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
+ 80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
+ 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
+
+ 100, 101, 102, 103, 104, 105, 106, 107, 108, 109,
+ 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
+ 120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
+ 130, 131, 132, 133, 134, 135, 136, 137, 138, 139,
+ 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
+ 150, 151, 152, 153, 154, 155, 156, 157, 158, 159,
+ 160, 161, 162, 163, 164, 165, 166, 167, 168, 169,
+ 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
+ 180, 181, 182, 183, 184, 185, 186, 187, 188, 189,
+ 190, 191, 192, 193, 194, 195, 196, 197, 198, 199,
+
+ 200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
+ 210, 211, 212, 213, 214, 215, 216, 217, 218, 219,
+ 220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
+ 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
+ 240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
+ 250, 251, 252, 253, 254, 255
+};
+
+/* The compiler keeps an inverted translation table.
+ * This looks up/inititalize elements.
+ * VALID is an array of booleans that validate CACHE.
+ */
+
+#ifdef __STDC__
+static rx_Bitset
+inverse_translation (int * n_members, int cset_size, char * valid, rx_Bitset cache, unsigned char * translate, int c)
+#else
+static rx_Bitset
+inverse_translation (n_members, cset_size, valid, cache, translate, c)
+     int * n_members;
+     int cset_size;
+     char * valid;
+     rx_Bitset cache;
+     unsigned char * translate;
+     int c;
+#endif
+{
+  rx_Bitset cs;
+  cs = cache + c * rx_bitset_numb_subsets (cset_size); 
+
+  if (!valid[c])
+    {
+      int x;
+      int c_tr;
+      int membs;
+
+      c_tr = TRANSLATE(c);
+      rx_bitset_null (cset_size, cs);
+      membs = 0;
+      for (x = 0; x < 256; ++x)
+       if (TRANSLATE(x) == c_tr)
+         {
+           RX_bitset_enjoin (cs, x);
+           membs++;
+         }
+      valid[c] = 1;
+      n_members[c] = membs;
+    }
+  return cs;
+}
+
+\f
+
+
+/* More subroutine declarations and macros for regex_compile.  */
+
+/* Returns true if REGNUM is in one of COMPILE_STACK's elements and 
+ * false if it's not.
+ */
+
+#ifdef __STDC__
+static int
+group_in_compile_stack (compile_stack_type compile_stack, regnum_t regnum)
+#else
+static int
+group_in_compile_stack (compile_stack, regnum)
+    compile_stack_type compile_stack;
+    regnum_t regnum;
+#endif
+{
+  int this_element;
+
+  for (this_element = compile_stack.avail - 1;  
+       this_element >= 0; 
+       this_element--)
+    if (compile_stack.stack[this_element].regnum == regnum)
+      return 1;
+
+  return 0;
+}
+
+
+/*
+ * Read the ending character of a range (in a bracket expression) from the
+ * uncompiled pattern *P_PTR (which ends at PEND).  We assume the
+ * starting character is in `P[-2]'.  (`P[-1]' is the character `-'.)
+ * Then we set the translation of all bits between the starting and
+ * ending characters (inclusive) in the compiled pattern B.
+ * 
+ * Return an error code.
+ * 
+ * We use these short variable names so we can use the same macros as
+ * `regex_compile' itself.  
+ */
+
+#ifdef __STDC__
+static int
+compile_range (int * n_members, int cset_size, rx_Bitset cs, const char ** p_ptr, const char * pend, unsigned char * translate, unsigned long syntax, rx_Bitset inv_tr, char * valid_inv_tr)
+#else
+static int
+compile_range (n_members, cset_size, cs, p_ptr, pend, translate, syntax, inv_tr, valid_inv_tr)
+     int * n_members;
+     int cset_size;
+     rx_Bitset cs;
+     const char ** p_ptr;
+     const char * pend;
+     unsigned char * translate;
+     unsigned long syntax;
+     rx_Bitset inv_tr;
+     char * valid_inv_tr;
+#endif
+{
+  unsigned this_char;
+
+  const char *p = *p_ptr;
+
+  unsigned char range_end;
+  unsigned char range_start = TRANSLATE(p[-2]);
+
+  if (p == pend)
+    return REG_ERANGE;
+
+  PATFETCH (range_end);
+
+  (*p_ptr)++;
+
+  if (range_start > range_end)
+    return syntax & RE_NO_EMPTY_RANGES ? REG_ERANGE : REG_NOERROR;
+
+  for (this_char = range_start; this_char <= range_end; this_char++)
+    {
+      rx_Bitset it =
+       inverse_translation (n_members, cset_size, valid_inv_tr, inv_tr, translate, this_char);
+      rx_bitset_union (cset_size, cs, it);
+    }
+  
+  return REG_NOERROR;
+}
+\f
+
+#ifdef __STDC__
+static int
+pointless_if_repeated (struct rexp_node * node)
+#else
+static int
+pointless_if_repeated (node)
+     struct rexp_node * node;
+#endif
+{
+  if (!node)
+    return 1;
+  switch (node->type)
+    {
+    case r_cset:
+    case r_string:
+    case r_cut:
+      return 0;
+    case r_concat:
+    case r_alternate:
+      return (pointless_if_repeated (node->params.pair.left)
+             && pointless_if_repeated (node->params.pair.right));
+    case r_opt:
+    case r_star:
+    case r_interval:
+    case r_parens:
+      return pointless_if_repeated (node->params.pair.left);
+    case r_context:
+      switch (node->params.intval)
+       {
+       case '=':
+       case '<':
+       case '^':
+       case 'b':
+       case 'B':
+       case '`':
+       case '\'':
+         return 1;
+       default:
+         return 0;
+       }
+    default:
+      return 0;
+    }
+}
+
+\f
+
+#ifdef __STDC__
+static int
+factor_string (struct rexp_node *** lastp, int cset_size)
+#else
+static int
+factor_string (lastp, cset_size)
+     struct rexp_node *** lastp;
+     int cset_size;
+#endif
+{
+  struct rexp_node ** expp;
+  struct rexp_node * exp;
+  rx_Bitset cs;
+  struct rexp_node * cset_node;
+
+  expp = *lastp;
+  exp = *expp;                 /* presumed r_string */
+
+  cs = rx_cset (cset_size);
+  if (!cs)
+    return -1;
+  RX_bitset_enjoin (cs, exp->params.cstr.contents[exp->params.cstr.len - 1]);
+  cset_node = rx_mk_r_cset (r_cset, cset_size, cs);
+  if (!cset_node)
+    {
+      rx_free_cset (cs);
+      return -1;
+    }
+  if (exp->params.cstr.len == 1)
+    {
+      rx_free_rexp (exp);
+      *expp = cset_node;
+      /* lastp remains the same */
+      return 0;
+    }
+  else
+    {
+      struct rexp_node * concat_node;
+      exp->params.cstr.len--;
+      concat_node = rx_mk_r_binop (r_concat, exp, cset_node);
+      if (!concat_node)
+       {
+         rx_free_rexp (cset_node);
+         return -1;
+       }
+      *expp = concat_node;
+      *lastp = &concat_node->params.pair.right;
+      return 0;
+    }
+}
+
+\f
+
+#define isa_blank(C) (((C) == ' ') || ((C) == '\t'))
+
+#ifdef __STDC__
+int
+rx_parse (struct rexp_node ** rexp_p,
+         const char *pattern,
+         int size,
+         unsigned long syntax,
+         int cset_size,
+         unsigned char *translate)
+#else
+int
+rx_parse (rexp_p, pattern, size, syntax, cset_size, translate)
+     struct rexp_node ** rexp_p;
+     const char *pattern;
+     int size;
+     unsigned long syntax;
+     int cset_size;
+     unsigned char *translate;
+#endif
+{
+  int compile_error;
+  RX_subset
+    inverse_translate [CHAR_SET_SIZE * rx_bitset_numb_subsets(CHAR_SET_SIZE)];
+  char validate_inv_tr [CHAR_SET_SIZE];
+  int n_members [CHAR_SET_SIZE];
+
+  /* We fetch characters from PATTERN here.  Even though PATTERN is
+   * `char *' (i.e., signed), we declare these variables as unsigned, so
+   * they can be reliably used as array indices.  
+   */
+  register unsigned char c;
+  register unsigned char c1;
+  
+  /* A random tempory spot in PATTERN.  */
+  const char *p1;
+  
+  /* Keeps track of unclosed groups.  */
+  compile_stack_type compile_stack;
+
+  /* Points to the current (ending) position in the pattern.  */
+  const char *p;
+  const char *pend;
+  
+  /* When parsing is done, this will hold the expression tree. */
+  struct rexp_node * rexp;
+
+  /* This and top_expression are saved on the compile stack. */
+  struct rexp_node ** top_expression;
+  struct rexp_node ** last_non_regular_expression;
+  struct rexp_node ** last_expression;
+  
+  /* Parameter to `goto append_node' */
+  struct rexp_node * append;
+
+  /* Counts open-groups as they are encountered.  This is the index of the
+   * innermost group being compiled.
+   */
+  regnum_t regnum;
+
+  /* True iff the sub-expression just started
+   * is purely syntactic.  Otherwise, a regmatch data 
+   * slot is allocated for the subexpression.
+   */
+  int syntax_only_parens;
+
+  /* Place in the uncompiled pattern (i.e., the {) to
+   * which to go back if the interval is invalid.  
+   */
+  const char *beg_interval;
+
+  int side;
+
+\f
+
+  if (!translate)
+    translate = rx_id_translation;
+
+  /* Points to the current (ending) position in the pattern.  */
+  p = pattern;
+  pend = pattern + size;
+  
+  /* When parsing is done, this will hold the expression tree. */
+  rexp = 0;
+
+  /* In the midst of compilation, this holds onto the regexp 
+   * first parst while rexp goes on to aquire additional constructs.
+   */
+  top_expression = &rexp;
+  last_non_regular_expression = top_expression;
+  last_expression = top_expression;
+  
+  /* Counts open-groups as they are encountered.  This is the index of the
+   * innermost group being compiled.
+   */
+  regnum = 0;
+
+  rx_bzero ((char *)validate_inv_tr, sizeof (validate_inv_tr));
+
+
+  /* Initialize the compile stack.  */
+  compile_stack.stack =  (( compile_stack_elt_t *) malloc ((INIT_COMPILE_STACK_SIZE) * sizeof ( compile_stack_elt_t)));
+  if (compile_stack.stack == 0)
+    return REG_ESPACE;
+
+  compile_stack.size = INIT_COMPILE_STACK_SIZE;
+  compile_stack.avail = 0;
+
+#if !defined (emacs) && !defined (SYNTAX_TABLE)
+  /* Initialize the syntax table.  */
+   init_syntax_once ();
+#endif
+
+  /* Loop through the uncompiled pattern until we're at the end.  */
+  while (p != pend)
+    {
+      PATFETCH (c);
+
+      switch (c)
+        {
+        case '^':
+          {
+            if (   /* If at start of pattern, it's an operator.  */
+                   p == pattern + 1
+                   /* If context independent, it's an operator.  */
+                || syntax & RE_CONTEXT_INDEP_ANCHORS
+                   /* Otherwise, depends on what's come before.  */
+                || at_begline_loc_p (pattern, p, syntax))
+             {
+               struct rexp_node * n
+                 = rx_mk_r_int (r_context, '^');
+               if (!n)
+                 goto space_error;
+               append = n;
+               goto append_node;
+             }
+            else
+              goto normal_char;
+          }
+          break;
+
+
+        case '$':
+          {
+            if (   /* If at end of pattern, it's an operator.  */
+                   p == pend 
+                   /* If context independent, it's an operator.  */
+                || syntax & RE_CONTEXT_INDEP_ANCHORS
+                   /* Otherwise, depends on what's next.  */
+                || at_endline_loc_p (p, pend, syntax))
+             {
+               struct rexp_node * n
+                 = rx_mk_r_int (r_context, '$');
+               if (!n)
+                 goto space_error;
+               append = n;
+               goto append_node;
+             }
+             else
+               goto normal_char;
+           }
+           break;
+
+
+       case '+':
+        case '?':
+          if ((syntax & RE_BK_PLUS_QM)
+              || (syntax & RE_LIMITED_OPS))
+            goto normal_char;
+
+        handle_plus:
+        case '*':
+          /* If there is no previous pattern... */
+          if (pointless_if_repeated (*last_expression))
+            {
+              if (syntax & RE_CONTEXT_INVALID_OPS)
+               {
+                 compile_error = REG_BADRPT;
+                 goto error_return;
+               }
+              else if (!(syntax & RE_CONTEXT_INDEP_OPS))
+                goto normal_char;
+            }
+
+          {
+            /* 1 means zero (many) matches is allowed.  */
+            char zero_times_ok = 0, many_times_ok = 0;
+
+            /* If there is a sequence of repetition chars, collapse it
+               down to just one (the right one).  We can't combine
+               interval operators with these because of, e.g., `a{2}*',
+               which should only match an even number of `a's.  */
+
+            for (;;)
+              {
+                zero_times_ok |= c != '+';
+                many_times_ok |= c != '?';
+
+                if (p == pend)
+                  break;
+
+                PATFETCH (c);
+
+                if (c == '*'
+                    || (!(syntax & RE_BK_PLUS_QM) && (c == '+' || c == '?')))
+                  ;
+
+                else if (syntax & RE_BK_PLUS_QM  &&  c == '\\')
+                  {
+                    if (p == pend)
+                     {
+                       compile_error = REG_EESCAPE;
+                       goto error_return;
+                     }
+
+                    PATFETCH (c1);
+                    if (!(c1 == '+' || c1 == '?'))
+                      {
+                        PATUNFETCH;
+                        PATUNFETCH;
+                        break;
+                      }
+
+                    c = c1;
+                  }
+                else
+                  {
+                    PATUNFETCH;
+                    break;
+                  }
+
+                /* If we get here, we found another repeat character.  */
+               }
+
+           /* Now we know whether or not zero matches is allowed
+            * and also whether or not two or more matches is allowed.
+            */
+
+           {
+             struct rexp_node * inner_exp;
+             struct rexp_node * star;
+
+             if (*last_expression && ((*last_expression)->type == r_string))
+               if (factor_string (&last_expression, cset_size))
+                 goto space_error;
+             inner_exp = *last_expression;
+             star = rx_mk_r_monop ((many_times_ok
+                                    ? (zero_times_ok ? r_star : r_plus)
+                                    : r_opt),
+                                   inner_exp);
+             if (!star)
+               goto space_error;
+             *last_expression = star;
+           }
+         }
+         break;
+
+
+       case '.':
+         {
+           rx_Bitset cs;
+           struct rexp_node * n;
+           cs = rx_cset (cset_size);
+           if (!cs)
+             goto space_error;
+           n = rx_mk_r_cset (r_cset, cset_size, cs);
+           if (!n)
+             {
+               rx_free_cset (cs);
+               goto space_error;
+             }
+           rx_bitset_universe (cset_size, cs);
+           if (!(syntax & RE_DOT_NEWLINE))
+             RX_bitset_remove (cs, '\n');
+           if (syntax & RE_DOT_NOT_NULL)
+             RX_bitset_remove (cs, 0);
+
+           append = n;
+           goto append_node;
+           break;
+         }
+
+
+        case '[':
+         if (p == pend)
+           {
+             compile_error = REG_EBRACK;
+             goto error_return;
+           }
+          {
+            int had_char_class;
+           rx_Bitset cs;
+           struct rexp_node * node;
+           int is_inverted;
+
+            had_char_class = 0;
+           is_inverted = *p == '^';
+           cs = rx_cset (cset_size);
+           if (!cs)
+             goto space_error;
+           node = rx_mk_r_cset (r_cset, cset_size ,cs);
+           if (!node)
+             {
+               rx_free_cset (cs);
+               goto space_error;
+             }
+           
+           /* This branch of the switch is normally exited with
+            *`goto append_node'
+            */
+           append = node;
+           
+            if (is_inverted)
+             p++;
+           
+            /* Remember the first position in the bracket expression.  */
+            p1 = p;
+           
+            /* Read in characters and ranges, setting map bits.  */
+            for (;;)
+              {
+                if (p == pend)
+                 {
+                   compile_error = REG_EBRACK;
+                   goto error_return;
+                 }
+               
+                PATFETCH (c);
+               
+                /* \ might escape characters inside [...] and [^...].  */
+                if ((syntax & RE_BACKSLASH_ESCAPE_IN_LISTS) && c == '\\')
+                  {
+                    if (p == pend)
+                     {
+                       compile_error = REG_EESCAPE;
+                       goto error_return;
+                     }
+                   
+                    PATFETCH (c1);
+                   {
+                     rx_Bitset it = inverse_translation (n_members,
+                                                         cset_size,
+                                                         validate_inv_tr,
+                                                         inverse_translate,
+                                                         translate,
+                                                         c1);
+                     rx_bitset_union (cset_size, cs, it);
+                   }
+                    continue;
+                  }
+               
+                /* Could be the end of the bracket expression.  If it's
+                   not (i.e., when the bracket expression is `[]' so
+                   far), the ']' character bit gets set way below.  */
+                if (c == ']' && p != p1 + 1)
+                  goto finalize_class_and_append;
+               
+                /* Look ahead to see if it's a range when the last thing
+                   was a character class.  */
+                if (had_char_class && c == '-' && *p != ']')
+                  {
+                   compile_error = REG_ERANGE;
+                   goto error_return;
+                 }
+               
+                /* Look ahead to see if it's a range when the last thing
+                   was a character: if this is a hyphen not at the
+                   beginning or the end of a list, then it's the range
+                   operator.  */
+                if (c == '-' 
+                    && !(p - 2 >= pattern && p[-2] == '[') 
+                    && !(p - 3 >= pattern && p[-3] == '[' && p[-2] == '^')
+                    && *p != ']')
+                  {
+                    int ret
+                      = compile_range (n_members, cset_size, cs, &p, pend, translate, syntax,
+                                      inverse_translate, validate_inv_tr);
+                    if (ret != REG_NOERROR)
+                     {
+                       compile_error = ret;
+                       goto error_return;
+                     }
+                  }
+               
+                else if (p[0] == '-' && p[1] != ']')
+                  { /* This handles ranges made up of characters only.  */
+                    int ret;
+                   
+                   /* Move past the `-'.  */
+                    PATFETCH (c1);
+                    
+                    ret = compile_range (n_members, cset_size, cs, &p, pend, translate, syntax,
+                                        inverse_translate, validate_inv_tr);
+                    if (ret != REG_NOERROR)
+                     {
+                       compile_error = ret;
+                       goto error_return;
+                     }
+                  }
+               
+                /* See if we're at the beginning of a possible character
+                   class.  */
+               
+               else if ((syntax & RE_CHAR_CLASSES)
+                        && (c == '[') && (*p == ':'))
+                  {
+                    char str[CHAR_CLASS_MAX_LENGTH + 1];
+                   
+                    PATFETCH (c);
+                    c1 = 0;
+                   
+                    /* If pattern is `[[:'.  */
+                    if (p == pend)
+                     {
+                       compile_error = REG_EBRACK;
+                       goto error_return;
+                     }
+                   
+                    for (;;)
+                      {
+                        PATFETCH (c);
+                        if (c == ':' || c == ']' || p == pend
+                            || c1 == CHAR_CLASS_MAX_LENGTH)
+                         break;
+                        str[c1++] = c;
+                      }
+                    str[c1] = '\0';
+                   
+                    /* If isn't a word bracketed by `[:' and:`]':
+                       undo the ending character, the letters, and leave 
+                       the leading `:' and `[' (but set bits for them).  */
+                    if (c == ':' && *p == ']')
+                      {
+                       if (!strncmp (str, "cut", 3))
+                         {
+                           int val;
+                           if (1 != sscanf (str + 3, " %d", &val))
+                             {
+                               compile_error = REG_ECTYPE;
+                               goto error_return;
+                             }
+                           /* Throw away the ]] */
+                           PATFETCH (c);
+                           PATFETCH (c);
+                           {
+                             struct rexp_node * cut;
+                             cut = rx_mk_r_int (r_cut, val);
+                             append = cut;
+                             goto append_node;
+                           }
+                         }
+                       else if (!strncmp (str, "(", 1))
+                         {
+                           /* Throw away the ]] */
+                           PATFETCH (c);
+                           PATFETCH (c);
+                           syntax_only_parens = 1;
+                           goto handle_open;
+                         }
+                       else if (!strncmp (str, ")", 1))
+                         {
+                           /* Throw away the ]] */
+                           PATFETCH (c);
+                           PATFETCH (c);
+                           syntax_only_parens = 1;
+                           goto handle_close;
+                         }
+                       else
+                         {
+                           int ch;
+                           int is_alnum = !strcmp (str, "alnum");
+                           int is_alpha = !strcmp (str, "alpha");
+                           int is_blank = !strcmp (str, "blank");
+                           int is_cntrl = !strcmp (str, "cntrl");
+                           int is_digit = !strcmp (str, "digit");
+                           int is_graph = !strcmp (str, "graph");
+                           int is_lower = !strcmp (str, "lower");
+                           int is_print = !strcmp (str, "print");
+                           int is_punct = !strcmp (str, "punct");
+                           int is_space = !strcmp (str, "space");
+                           int is_upper = !strcmp (str, "upper");
+                           int is_xdigit = !strcmp (str, "xdigit");
+                        
+                           if (!IS_CHAR_CLASS (str))
+                             {
+                               compile_error = REG_ECTYPE;
+                               goto error_return;
+                             }
+                       
+                           /* Throw away the ] at the end of the character
+                              class.  */
+                           PATFETCH (c);                                       
+                       
+                           if (p == pend) { compile_error = REG_EBRACK; goto error_return; }
+                       
+                           for (ch = 0; ch < 1 << CHARBITS; ch++)
+                             {
+                               if (   (is_alnum  && isalnum (ch))
+                                   || (is_alpha  && isalpha (ch))
+                                   || (is_blank  && isa_blank (ch))
+                                   || (is_cntrl  && iscntrl (ch))
+                                   || (is_digit  && isdigit (ch))
+                                   || (is_graph  && isgraph (ch))
+                                   || (is_lower  && islower (ch))
+                                   || (is_print  && isprint (ch))
+                                   || (is_punct  && ispunct (ch))
+                                   || (is_space  && isspace (ch))
+                                   || (is_upper  && isupper (ch))
+                                   || (is_xdigit && isxdigit (ch)))
+                                 {
+                                   rx_Bitset it =
+                                     inverse_translation (n_members,
+                                                          cset_size,
+                                                          validate_inv_tr,
+                                                          inverse_translate,
+                                                          translate,
+                                                          ch);
+                                   rx_bitset_union (cset_size,
+                                                    cs, it);
+                                 }
+                             }
+                           had_char_class = 1;
+                         }
+                     }
+                    else
+                      {
+                        c1++;
+                        while (c1--)    
+                          PATUNFETCH;
+                       {
+                         rx_Bitset it =
+                           inverse_translation (n_members,
+                                                cset_size,
+                                                validate_inv_tr,
+                                                inverse_translate,
+                                                translate,
+                                                '[');
+                         rx_bitset_union (cset_size,
+                                          cs, it);
+                       }
+                       {
+                         rx_Bitset it =
+                           inverse_translation (n_members,
+                                                cset_size,
+                                                validate_inv_tr,
+                                                inverse_translate,
+                                                translate,
+                                                ':');
+                         rx_bitset_union (cset_size,
+                                          cs, it);
+                       }
+                        had_char_class = 0;
+                      }
+                  }
+                else
+                  {
+                    had_char_class = 0;
+                   {
+                     rx_Bitset it = inverse_translation (n_members,
+                                                         cset_size,
+                                                         validate_inv_tr,
+                                                         inverse_translate,
+                                                         translate,
+                                                         c);
+                     rx_bitset_union (cset_size, cs, it);
+                   }
+                  }
+              }
+
+         finalize_class_and_append:
+           if (is_inverted)
+             {
+               rx_bitset_complement (cset_size, cs);
+               if (syntax & RE_HAT_LISTS_NOT_NEWLINE)
+                 RX_bitset_remove (cs, '\n');
+             }
+           goto append_node;
+          }
+          break;
+
+
+       case '(':
+          if (syntax & RE_NO_BK_PARENS)
+           {
+             syntax_only_parens = 0;
+             goto handle_open;
+           }
+          else
+            goto normal_char;
+
+
+        case ')':
+          if (syntax & RE_NO_BK_PARENS)
+           {
+             syntax_only_parens = 0;
+             goto handle_close;
+           }
+          else
+            goto normal_char;
+
+
+        case '\n':
+          if (syntax & RE_NEWLINE_ALT)
+            goto handle_alt;
+          else
+            goto normal_char;
+
+
+       case '|':
+          if (syntax & RE_NO_BK_VBAR)
+            goto handle_alt;
+          else
+            goto normal_char;
+
+
+        case '{':
+         if ((syntax & RE_INTERVALS) && (syntax & RE_NO_BK_BRACES))
+           goto handle_interval;
+         else
+           goto normal_char;
+
+
+        case '\\':
+          if (p == pend) { compile_error = REG_EESCAPE; goto error_return; }
+
+          /* Do not translate the character after the \, so that we can
+             distinguish, e.g., \B from \b, even if we normally would
+             translate, e.g., B to b.  */
+          PATFETCH_RAW (c);
+
+          switch (c)
+            {
+            case '(':
+              if (syntax & RE_NO_BK_PARENS)
+                goto normal_backslash;
+
+             syntax_only_parens = 0;
+
+            handle_open:
+             if (!syntax_only_parens)
+               regnum++;
+
+              if (COMPILE_STACK_FULL)
+                { 
+                  compile_stack.stack
+                   = ((compile_stack_elt_t *)
+                      realloc (compile_stack.stack,
+                               (compile_stack.size << 1) * sizeof (compile_stack_elt_t)));
+                 if (compile_stack.stack == 0)
+                   goto space_error;
+                 compile_stack.size <<= 1;
+               }
+
+             if (*last_non_regular_expression)
+               {
+                 struct rexp_node * concat;
+                 concat = rx_mk_r_binop (r_concat, *last_non_regular_expression, 0);
+                 if (!concat)
+                   goto space_error;
+                 *last_non_regular_expression = concat;
+                 last_non_regular_expression = &concat->params.pair.right;
+                 last_expression = last_non_regular_expression;
+               }
+
+              /*
+              * These are the values to restore when we hit end of this
+               * group.  
+              */
+             COMPILE_STACK_TOP.top_expression = top_expression;
+             COMPILE_STACK_TOP.last_expression = last_expression;
+             COMPILE_STACK_TOP.last_non_regular_expression = last_non_regular_expression;
+
+             if (syntax_only_parens)
+               COMPILE_STACK_TOP.regnum = -1;
+             else
+               COMPILE_STACK_TOP.regnum = regnum;
+             
+              compile_stack.avail++;
+             
+             top_expression = last_non_regular_expression;
+             break;
+
+
+            case ')':
+              if (syntax & RE_NO_BK_PARENS) goto normal_backslash;
+             syntax_only_parens = 0;
+
+            handle_close:
+              /* See similar code for backslashed left paren above.  */
+              if (COMPILE_STACK_EMPTY)
+                if (syntax & RE_UNMATCHED_RIGHT_PAREN_ORD)
+                  goto normal_char;
+                else
+                  { compile_error = REG_ERPAREN; goto error_return; }
+
+              /* Since we just checked for an empty stack above, this
+               * ``can't happen''. 
+              */
+
+              {
+                /* We don't just want to restore into `regnum', because
+                 * later groups should continue to be numbered higher,
+                 * as in `(ab)c(de)' -- the second group is #2.
+                */
+                regnum_t this_group_regnum;
+               struct rexp_node ** inner;
+               struct rexp_node * parens;
+
+               inner = top_expression;
+                compile_stack.avail--;
+
+               if (!!syntax_only_parens != (COMPILE_STACK_TOP.regnum == -1))
+                 { compile_error = REG_ERPAREN; goto error_return; }
+
+               top_expression = COMPILE_STACK_TOP.top_expression;
+               last_expression = COMPILE_STACK_TOP.last_expression;
+               last_non_regular_expression = COMPILE_STACK_TOP.last_non_regular_expression;
+                this_group_regnum = COMPILE_STACK_TOP.regnum;
+
+               {
+                 parens = rx_mk_r_monop (r_parens, *inner);
+                 if (!parens)
+                   goto space_error;
+                 parens->params.intval = this_group_regnum;
+                 *inner = parens;
+                 break;
+               }
+             }
+
+            case '|':                                  /* `\|'.  */
+              if ((syntax & RE_LIMITED_OPS) || (syntax & RE_NO_BK_VBAR))
+                goto normal_backslash;
+            handle_alt:
+              if (syntax & RE_LIMITED_OPS)
+                goto normal_char;
+
+             {
+               struct rexp_node * alt;
+
+               alt = rx_mk_r_binop (r_alternate, *top_expression, 0);
+               if (!alt)
+                 goto space_error;
+               *top_expression = alt;
+               last_expression = &alt->params.pair.right;
+               last_non_regular_expression = &alt->params.pair.right;
+             }
+              break;
+
+
+            case '{': 
+              /* If \{ is a literal.  */
+              if (!(syntax & RE_INTERVALS)
+                     /* If we're at `\{' and it's not the open-interval 
+                        operator.  */
+                  || ((syntax & RE_INTERVALS) && (syntax & RE_NO_BK_BRACES))
+                  || (p - 2 == pattern  &&  p == pend))
+                goto normal_backslash;
+
+            handle_interval:
+              {
+                /* If got here, then the syntax allows intervals. 
+                */
+
+                /* At least (most) this many matches must be made.  
+                */
+                int lower_bound;
+               int upper_bound;
+
+               lower_bound = -1;
+               upper_bound = -1;
+
+               /* We're about to parse the bounds of the interval.
+                * It may turn out that this isn't an interval after
+                * all, in which case these same characters will have
+                * to be reparsed as literals.   This remembers
+                * the backtrack point in the parse:
+                */
+                beg_interval = p - 1;
+
+                if (p == pend)
+                  {
+                    if (syntax & RE_NO_BK_BRACES)
+                      goto unfetch_interval;
+                    else
+                      { compile_error = REG_EBRACE; goto error_return; }
+                  }
+
+                GET_UNSIGNED_NUMBER (lower_bound);
+
+                if (c == ',')
+                  {
+                    GET_UNSIGNED_NUMBER (upper_bound);
+                    if (upper_bound < 0) upper_bound = RE_DUP_MAX;
+                  }
+                else
+                  /* Interval such as `{n}' => match exactly n times.
+                  */
+                  upper_bound = lower_bound;
+
+                if (lower_bound < 0
+                   || upper_bound > RE_DUP_MAX
+                    || lower_bound > upper_bound)
+                  {
+                    if (syntax & RE_NO_BK_BRACES)
+                      goto unfetch_interval;
+                    else 
+                      { compile_error = REG_BADBR; goto error_return; }
+                  }
+
+                if (!(syntax & RE_NO_BK_BRACES)) 
+                  {
+                    if (c != '\\') { compile_error = REG_EBRACE; goto error_return; }
+                    PATFETCH (c);
+                  }
+
+                if (c != '}')
+                  {
+                    if (syntax & RE_NO_BK_BRACES)
+                      goto unfetch_interval;
+                    else 
+                      { compile_error = REG_BADBR; goto error_return; }
+                  }
+
+                /* We just parsed a valid interval.
+                * lower_bound and upper_bound are set.
+                */
+
+                /* If it's invalid to have no preceding re.
+                */
+                if (pointless_if_repeated (*last_expression))
+                  {
+                    if (syntax & RE_CONTEXT_INVALID_OPS)
+                      { compile_error = REG_BADRPT; goto error_return; }
+                    else if (!(syntax & RE_CONTEXT_INDEP_OPS))
+                     /* treat the interval spec as literal chars. */
+                      goto unfetch_interval; 
+                  }
+
+               {
+                 struct rexp_node * interval;
+
+                 if (*last_expression && ((*last_expression)->type == r_string))
+                   if (factor_string (&last_expression, cset_size))
+                     goto space_error;
+                 interval = rx_mk_r_monop (r_interval, *last_expression);
+                 if (!interval)
+                   goto space_error;
+                 interval->params.intval = lower_bound;
+                 interval->params.intval2 = upper_bound;
+                 *last_expression = interval;
+                 last_non_regular_expression = last_expression;
+               }
+                beg_interval = 0;
+              }
+              break;
+
+            unfetch_interval:
+              /* If an invalid interval, match the characters as literals.  */
+               p = beg_interval;
+               beg_interval = 0;
+
+               /* normal_char and normal_backslash need `c'.  */
+               PATFETCH (c);   
+
+               if (!(syntax & RE_NO_BK_BRACES))
+                 {
+                   if ((p > pattern)  &&  (p[-1] == '\\'))
+                     goto normal_backslash;
+                 }
+               goto normal_char;
+
+#ifdef emacs
+            /* There is no way to specify the before_dot and after_dot
+             * operators.  rms says this is ok.  --karl
+            */
+            case '=':
+             side = '=';
+             goto add_side_effect;
+              break;
+
+            case 's':
+           case 'S':
+             {
+               rx_Bitset cs;
+               struct rexp_node * set;
+
+               cs = rx_cset (&cset_size);
+               if (!cs)
+                 goto space_error;
+               set = rx_mk_r_cset (r_cset, cset_size, cs);
+               if (!set)
+                 {
+                   rx_free_cset (cs);
+                   goto space_error;
+                 }
+               if (c == 'S')
+                 rx_bitset_universe (cset_size, cs);
+
+               PATFETCH (c);
+               {
+                 int x;
+                 enum syntaxcode code = syntax_spec_code [c];
+                 for (x = 0; x < 256; ++x)
+                   {
+                     
+                     if (SYNTAX (x) == code)
+                       {
+                         rx_Bitset it =
+                           inverse_translation (n_members,
+                                                cset_size, validate_inv_tr,
+                                                inverse_translate,
+                                                translate, x);
+                         rx_bitset_xor (cset_size, cs, it);
+                       }
+                   }
+               }
+               append = set;
+               goto append_node;
+             }
+              break;
+#endif /* emacs */
+
+
+            case 'w':
+            case 'W':
+             {
+               rx_Bitset cs;
+               struct rexp_node * n;
+
+               cs = rx_cset (cset_size);
+               n = (cs ? rx_mk_r_cset (r_cset, cset_size, cs) : 0);
+               if (!(cs && n))
+                 {
+                   if (cs)
+                     rx_free_cset (cs);
+                   goto space_error;
+                 }
+               if (c == 'W')
+                 rx_bitset_universe (cset_size ,cs);
+               {
+                 int x;
+                 for (x = cset_size - 1; x > 0; --x)
+                   if (SYNTAX(x) & Sword)
+                     RX_bitset_toggle (cs, x);
+               }
+               append = n;
+               goto append_node;
+             }
+              break;
+
+            case '<':
+             side = '<';
+             goto add_side_effect;
+              break;
+
+            case '>':
+              side = '>';
+             goto add_side_effect;
+              break;
+
+            case 'b':
+              side = 'b';
+             goto add_side_effect;
+              break;
+
+            case 'B':
+              side = 'B';
+             goto add_side_effect;
+              break;
+
+            case '`':
+             side = '`';
+             goto add_side_effect;
+             break;
+             
+            case '\'':
+             side = '\'';
+             goto add_side_effect;
+              break;
+
+           add_side_effect:
+             {
+               struct rexp_node * se;
+               se = rx_mk_r_int (r_context, side);
+               if (!se)
+                 goto space_error;
+               append = se;
+               goto append_node;
+             }
+             break;
+
+            case '1': case '2': case '3': case '4': case '5':
+            case '6': case '7': case '8': case '9':
+              if (syntax & RE_NO_BK_REFS)
+                goto normal_char;
+
+              c1 = c - '0';
+
+              /* Can't back reference to a subexpression if inside of it.  */
+              if (group_in_compile_stack (compile_stack, c1))
+                goto normal_char;
+
+              if (c1 > regnum)
+                { compile_error = REG_ESUBREG; goto error_return; }
+
+             side = c;
+             goto add_side_effect;
+              break;
+
+            case '+':
+            case '?':
+              if (syntax & RE_BK_PLUS_QM)
+                goto handle_plus;
+              else
+                goto normal_backslash;
+
+            default:
+            normal_backslash:
+              /* You might think it would be useful for \ to mean
+               * not to translate; but if we don't translate it
+               * it will never match anything.
+              */
+              c = TRANSLATE (c);
+              goto normal_char;
+            }
+          break;
+
+
+       default:
+        /* Expects the character in `c'.  */
+       normal_char:
+           {
+             rx_Bitset cs;
+             struct rexp_node * match;
+             rx_Bitset it;
+
+             it = inverse_translation (n_members,
+                                       cset_size, validate_inv_tr,
+                                       inverse_translate, translate, c);
+
+             if (1 != n_members[c])
+               {
+                 cs = rx_cset (cset_size);
+                 match = (cs ? rx_mk_r_cset (r_cset, cset_size, cs) : 0);
+                 if (!(cs && match))
+                   {
+                     if (cs)
+                       rx_free_cset (cs);
+                     goto space_error;
+                   }
+                 rx_bitset_union (CHAR_SET_SIZE, cs, it);
+                 append = match;
+                 goto append_node;
+               }
+             else
+               {
+                 if (*last_expression && (*last_expression)->type == r_string)
+                   {           
+                     if (rx_adjoin_string (&((*last_expression)->params.cstr), c))
+                       goto space_error;
+                     break;
+                   }
+                 else
+                   {
+                     append = rx_mk_r_str (r_string, c);
+                     if(!append)
+                       goto space_error;
+                     goto append_node;
+                   }
+               }
+             break;
+
+           append_node:
+             /* This genericly appends the rexp APPEND to *LAST_EXPRESSION
+              * and then parses the next character normally.
+              */
+             if (RX_regular_node_type (append->type))
+               {
+                 if (!*last_expression)
+                   *last_expression = append;
+                 else
+                   {
+                     struct rexp_node * concat;
+                     concat = rx_mk_r_binop (r_concat,
+                                             *last_expression, append);
+                     if (!concat)
+                       goto space_error;
+                     *last_expression = concat;
+                     last_expression = &concat->params.pair.right;
+                   }
+               }
+             else
+               {
+                 if (!*last_non_regular_expression)
+                   {
+                     *last_non_regular_expression = append;
+                     last_expression = last_non_regular_expression;
+                   }
+                 else
+                   {
+                     struct rexp_node * concat;
+                     concat = rx_mk_r_binop (r_concat,
+                                             *last_non_regular_expression, append);
+                     if (!concat)
+                       goto space_error;
+                     *last_non_regular_expression = concat;
+                     last_non_regular_expression = &concat->params.pair.right;
+                     last_expression = last_non_regular_expression;
+                   }
+               }
+           }
+       } /* switch (c) */
+    } /* while p != pend */
+
+  
+  /* Through the pattern now.  */
+
+  if (!COMPILE_STACK_EMPTY) 
+    { compile_error = REG_EPAREN; goto error_return; }
+  free (compile_stack.stack);
+
+
+  *rexp_p = rexp;
+  return REG_NOERROR;
+
+ space_error:
+  compile_error = REG_ESPACE;
+
+ error_return:
+  free (compile_stack.stack);
+  /* Free expressions pushed onto the compile stack! */
+  if (rexp)
+    rx_free_rexp (rexp);
+  return compile_error;
+}
+
+
diff --git a/rx/rxgnucomp.h b/rx/rxgnucomp.h
new file mode 100644 (file)
index 0000000..d572ece
--- /dev/null
@@ -0,0 +1,210 @@
+/* classes: h_files */
+
+#ifndef RXGNUCOMPH
+#define RXGNUCOMPH
+/*     Copyright (C) 1992, 1993, 1994, 1995 Free Software Foundation, Inc.
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include "rxcset.h"
+#include "rxnode.h"
+
+
+\f
+
+/* This is an array of error messages corresponding to the error codes.
+ */
+extern const char *rx_error_msg[];
+
+
+\f
+/* {Syntax Bits}
+ */
+
+/* The following bits are used to determine the regexp syntax we
+   recognize.  The set/not-set meanings are chosen so that Emacs syntax
+   remains the value 0.  The bits are given in alphabetical order, and
+   the definitions shifted by one from the previous bit; thus, when we
+   add or remove a bit, only one other definition need change.  */
+
+enum RE_SYNTAX_BITS
+{
+/* If this bit is not set, then \ inside a bracket expression is literal.
+   If set, then such a \ quotes the following character.  */
+  RE_BACKSLASH_ESCAPE_IN_LISTS = (1),
+
+/* If this bit is not set, then + and ? are operators, and \+ and \? are
+     literals. 
+   If set, then \+ and \? are operators and + and ? are literals.  */
+  RE_BK_PLUS_QM = (RE_BACKSLASH_ESCAPE_IN_LISTS << 1),
+
+/* If this bit is set, then character classes are supported.  They are:
+     [:alpha:], [:upper:], [:lower:],  [:digit:], [:alnum:], [:xdigit:],
+     [:space:], [:print:], [:punct:], [:graph:], and [:cntrl:].
+   If not set, then character classes are not supported.  */
+  RE_CHAR_CLASSES = (RE_BK_PLUS_QM << 1),
+
+/* If this bit is set, then ^ and $ are always anchors (outside bracket
+     expressions, of course).
+   If this bit is not set, then it depends:
+        ^  is an anchor if it is at the beginning of a regular
+           expression or after an open-group or an alternation operator;
+        $  is an anchor if it is at the end of a regular expression, or
+           before a close-group or an alternation operator.  
+
+   This bit could be (re)combined with RE_CONTEXT_INDEP_OPS, because
+   POSIX draft 11.2 says that * etc. in leading positions is undefined.
+   We already implemented a previous draft which made those constructs
+   invalid, though, so we haven't changed the code back.  */
+  RE_CONTEXT_INDEP_ANCHORS = (RE_CHAR_CLASSES << 1),
+
+/* If this bit is set, then special characters are always special
+     regardless of where they are in the pattern.
+   If this bit is not set, then special characters are special only in
+     some contexts; otherwise they are ordinary.  Specifically, 
+     * + ? and intervals are only special when not after the beginning,
+     open-group, or alternation operator.  */
+  RE_CONTEXT_INDEP_OPS = (RE_CONTEXT_INDEP_ANCHORS << 1),
+
+/* If this bit is set, then *, +, ?, and { cannot be first in an re or
+     immediately after an alternation or begin-group operator.  */
+  RE_CONTEXT_INVALID_OPS = (RE_CONTEXT_INDEP_OPS << 1),
+
+/* If this bit is set, then . matches newline.
+   If not set, then it doesn't.  */
+  RE_DOT_NEWLINE = (RE_CONTEXT_INVALID_OPS << 1),
+
+/* If this bit is set, then . doesn't match NUL.
+   If not set, then it does.  */
+  RE_DOT_NOT_NULL = (RE_DOT_NEWLINE << 1),
+
+/* If this bit is set, nonmatching lists [^...] do not match newline.
+   If not set, they do.  */
+  RE_HAT_LISTS_NOT_NEWLINE = (RE_DOT_NOT_NULL << 1),
+
+/* If this bit is set, either \{...\} or {...} defines an
+     interval, depending on RE_NO_BK_BRACES. 
+   If not set, \{, \}, {, and } are literals.  */
+  RE_INTERVALS = (RE_HAT_LISTS_NOT_NEWLINE << 1),
+
+/* If this bit is set, +, ? and | aren't recognized as operators.
+   If not set, they are.  */
+  RE_LIMITED_OPS = (RE_INTERVALS << 1),
+
+/* If this bit is set, newline is an alternation operator.
+   If not set, newline is literal.  */
+  RE_NEWLINE_ALT = (RE_LIMITED_OPS << 1),
+
+/* If this bit is set, then `{...}' defines an interval, and \{ and \}
+     are literals.
+  If not set, then `\{...\}' defines an interval.  */
+  RE_NO_BK_BRACES = (RE_NEWLINE_ALT << 1),
+
+/* If this bit is set, (...) defines a group, and \( and \) are literals.
+   If not set, \(...\) defines a group, and ( and ) are literals.  */
+  RE_NO_BK_PARENS = (RE_NO_BK_BRACES << 1),
+
+/* If this bit is set, then \<digit> matches <digit>.
+   If not set, then \<digit> is a back-reference.  */
+  RE_NO_BK_REFS = (RE_NO_BK_PARENS << 1),
+
+/* If this bit is set, then | is an alternation operator, and \| is literal. 
+   If not set, then \| is an alternation operator, and | is literal.  */
+  RE_NO_BK_VBAR = (RE_NO_BK_REFS << 1),
+
+/* If this bit is set, then an ending range point collating higher
+     than the starting range point, as in [z-a], is invalid.
+   If not set, then when ending range point collates higher than the
+     starting range point, the range is ignored.  */
+  RE_NO_EMPTY_RANGES = (RE_NO_BK_VBAR << 1),
+
+/* If this bit is set, then an unmatched ) is ordinary.
+   If not set, then an unmatched ) is invalid.  */
+  RE_UNMATCHED_RIGHT_PAREN_ORD = (RE_NO_EMPTY_RANGES << 1),
+  
+  RE_SYNTAX_EMACS = 0,
+
+  RE_SYNTAX_AWK = (RE_BACKSLASH_ESCAPE_IN_LISTS | RE_DOT_NOT_NULL                      
+                  | RE_NO_BK_PARENS            | RE_NO_BK_REFS                         
+                  | RE_NO_BK_VBAR               | RE_NO_EMPTY_RANGES                   
+                  | RE_UNMATCHED_RIGHT_PAREN_ORD),
+
+  RE_SYNTAX_GREP = (RE_BK_PLUS_QM              | RE_CHAR_CLASSES                               
+                   | RE_HAT_LISTS_NOT_NEWLINE | RE_INTERVALS                           
+                   | RE_NEWLINE_ALT),
+
+  RE_SYNTAX_EGREP = (RE_CHAR_CLASSES        | RE_CONTEXT_INDEP_ANCHORS                 
+                    | RE_CONTEXT_INDEP_OPS | RE_HAT_LISTS_NOT_NEWLINE                  
+                    | RE_NEWLINE_ALT       | RE_NO_BK_PARENS                           
+                    | RE_NO_BK_VBAR),
+  
+  RE_SYNTAX_POSIX_EGREP = (RE_SYNTAX_EGREP | RE_INTERVALS | RE_NO_BK_BRACES),
+
+  /* Syntax bits common to both basic and extended POSIX regex syntax.  */
+  _RE_SYNTAX_POSIX_COMMON = (RE_CHAR_CLASSES | RE_DOT_NEWLINE      | RE_DOT_NOT_NULL           
+                            | RE_INTERVALS  | RE_NO_EMPTY_RANGES),
+
+  RE_SYNTAX_POSIX_BASIC = (_RE_SYNTAX_POSIX_COMMON | RE_BK_PLUS_QM),
+
+  /* Differs from ..._POSIX_BASIC only in that RE_BK_PLUS_QM becomes
+     RE_LIMITED_OPS, i.e., \? \+ \| are not recognized.  */
+
+  RE_SYNTAX_POSIX_MINIMAL_BASIC = (_RE_SYNTAX_POSIX_COMMON | RE_LIMITED_OPS),
+
+  RE_SYNTAX_POSIX_EXTENDED = (_RE_SYNTAX_POSIX_COMMON | RE_CONTEXT_INDEP_ANCHORS                       
+                             | RE_CONTEXT_INDEP_OPS  | RE_NO_BK_BRACES                         
+                             | RE_NO_BK_PARENS       | RE_NO_BK_VBAR                           
+                             | RE_UNMATCHED_RIGHT_PAREN_ORD),
+
+  /* Differs from ..._POSIX_EXTENDED in that RE_CONTEXT_INVALID_OPS
+     replaces RE_CONTEXT_INDEP_OPS and RE_NO_BK_REFS is added.  */
+  RE_SYNTAX_POSIX_MINIMAL_EXTENDED = (_RE_SYNTAX_POSIX_COMMON  | RE_CONTEXT_INDEP_ANCHORS
+                                     | RE_CONTEXT_INVALID_OPS | RE_NO_BK_BRACES
+                                     | RE_NO_BK_PARENS        | RE_NO_BK_REFS
+                                     | RE_NO_BK_VBAR       | RE_UNMATCHED_RIGHT_PAREN_ORD),
+
+  RE_SYNTAX_SED = RE_SYNTAX_POSIX_BASIC,
+
+  RE_SYNTAX_POSIX_AWK = (RE_SYNTAX_POSIX_EXTENDED | RE_BACKSLASH_ESCAPE_IN_LISTS)
+};
+\f
+
+/* Maximum number of duplicates an interval can allow.  Some systems
+   (erroneously) define this in other header files, but we want our
+   value, so remove any previous define.  */
+#undef RE_DUP_MAX
+#define RE_DUP_MAX ((1 << 15) - 1) 
+
+
+\f
+#ifdef __STDC__
+extern int rx_parse (struct rexp_node ** rexp_p,
+                              const char *pattern,
+                              int size,
+                              unsigned long syntax,
+                              int cset_size,
+                              unsigned char *translate);
+
+#else /* STDC */
+extern int rx_parse ();
+
+#endif /* STDC */
+
+
+#endif  /* RXGNUCOMPH */
diff --git a/rx/rxhash.c b/rx/rxhash.c
new file mode 100644 (file)
index 0000000..4e95973
--- /dev/null
@@ -0,0 +1,394 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+/*
+ * Tom Lord (lord@cygnus.com, lord@gnu.ai.mit.edu)
+ */
+\f
+
+#include "rxall.h"
+#include "rxhash.h"
+
+\f
+
+#ifdef __STDC__
+static struct rx_hash *
+default_hash_alloc (struct rx_hash_rules * rules)
+#else
+static struct rx_hash *
+default_hash_alloc (rules)
+     struct rx_hash_rules * rules;
+#endif
+{
+  return (struct rx_hash *)malloc (sizeof (struct rx_hash));
+}
+
+
+#ifdef __STDC__
+static struct rx_hash_item *
+default_hash_item_alloc (struct rx_hash_rules * rules, void * value)
+#else
+static struct rx_hash_item *
+default_hash_item_alloc (rules, value)
+     struct rx_hash_rules * rules;
+     void * value;
+#endif
+{
+  struct rx_hash_item * it;
+  it = (struct rx_hash_item *)malloc (sizeof (*it));
+  if (it)
+    {
+      it->data = value;
+      it->binding = 0;
+    }
+  return it;
+}
+
+
+#ifdef __STDC__
+static void
+default_free_hash (struct rx_hash * tab,
+                   struct rx_hash_rules * rules)
+#else
+static void
+default_free_hash (tab, rules)
+     struct rx_hash * tab;
+     struct rx_hash_rules * rules;
+#endif
+{
+  free ((char *)tab);
+}
+
+
+#ifdef __STDC__
+static void
+default_free_hash_item (struct rx_hash_item * item,
+                        struct rx_hash_rules * rules)
+#else
+static void
+default_free_hash_item (item, rules)
+     struct rx_hash_item * item;
+     struct rx_hash_rules * rules;
+#endif
+{
+  free ((char *)item);
+}
+
+#ifdef __STDC__
+static int 
+default_eq (void * va, void * vb)
+#else
+static int 
+default_eq (va, vb)
+     void * va;
+     void * vb;
+#endif
+{
+  return va == vb;
+}
+
+\f
+
+#define EQ(rules) ((rules && rules->eq) ? rules->eq : default_eq)
+#define HASH_ALLOC(rules) ((rules && rules->hash_alloc) ? rules->hash_alloc : default_hash_alloc)
+#define FREE_HASH(rules) ((rules && rules->free_hash) ? rules->free_hash : default_free_hash)
+#define ITEM_ALLOC(rules) ((rules && rules->hash_item_alloc) ? rules->hash_item_alloc : default_hash_item_alloc)
+#define FREE_HASH_ITEM(rules) ((rules && rules->free_hash_item) ? rules->free_hash_item : default_free_hash_item)
+
+\f
+static unsigned long rx_hash_masks[4] =
+{
+  0x12488421,
+  0x96699669,
+  0xbe7dd7eb,
+  0xffffffff
+};
+
+/* hash to bucket */
+#define JOIN_BYTE(H, B)  (((H) + ((H) << 3) + (B)) & 0xf)
+
+#define H2B(X) JOIN_BYTE (JOIN_BYTE (JOIN_BYTE ((X & 0xf), ((X>>8) & 0xf)), ((X>>16) & 0xf)), ((X>>24) & 0xf))
+
+#define BKTS 16
+
+/* Hash tables */
+#ifdef __STDC__
+struct rx_hash_item * 
+rx_hash_find (struct rx_hash * table,
+             unsigned long hash,
+             void * value,
+             struct rx_hash_rules * rules)
+#else
+struct rx_hash_item * 
+rx_hash_find (table, hash, value, rules)
+     struct rx_hash * table;
+     unsigned long hash;
+     void * value;
+     struct rx_hash_rules * rules;
+#endif
+{
+  rx_hash_eq eq = EQ (rules);
+  int maskc = 0;
+  long mask = rx_hash_masks [0];
+  int bucket = H2B(hash & mask);
+
+  while (RX_bitset_member (&table->nested_p, bucket))
+    {
+      table = (struct rx_hash *)(table->children [bucket]);
+      ++maskc;
+      mask = rx_hash_masks[maskc];
+      bucket = H2B (hash & mask);
+    }
+
+  {
+    struct rx_hash_item * it;
+    it = (struct rx_hash_item *)(table->children[bucket]);
+    while (it)
+      if (eq (it->data, value))
+       return it;
+      else
+       it = it->next_same_hash;
+  }
+
+  return 0;
+}
+
+
+#ifdef __STDC__
+static int 
+listlen (struct rx_hash_item * bucket)
+#else
+static int 
+listlen (bucket)
+     struct rx_hash_item * bucket;
+#endif
+{
+  int i;
+  for (i = 0; bucket; ++i, bucket = bucket->next_same_hash)
+    ;
+  return i;
+}
+
+#ifdef __STDC__
+static int
+overflows (struct rx_hash_item * bucket)
+#else
+static int
+overflows (bucket)
+     struct rx_hash_item * bucket;
+#endif
+{
+  return !(   bucket
+          && bucket->next_same_hash
+          && bucket->next_same_hash->next_same_hash
+          && bucket->next_same_hash->next_same_hash->next_same_hash);
+}
+
+
+#ifdef __STDC__
+struct rx_hash_item *
+rx_hash_store (struct rx_hash * table,
+              unsigned long hash,
+              void * value,
+              struct rx_hash_rules * rules)
+#else
+struct rx_hash_item *
+rx_hash_store (table, hash, value, rules)
+     struct rx_hash * table;
+     unsigned long hash;
+     void * value;
+     struct rx_hash_rules * rules;
+#endif
+{
+  rx_hash_eq eq = EQ (rules);
+  int maskc = 0;
+  long mask = rx_hash_masks [0];
+  int bucket = H2B(hash & mask);
+  int depth = 0;
+  
+  while (RX_bitset_member (&table->nested_p, bucket))
+    {
+      table = (struct rx_hash *)(table->children [bucket]);
+      ++maskc;
+      mask = rx_hash_masks[maskc];
+      bucket = H2B(hash & mask);
+      ++depth;
+    }
+  
+  {
+    struct rx_hash_item * it;
+    it = (struct rx_hash_item *)(table->children[bucket]);
+    while (it)
+      if (eq (it->data, value))
+       return it;
+      else
+       it = it->next_same_hash;
+  }
+  
+  {
+    if (   (depth < 3)
+       && (overflows ((struct rx_hash_item *)table->children [bucket])))
+      {
+       struct rx_hash * newtab;
+       newtab = (struct rx_hash *) HASH_ALLOC(rules) (rules);
+       if (!newtab)
+         goto add_to_bucket;
+       rx_bzero ((char *)newtab, sizeof (*newtab));
+       newtab->parent = table;
+       {
+         struct rx_hash_item * them;
+         unsigned long newmask;
+         them = (struct rx_hash_item *)table->children[bucket];
+         newmask = rx_hash_masks[maskc + 1];
+         while (them)
+           {
+             struct rx_hash_item * save = them->next_same_hash;
+             int new_buck = H2B(them->hash & newmask);
+             them->next_same_hash = ((struct rx_hash_item *)
+                                     newtab->children[new_buck]);
+             ((struct rx_hash_item **)newtab->children)[new_buck] = them;
+             them->table = newtab;
+             them = save;
+             ++newtab->refs;
+             --table->refs;
+           }
+         ((struct rx_hash **)table->children)[bucket] = newtab;
+         RX_bitset_enjoin (&table->nested_p, bucket);
+         ++table->refs;
+         table = newtab;
+         bucket = H2B(hash & newmask);
+       }
+      }
+  }
+ add_to_bucket:
+  {
+    struct rx_hash_item  * it = ((struct rx_hash_item *)
+                                ITEM_ALLOC(rules) (rules, value));
+    if (!it)
+      return 0;
+    it->hash = hash;
+    it->table = table;
+    /* DATA and BINDING are to be set in hash_item_alloc */
+    it->next_same_hash = (struct rx_hash_item *)table->children [bucket];
+    ((struct rx_hash_item **)table->children)[bucket] = it;
+    ++table->refs;
+    return it;
+  }
+}
+
+
+#ifdef __STDC__
+void
+rx_hash_free (struct rx_hash_item * it, struct rx_hash_rules * rules)
+#else
+void
+rx_hash_free (it, rules)
+     struct rx_hash_item * it;
+     struct rx_hash_rules * rules;
+#endif
+{
+  if (it)
+    {
+      struct rx_hash * table = it->table;
+      unsigned long hash = it->hash;
+      int depth = (table->parent
+                  ? (table->parent->parent
+                     ? (table->parent->parent->parent
+                        ? 3
+                        : 2)
+                     : 1)
+                  : 0);
+      int bucket = H2B (hash & rx_hash_masks [depth]);
+      struct rx_hash_item ** pos
+       = (struct rx_hash_item **)&table->children [bucket];
+      
+      while (*pos != it)
+       pos = &(*pos)->next_same_hash;
+      *pos = it->next_same_hash;
+      FREE_HASH_ITEM(rules) (it, rules);
+      --table->refs;
+      while (!table->refs && depth)
+       {
+         struct rx_hash * save = table;
+         table = table->parent;
+         --depth;
+         bucket = H2B(hash & rx_hash_masks [depth]);
+         --table->refs;
+         table->children[bucket] = 0;
+         RX_bitset_remove (&table->nested_p, bucket);
+         FREE_HASH (rules) (save, rules);
+       }
+    }
+}
+
+#ifdef __STDC__
+void
+rx_free_hash_table (struct rx_hash * tab, rx_hash_freefn freefn,
+                   struct rx_hash_rules * rules)
+#else
+void
+rx_free_hash_table (tab, freefn, rules)
+     struct rx_hash * tab;
+     rx_hash_freefn freefn;
+     struct rx_hash_rules * rules;
+#endif
+{
+  int x;
+
+  for (x = 0; x < BKTS; ++x)
+    if (RX_bitset_member (&tab->nested_p, x))
+      {
+       rx_free_hash_table ((struct rx_hash *)tab->children[x],
+                           freefn, rules);
+       FREE_HASH (rules) ((struct rx_hash *)tab->children[x], rules);
+      }
+    else
+      {
+       struct rx_hash_item * them = (struct rx_hash_item *)tab->children[x];
+       while (them)
+         {
+           struct rx_hash_item * that = them;
+           them = that->next_same_hash;
+           freefn (that);
+           FREE_HASH_ITEM (rules) (that, rules);
+         }
+      }
+}
+
+
+
+#ifdef __STDC__
+int 
+rx_count_hash_nodes (struct rx_hash * st)
+#else
+int 
+rx_count_hash_nodes (st)
+     struct rx_hash * st;
+#endif
+{
+  int x;
+  int count = 0;
+  for (x = 0; x < BKTS; ++x)
+    count += ((RX_bitset_member (&st->nested_p, x))
+             ? rx_count_hash_nodes ((struct rx_hash *)st->children[x])
+             : listlen ((struct rx_hash_item *)(st->children[x])));
+  
+  return count;
+}
+
diff --git a/rx/rxhash.h b/rx/rxhash.h
new file mode 100644 (file)
index 0000000..9763fdf
--- /dev/null
@@ -0,0 +1,112 @@
+/* classes: h_files */
+
+#ifndef RXHASHH
+#define RXHASHH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+/*
+ * Tom Lord (lord@cygnus.com, lord@gnu.ai.mit.edu)
+ */
+\f
+
+#include "rxbitset.h"
+
+/* giant inflatable hash trees */
+
+struct rx_hash_item
+{
+  struct rx_hash_item * next_same_hash;
+  struct rx_hash * table;
+  unsigned long hash;
+  void * data;
+  void * binding;
+};
+
+struct rx_hash
+{
+  struct rx_hash * parent;
+  int refs;
+  RX_subset nested_p;
+  void ** children[16];
+};
+
+struct rx_hash_rules;
+
+/* rx_hash_eq should work like the == operator. */
+
+#ifdef __STDC__
+typedef int (*rx_hash_eq)(void *, void *);
+typedef struct rx_hash * (*rx_alloc_hash)(struct rx_hash_rules *);
+typedef void (*rx_free_hash)(struct rx_hash *,
+                           struct rx_hash_rules *);
+typedef struct rx_hash_item * (*rx_alloc_hash_item)(struct rx_hash_rules *,
+                                                   void *);
+typedef void (*rx_free_hash_item)(struct rx_hash_item *,
+                                struct rx_hash_rules *);
+typedef void (*rx_hash_freefn) (struct rx_hash_item * it);
+#else
+typedef int (*rx_hash_eq)();
+typedef struct rx_hash * (*rx_alloc_hash)();
+typedef void (*rx_free_hash)();
+typedef struct rx_hash_item * (*rx_alloc_hash_item)();
+typedef void (*rx_free_hash_item)();
+typedef void (*rx_hash_freefn) ();
+#endif
+
+struct rx_hash_rules
+{
+  rx_hash_eq eq;
+  rx_alloc_hash hash_alloc;
+  rx_free_hash free_hash;
+  rx_alloc_hash_item hash_item_alloc;
+  rx_free_hash_item free_hash_item;
+};
+
+\f
+#ifdef __STDC__
+extern struct rx_hash_item * rx_hash_find (struct rx_hash * table,
+                                                  unsigned long hash,
+                                                  void * value,
+                                                  struct rx_hash_rules * rules);
+extern struct rx_hash_item * rx_hash_store (struct rx_hash * table,
+                                                   unsigned long hash,
+                                                   void * value,
+                                                   struct rx_hash_rules * rules);
+extern void rx_hash_free (struct rx_hash_item * it, struct rx_hash_rules * rules);
+extern void rx_free_hash_table (struct rx_hash * tab, rx_hash_freefn freefn,
+                                       struct rx_hash_rules * rules);
+extern int rx_count_hash_nodes (struct rx_hash * st);
+
+#else /* STDC */
+extern struct rx_hash_item * rx_hash_find ();
+extern struct rx_hash_item * rx_hash_store ();
+extern void rx_hash_free ();
+extern void rx_free_hash_table ();
+extern int rx_count_hash_nodes ();
+
+#endif /* STDC */
+
+
+
+
+
+#endif  /* RXHASHH */
+
diff --git a/rx/rxnfa.c b/rx/rxnfa.c
new file mode 100644 (file)
index 0000000..d67dd0e
--- /dev/null
@@ -0,0 +1,853 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+/*
+ * Tom Lord (lord@cygnus.com, lord@gnu.ai.mit.edu)
+ */
+\f
+
+#include "rxall.h"
+#include "rxnfa.h"
+
+\f
+
+#define remalloc(M, S) (M ? realloc (M, S) : malloc (S))
+
+\f
+/* {Low Level Data Structure Code}
+ */
+
+/* Constructs a new nfa node. */
+#ifdef __STDC__
+struct rx_nfa_state *
+rx_nfa_state (struct rx *rx)
+#else
+struct rx_nfa_state *
+rx_nfa_state (rx)
+     struct rx *rx;
+#endif
+{
+  struct rx_nfa_state * n = (struct rx_nfa_state *)malloc (sizeof (*n));
+  if (!n)
+    return 0;
+  rx_bzero ((char *)n, sizeof (*n));
+  n->next = rx->nfa_states;
+  rx->nfa_states = n;
+  return n;
+}
+
+
+#ifdef __STDC__
+static void
+rx_free_nfa_state (struct rx_nfa_state * n)
+#else
+static void
+rx_free_nfa_state (n)
+  struct rx_nfa_state * n;
+#endif
+{
+  free ((char *)n);
+}
+
+
+/* This adds an edge between two nodes, but doesn't initialize the 
+ * edge label.
+ */
+
+#ifdef __STDC__
+struct rx_nfa_edge * 
+rx_nfa_edge (struct rx *rx,
+            enum rx_nfa_etype type,
+            struct rx_nfa_state *start,
+            struct rx_nfa_state *dest)
+#else
+struct rx_nfa_edge * 
+rx_nfa_edge (rx, type, start, dest)
+     struct rx *rx;
+     enum rx_nfa_etype type;
+     struct rx_nfa_state *start;
+     struct rx_nfa_state *dest;
+#endif
+{
+  struct rx_nfa_edge *e;
+  e = (struct rx_nfa_edge *)malloc (sizeof (*e));
+  if (!e)
+    return 0;
+  e->next = start->edges;
+  start->edges = e;
+  e->type = type;
+  e->dest = dest;
+  return e;
+}
+
+
+#ifdef __STDC__
+static void
+rx_free_nfa_edge (struct rx_nfa_edge * e)
+#else
+static void
+rx_free_nfa_edge (e)
+     struct rx_nfa_edge * e;
+#endif
+{
+  free ((char *)e);
+}
+
+
+/* This constructs a POSSIBLE_FUTURE, which is a kind epsilon-closure
+ * of an NFA.  These are added to an nfa automaticly by eclose_nfa.
+ */  
+
+#ifdef __STDC__
+static struct rx_possible_future * 
+rx_possible_future (struct rx * rx,
+                struct rx_se_list * effects)
+#else
+static struct rx_possible_future * 
+rx_possible_future (rx, effects)
+     struct rx * rx;
+     struct rx_se_list * effects;
+#endif
+{
+  struct rx_possible_future *ec;
+  ec = (struct rx_possible_future *) malloc (sizeof (*ec));
+  if (!ec)
+    return 0;
+  ec->destset = 0;
+  ec->next = 0;
+  ec->effects = effects;
+  return ec;
+}
+
+
+#ifdef __STDC__
+static void
+rx_free_possible_future (struct rx_possible_future * pf)
+#else
+static void
+rx_free_possible_future (pf)
+     struct rx_possible_future * pf;
+#endif
+{
+  free ((char *)pf);
+}
+
+
+#ifdef __STDC__
+static void
+rx_free_nfa_graph (struct rx *rx)
+#else
+static void
+rx_free_nfa_graph (rx)
+     struct rx *rx;
+#endif
+{
+  while (rx->nfa_states)
+    {
+      while (rx->nfa_states->edges)
+       {
+         switch (rx->nfa_states->edges->type)
+           {
+           case ne_cset:
+             rx_free_cset (rx->nfa_states->edges->params.cset);
+             break;
+           default:
+             break;
+           }
+         {
+           struct rx_nfa_edge * e;
+           e = rx->nfa_states->edges;
+           rx->nfa_states->edges = rx->nfa_states->edges->next;
+           rx_free_nfa_edge (e);
+         }
+       } /* while (rx->nfa_states->edges) */
+      {
+       /* Iterate over the partial epsilon closures of rx->nfa_states */
+       struct rx_possible_future * pf = rx->nfa_states->futures;
+       while (pf)
+         {
+           struct rx_possible_future * pft = pf;
+           pf = pf->next;
+           rx_free_possible_future (pft);
+         }
+      }
+      {
+       struct rx_nfa_state *n;
+       n = rx->nfa_states;
+       rx->nfa_states = rx->nfa_states->next;
+       rx_free_nfa_state (n);
+      }
+    }
+}
+
+
+\f
+/* {Translating a Syntax Tree into an NFA}
+ *
+ */
+
+
+/* This is the Thompson regexp->nfa algorithm. 
+ * It is modified to allow for `side-effect epsilons.'  Those are
+ * edges that are taken whenever a similarly placed epsilon edge 
+ * would be, but which also imply that some side effect occurs 
+ * when the edge is taken.
+ *
+ * Side effects are used to model parts of the pattern langauge 
+ * that are not regular.
+ */
+
+#ifdef __STDC__
+int
+rx_build_nfa (struct rx *rx,
+             struct rexp_node *rexp,
+             struct rx_nfa_state **start,
+             struct rx_nfa_state **end)
+#else
+int
+rx_build_nfa (rx, rexp, start, end)
+     struct rx *rx;
+     struct rexp_node *rexp;
+     struct rx_nfa_state **start;
+     struct rx_nfa_state **end;
+#endif
+{
+  struct rx_nfa_edge *edge;
+
+  /* Start & end nodes may have been allocated by the caller. */
+  *start = *start ? *start : rx_nfa_state (rx);
+
+  if (!*start)
+    return 0;
+
+  if (!rexp)
+    {
+      *end = *start;
+      return 1;
+    }
+
+  *end = *end ? *end : rx_nfa_state (rx);
+
+  if (!*end)
+    {
+      rx_free_nfa_state (*start);
+      return 0;
+    }
+
+  switch (rexp->type)
+    {
+    case r_cset:
+      edge = rx_nfa_edge (rx, ne_cset, *start, *end);
+      (*start)->has_cset_edges = 1;
+      if (!edge)
+       return 0;
+      edge->params.cset = rx_copy_cset (rx->local_cset_size,
+                                       rexp->params.cset);
+      if (!edge->params.cset)
+       {
+         rx_free_nfa_edge (edge);
+         return 0;
+       }
+      return 1;
+
+    case r_string:
+      {
+       if (rexp->params.cstr.len == 1)
+         {
+           edge = rx_nfa_edge (rx, ne_cset, *start, *end);
+           (*start)->has_cset_edges = 1;
+           if (!edge)
+             return 0;
+           edge->params.cset = rx_cset (rx->local_cset_size);
+           if (!edge->params.cset)
+             {
+               rx_free_nfa_edge (edge);
+               return 0;
+             }
+           RX_bitset_enjoin (edge->params.cset, rexp->params.cstr.contents[0]);
+           return 1;
+         }
+       else
+         {
+           struct rexp_node copied;
+           struct rx_nfa_state * shared;
+
+           copied = *rexp;
+           shared = 0;
+           copied.params.cstr.len--;
+           copied.params.cstr.contents++;
+           if (!rx_build_nfa (rx, &copied, &shared, end))
+             return 0;
+           copied.params.cstr.len = 1;
+           copied.params.cstr.contents--;
+           return rx_build_nfa (rx, &copied, start, &shared);
+         }
+      }
+    case r_opt:
+      return (rx_build_nfa (rx, rexp->params.pair.left, start, end)
+             && rx_nfa_edge (rx, ne_epsilon, *start, *end));
+
+    case r_plus:
+      {
+       struct rx_nfa_state * star_start = 0;
+       struct rx_nfa_state * star_end = 0;
+       struct rx_nfa_state * shared;
+
+       shared = 0;
+       if (!rx_build_nfa (rx, rexp->params.pair.left, start, &shared))
+         return 0;
+       return (rx_build_nfa (rx, rexp->params.pair.left,
+                             &star_start, &star_end)
+               && star_start
+               && star_end
+               && rx_nfa_edge (rx, ne_epsilon, star_start, star_end)
+               && rx_nfa_edge (rx, ne_epsilon, shared, star_start)
+               && rx_nfa_edge (rx, ne_epsilon, star_end, *end)
+               && rx_nfa_edge (rx, ne_epsilon, star_end, star_start));
+      }
+
+    case r_interval:
+    case r_star:
+      {
+       struct rx_nfa_state * star_start = 0;
+       struct rx_nfa_state * star_end = 0;
+       return (rx_build_nfa (rx, rexp->params.pair.left,
+                             &star_start, &star_end)
+               && star_start
+               && star_end
+               && rx_nfa_edge (rx, ne_epsilon, star_start, star_end)
+               && rx_nfa_edge (rx, ne_epsilon, *start, star_start)
+               && rx_nfa_edge (rx, ne_epsilon, star_end, *end)
+
+               && rx_nfa_edge (rx, ne_epsilon, star_end, star_start));
+      }
+
+    case r_cut:
+      {
+       struct rx_nfa_state * cut_end = 0;
+
+       cut_end = rx_nfa_state (rx);
+       if (!(cut_end && rx_nfa_edge (rx, ne_epsilon, *start, cut_end)))
+         {
+           rx_free_nfa_state (*start);
+           rx_free_nfa_state (*end);
+           if (cut_end)
+             rx_free_nfa_state (cut_end);
+           return 0;
+         }
+       cut_end->is_final = rexp->params.intval;
+       return 1;
+      }
+
+    case r_parens:
+      return rx_build_nfa (rx, rexp->params.pair.left, start, end);
+
+    case r_concat:
+      {
+       struct rx_nfa_state *shared = 0;
+       return
+         (rx_build_nfa (rx, rexp->params.pair.left, start, &shared)
+          && rx_build_nfa (rx, rexp->params.pair.right, &shared, end));
+      }
+
+    case r_alternate:
+      {
+       struct rx_nfa_state *ls = 0;
+       struct rx_nfa_state *le = 0;
+       struct rx_nfa_state *rs = 0;
+       struct rx_nfa_state *re = 0;
+       return (rx_build_nfa (rx, rexp->params.pair.left, &ls, &le)
+               && rx_build_nfa (rx, rexp->params.pair.right, &rs, &re)
+               && rx_nfa_edge (rx, ne_epsilon, *start, ls)
+               && rx_nfa_edge (rx, ne_epsilon, *start, rs)
+               && rx_nfa_edge (rx, ne_epsilon, le, *end)
+               && rx_nfa_edge (rx, ne_epsilon, re, *end));
+      }
+
+    case r_context:
+      edge = rx_nfa_edge (rx, ne_side_effect, *start, *end);
+      if (!edge)
+       return 0;
+      edge->params.side_effect = (void *)rexp->params.intval;
+      return 1;
+    }
+
+  /* this should never happen */
+  return 0;
+}
+
+\f
+/* {Low Level Data Structures for the Static NFA->SuperNFA Analysis}
+ *
+ * There are side effect lists -- lists of side effects occuring
+ * along an uninterrupted, acyclic path of side-effect epsilon edges.
+ * Such paths are collapsed to single edges in the course of computing
+ * epsilon closures.  The resulting single edges are labled with a list 
+ * of all the side effects from the original multi-edge path.  Equivalent
+ * lists of side effects are made == by the constructors below.
+ *
+ * There are also nfa state sets.  These are used to hold a list of all
+ * states reachable from a starting state for a given type of transition
+ * and side effect list.   These are also hash-consed.
+ */
+
+
+
+/* The next several functions compare, construct, etc. lists of side
+ * effects.  See ECLOSE_NFA (below) for details.
+ */
+
+/* Ordering of rx_se_list
+ * (-1, 0, 1 return value convention).
+ */
+
+#ifdef __STDC__
+static int 
+se_list_cmp (void * va, void * vb)
+#else
+static int 
+se_list_cmp (va, vb)
+     void * va;
+     void * vb;
+#endif
+{
+  struct rx_se_list * a = (struct rx_se_list *)va;
+  struct rx_se_list * b = (struct rx_se_list *)vb;
+
+  return ((va == vb)
+         ? 0
+         : (!va
+            ? -1
+            : (!vb
+               ? 1
+               : ((long)a->car < (long)b->car
+                  ? 1
+                  : ((long)a->car > (long)b->car
+                     ? -1
+                     : se_list_cmp ((void *)a->cdr, (void *)b->cdr))))));
+}
+
+
+#ifdef __STDC__
+static int 
+se_list_equal (void * va, void * vb)
+#else
+static int 
+se_list_equal (va, vb)
+     void * va;
+     void * vb;
+#endif
+{
+  return !(se_list_cmp (va, vb));
+}
+
+static struct rx_hash_rules se_list_hash_rules = { se_list_equal, 0, 0, 0, 0 };
+
+
+#ifdef __STDC__
+static struct rx_se_list * 
+side_effect_cons (struct rx * rx,
+                 void * se, struct rx_se_list * list)
+#else
+static struct rx_se_list * 
+side_effect_cons (rx, se, list)
+     struct rx * rx;
+     void * se;
+     struct rx_se_list * list;
+#endif
+{
+  struct rx_se_list * l;
+  l = ((struct rx_se_list *) malloc (sizeof (*l)));
+  if (!l)
+    return 0;
+  l->car = se;
+  l->cdr = list;
+  return l;
+}
+
+
+#ifdef __STDC__
+static struct rx_se_list *
+hash_cons_se_prog (struct rx * rx,
+                  struct rx_hash * memo,
+                  void * car, struct rx_se_list * cdr)
+#else
+static struct rx_se_list *
+hash_cons_se_prog (rx, memo, car, cdr)
+     struct rx * rx;
+     struct rx_hash * memo;
+     void * car;
+     struct rx_se_list * cdr;
+#endif
+{
+  long hash = (long)car ^ (long)cdr;
+  struct rx_se_list template;
+
+  template.car = car;
+  template.cdr = cdr;
+  {
+    struct rx_hash_item * it = rx_hash_store (memo, hash,
+                                             (void *)&template,
+                                             &se_list_hash_rules);
+    if (!it)
+      return 0;
+    if (it->data == (void *)&template)
+      {
+       struct rx_se_list * consed;
+       consed = (struct rx_se_list *) malloc (sizeof (*consed));
+       *consed = template;
+       it->data = (void *)consed;
+      }
+    return (struct rx_se_list *)it->data;
+  }
+}
+     
+
+#ifdef __STDC__
+static struct rx_se_list *
+hash_se_prog (struct rx * rx, struct rx_hash * memo, struct rx_se_list * prog)
+#else
+static struct rx_se_list *
+hash_se_prog (rx, memo, prog)
+     struct rx * rx;
+     struct rx_hash * memo;
+     struct rx_se_list * prog;
+#endif
+{
+  struct rx_se_list * answer = 0;
+  while (prog)
+    {
+      answer = hash_cons_se_prog (rx, memo, prog->car, answer);
+      if (!answer)
+       return 0;
+      prog = prog->cdr;
+    }
+  return answer;
+}
+
+
+
+/* {Constructors, etc. for NFA State Sets}
+ */
+
+#ifdef __STDC__
+static int 
+nfa_set_cmp (void * va, void * vb)
+#else
+static int 
+nfa_set_cmp (va, vb)
+     void * va;
+     void * vb;
+#endif
+{
+  struct rx_nfa_state_set * a = (struct rx_nfa_state_set *)va;
+  struct rx_nfa_state_set * b = (struct rx_nfa_state_set *)vb;
+
+  return ((va == vb)
+         ? 0
+         : (!va
+            ? -1
+            : (!vb
+               ? 1
+               : (a->car->id < b->car->id
+                  ? 1
+                  : (a->car->id > b->car->id
+                     ? -1
+                     : nfa_set_cmp ((void *)a->cdr, (void *)b->cdr))))));
+}
+
+#ifdef __STDC__
+static int 
+nfa_set_equal (void * va, void * vb)
+#else
+static int 
+nfa_set_equal (va, vb)
+     void * va;
+     void * vb;
+#endif
+{
+  return !nfa_set_cmp (va, vb);
+}
+
+static struct rx_hash_rules nfa_set_hash_rules = { nfa_set_equal, 0, 0, 0, 0 };
+
+
+#ifdef __STDC__
+static struct rx_nfa_state_set * 
+nfa_set_cons (struct rx * rx,
+             struct rx_hash * memo, struct rx_nfa_state * state,
+             struct rx_nfa_state_set * set)
+#else
+static struct rx_nfa_state_set * 
+nfa_set_cons (rx, memo, state, set)
+     struct rx * rx;
+     struct rx_hash * memo;
+     struct rx_nfa_state * state;
+     struct rx_nfa_state_set * set;
+#endif
+{
+  struct rx_nfa_state_set template;
+  struct rx_hash_item * node;
+  template.car = state;
+  template.cdr = set;
+  node = rx_hash_store (memo,
+                       (((long)state) >> 8) ^ (long)set,
+                       &template, &nfa_set_hash_rules);
+  if (!node)
+    return 0;
+  if (node->data == &template)
+    {
+      struct rx_nfa_state_set * l;
+      l = (struct rx_nfa_state_set *) malloc (sizeof (*l));
+      node->data = (void *) l;
+      if (!l)
+       return 0;
+      *l = template;
+    }
+  return (struct rx_nfa_state_set *)node->data;
+}
+
+
+#ifdef __STDC__
+static struct rx_nfa_state_set * 
+nfa_set_enjoin (struct rx * rx,
+               struct rx_hash * memo, struct rx_nfa_state * state,
+               struct rx_nfa_state_set * set)
+#else
+static struct rx_nfa_state_set * 
+nfa_set_enjoin (rx, memo, state, set)
+     struct rx * rx;
+     struct rx_hash * memo;
+     struct rx_nfa_state * state;
+     struct rx_nfa_state_set * set;
+#endif
+{
+  if (!set || (state->id < set->car->id))
+    return nfa_set_cons (rx, memo, state, set);
+  if (state->id == set->car->id)
+    return set;
+  else
+    {
+      struct rx_nfa_state_set * newcdr
+       = nfa_set_enjoin (rx, memo, state, set->cdr);
+      if (newcdr != set->cdr)
+       set = nfa_set_cons (rx, memo, set->car, newcdr);
+      return set;
+    }
+}
+
+\f
+/* {Computing Epsilon/Side-effect Closures.}
+ */
+
+struct eclose_frame
+{
+  struct rx_se_list *prog_backwards;
+};
+
+
+/* This is called while computing closures for "outnode".
+ * The current node in the traversal is "node".
+ * "frame" contains a list of a all side effects between 
+ * "outnode" and "node" from most to least recent.
+ *
+ * Explores forward from "node", adding new possible
+ * futures to outnode.
+ *
+ * Returns 0 on allocation failure.
+ */
+
+#ifdef __STDC__
+static int 
+eclose_node (struct rx *rx, struct rx_nfa_state *outnode,
+            struct rx_nfa_state *node, struct eclose_frame *frame)
+#else
+static int 
+eclose_node (rx, outnode, node, frame)
+     struct rx *rx;
+     struct rx_nfa_state *outnode;
+     struct rx_nfa_state *node;
+     struct eclose_frame *frame;
+#endif
+{
+  struct rx_nfa_edge *e = node->edges;
+
+  /* For each node, we follow all epsilon paths to build the closure.
+   * The closure omits nodes that have only epsilon edges.
+   * The closure is split into partial closures -- all the states in
+   * a partial closure are reached by crossing the same list of
+   * of side effects (though not necessarily the same path).
+   */
+  if (node->mark)
+    return 1;
+  node->mark = 1;
+
+  /* If "node" has more than just epsilon and 
+   * and side-effect transitions (id >= 0), or is final,
+   * then it has to be added to the possible futures
+   * of "outnode".
+   */
+  if (node->id >= 0 || node->is_final)
+    {
+      struct rx_possible_future **ec;
+      struct rx_se_list * prog_in_order;
+      int cmp;
+
+      prog_in_order = ((struct rx_se_list *)hash_se_prog (rx,
+                                                         &rx->se_list_memo,
+                                                         frame->prog_backwards));
+
+      ec = &outnode->futures;
+
+      while (*ec)
+       {
+         cmp = se_list_cmp ((void *)(*ec)->effects, (void *)prog_in_order);
+         if (cmp <= 0)
+           break;
+         ec = &(*ec)->next;
+       }
+
+      if (!*ec || (cmp < 0))
+       {
+         struct rx_possible_future * pf;
+         pf = rx_possible_future (rx, prog_in_order);
+         if (!pf)
+           return 0;
+         pf->next = *ec;
+         *ec = pf;
+       }
+      if (node->id >= 0)
+       {
+         (*ec)->destset = nfa_set_enjoin (rx, &rx->set_list_memo,
+                                          node, (*ec)->destset);
+         if (!(*ec)->destset)
+           return 0;
+       }
+    }
+
+  /* Recurse on outgoing epsilon and side effect nodes.
+   */
+  while (e)
+    {
+      switch (e->type)
+       {
+       case ne_epsilon:
+         if (!eclose_node (rx, outnode, e->dest, frame))
+           return 0;
+         break;
+       case ne_side_effect:
+         {
+           frame->prog_backwards = side_effect_cons (rx, 
+                                                     e->params.side_effect,
+                                                     frame->prog_backwards);
+           if (!frame->prog_backwards)
+             return 0;
+           if (!eclose_node (rx, outnode, e->dest, frame))
+             return 0;
+           {
+             struct rx_se_list * dying = frame->prog_backwards;
+             frame->prog_backwards = frame->prog_backwards->cdr;
+             free ((char *)dying);
+           }
+           break;
+         }
+       default:
+         break;
+       }
+      e = e->next;
+    }
+  node->mark = 0;
+  return 1;
+}
+
+
+#ifdef __STDC__
+struct rx_possible_future *
+rx_state_possible_futures (struct rx * rx, struct rx_nfa_state * n)
+#else
+struct rx_possible_future *
+rx_state_possible_futures (rx, n)
+     struct rx * rx;
+     struct rx_nfa_state * n;
+#endif
+{
+  if (n->futures_computed)
+    return n->futures;
+  else
+    {
+      struct eclose_frame frame;
+      frame.prog_backwards = 0;
+      if (!eclose_node (rx, n, n, &frame))
+       return 0;
+      else
+       {
+         n->futures_computed = 1;
+         return n->futures;
+       }
+    }
+}
+
+
+\f
+/* {Storing the NFA in a Contiguous Region of Memory}
+ */
+
+
+
+#ifdef __STDC__
+static void 
+se_memo_freer (struct rx_hash_item * node)
+#else
+static void 
+se_memo_freer (node)
+     struct rx_hash_item * node;
+#endif
+{
+  free ((char *)node->data);
+}
+
+
+#ifdef __STDC__
+static void 
+nfa_set_freer (struct rx_hash_item * node)
+#else
+static void 
+nfa_set_freer (node)
+     struct rx_hash_item * node;
+#endif
+{
+  free ((char *)node->data);
+}
+
+#ifdef __STDC__
+void
+rx_free_nfa (struct rx *rx)
+#else
+void
+rx_free_nfa (rx)
+     struct rx *rx;
+#endif
+{
+  rx_free_hash_table (&rx->se_list_memo, se_memo_freer, &se_list_hash_rules);
+  rx_bzero ((char *)&rx->se_list_memo, sizeof (rx->se_list_memo));
+  rx_free_hash_table (&rx->set_list_memo, nfa_set_freer, &nfa_set_hash_rules);
+  rx_bzero ((char *)&rx->set_list_memo, sizeof (rx->set_list_memo));
+  rx_free_nfa_graph (rx);
+}
diff --git a/rx/rxnfa.h b/rx/rxnfa.h
new file mode 100644 (file)
index 0000000..82a135b
--- /dev/null
@@ -0,0 +1,232 @@
+/* classes: h_files */
+
+#ifndef RXNFAH
+#define RXNFAH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+/*
+ * Tom Lord (lord@cygnus.com, lord@gnu.ai.mit.edu)
+ */
+\f
+#include "_rx.h"
+#include "rxnode.h"
+\f
+
+/* NFA
+ *
+ * A syntax tree is compiled into an NFA.  This page defines the structure
+ * of that NFA.
+ */
+
+struct rx_nfa_state
+{
+  /* These are kept in a list as the NFA is being built. 
+   * Here is the link.
+   */
+  struct rx_nfa_state *next;
+
+  /* After the NFA is built, states are given integer id's.
+   * States whose outgoing transitions are all either epsilon or 
+   * side effect edges are given ids less than 0.  Other states
+   * are given successive non-negative ids starting from 0.
+   *
+   * Here is the id for this state:
+   */
+  int id;
+
+  /* The list of NFA edges that go from this state to some other. */
+  struct rx_nfa_edge *edges;
+
+  /* If you land in this state, then you implicitly land
+   * in all other states reachable by only epsilon translations.
+   * Call the set of maximal loop-less paths to such states the 
+   * epsilon closure of this state.
+   *
+   * There may be other states that are reachable by a mixture of
+   * epsilon and side effect edges.  Consider the set of maximal loop-less
+   * paths of that sort from this state.  Call it the epsilon-side-effect
+   * closure of the state.
+   * 
+   * The epsilon closure of the state is a subset of the epsilon-side-
+   * effect closure.  It consists of all the paths that contain 
+   * no side effects -- only epsilon edges.
+   * 
+   * The paths in the epsilon-side-effect closure  can be partitioned
+   * into equivalance sets. Two paths are equivalant if they have the
+   * same set of side effects, in the same order.  The epsilon-closure
+   * is one of these equivalance sets.  Let's call these equivalance
+   * sets: observably equivalant path sets.  That name is chosen
+   * because equivalance of two paths means they cause the same side
+   * effects -- so they lead to the same subsequent observations other
+   * than that they may wind up in different target states.
+   *
+   * The superstate nfa, which is derived from this nfa, is based on
+   * the observation that all of the paths in an observably equivalant
+   * path set can be explored at the same time, provided that the
+   * matcher keeps track not of a single nfa state, but of a set of
+   * states.   In particular, after following all the paths in an
+   * observably equivalant set, you wind up at a set of target states.
+   * That set of target states corresponds to one state in the
+   * superstate NFA.
+   *
+   * Staticly, before matching begins, it is convenient to analyze the
+   * nfa.  Each state is labeled with a list of the observably
+   * equivalant path sets who's union covers all the
+   * epsilon-side-effect paths beginning in this state.  This list is
+   * called the possible futures of the state.
+   *
+   * A trivial example is this NFA:
+   *             s1
+   *         A  --->  B
+   *
+   *             s2  
+   *            --->  C
+   *
+   *             epsilon           s1
+   *            --------->  D   ------> E
+   * 
+   * 
+   * In this example, A has two possible futures.
+   * One invokes the side effect `s1' and contains two paths,
+   * one ending in state B, the other in state E.
+   * The other invokes the side effect `s2' and contains only
+   * one path, landing in state C.
+   *
+   * Here is a list of the possible futures of this state:
+   */
+  struct rx_possible_future *futures;
+  int futures_computed:1;
+
+
+  /* There is exactly one distinguished starting state in every NFA: */
+  unsigned int is_start:1;
+
+  int has_cset_edges:1;
+
+  /* There may be many final states if the "cut" operator was used.
+   * each will have a different non-0 value for this field:
+   */
+  int is_final;
+
+
+  /* These are used internally during NFA construction... */
+  unsigned int eclosure_needed:1;
+  unsigned int mark:1;
+};
+
+
+/* An edge in an NFA is typed: 
+ */
+enum rx_nfa_etype
+{
+  /* A cset edge is labled with a set of characters one of which
+   * must be matched for the edge to be taken.
+   */
+  ne_cset,
+
+  /* An epsilon edge is taken whenever its starting state is
+   * reached. 
+   */
+  ne_epsilon,
+
+  /* A side effect edge is taken whenever its starting state is
+   * reached.  Side effects may cause the match to fail or the
+   * position of the matcher to advance.
+   */
+  ne_side_effect
+};
+
+struct rx_nfa_edge
+{
+  struct rx_nfa_edge *next;
+  enum rx_nfa_etype type;
+  struct rx_nfa_state *dest;
+  union
+  {
+    rx_Bitset cset;
+    void * side_effect;
+  } params;
+};
+
+
+
+/* A possible future consists of a list of side effects
+ * and a set of destination states.  Below are their
+ * representations.  These structures are hash-consed so
+ * that lists with the same elements share a representation
+ * (their addresses are ==).
+ */
+
+struct rx_nfa_state_set
+{
+  struct rx_nfa_state * car;
+  struct rx_nfa_state_set * cdr;
+};
+
+struct rx_se_list
+{
+  void * car;
+  struct rx_se_list * cdr;
+};
+
+struct rx_possible_future
+{
+  struct rx_possible_future *next;
+  struct rx_se_list * effects;
+  struct rx_nfa_state_set * destset;
+};
+
+
+
+\f
+#ifdef __STDC__
+extern struct rx_nfa_state * rx_nfa_state (struct rx *rx);
+extern struct rx_nfa_edge * rx_nfa_edge (struct rx *rx,
+                                        enum rx_nfa_etype type,
+                                        struct rx_nfa_state *start,
+                                        struct rx_nfa_state *dest);
+extern int rx_build_nfa (struct rx *rx,
+                        struct rexp_node *rexp,
+                        struct rx_nfa_state **start,
+                        struct rx_nfa_state **end);
+extern struct rx_possible_future * rx_state_possible_futures (struct rx * rx, struct rx_nfa_state * n);
+extern void rx_free_nfa (struct rx *rx);
+
+#else /* STDC */
+extern struct rx_nfa_state * rx_nfa_state ();
+extern struct rx_nfa_edge * rx_nfa_edge ();
+extern int rx_build_nfa ();
+extern struct rx_possible_future * rx_state_possible_futures ();
+extern void rx_free_nfa ();
+
+#endif /* STDC */
+
+
+
+
+
+
+
+
+
+
+
+#endif  /* RXNFAH */
diff --git a/rx/rxnode.c b/rx/rxnode.c
new file mode 100644 (file)
index 0000000..a4e1cca
--- /dev/null
@@ -0,0 +1,545 @@
+/* classes: src_files */
+
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include "rxall.h"
+#include "rxnode.h"
+\f
+
+
+#define INITSIZE   8
+#define EXPANDSIZE 8
+
+#ifdef __STDC__
+static int
+rx_init_string(struct rx_string *thisone, char first)
+#else
+static int
+rx_init_string(thisone, first)
+     struct rx_string *thisone;
+     char first;
+#endif
+{
+  char *tmp;
+
+  tmp = (char *) malloc (INITSIZE);
+
+  if(!tmp)
+    return -1;
+
+  thisone->contents    = tmp;
+  thisone->contents[0] = first;
+  thisone->reallen     = INITSIZE;
+  thisone->len         = 1;
+  return 0;
+}
+
+#ifdef __STDC__
+static void
+rx_free_string (struct rx_string *junk)
+#else
+static void
+rx_free_string (junk)
+     struct rx_string *junk;
+#endif
+{
+  free (junk->contents);
+  junk->len = junk->reallen = 0;
+  junk->contents = 0;
+}
+
+
+#ifdef __STDC__
+int
+rx_adjoin_string (struct rx_string *str, char c)
+#else
+int
+rx_adjoin_string (str, c)
+     struct rx_string *str;
+     char c;
+#endif
+{
+  if (!str->contents)
+    return rx_init_string (str, c);
+
+  if (str->len == str->reallen)
+    {
+      char *temp;
+      temp = (char *) realloc (str->contents, str->reallen + EXPANDSIZE);
+
+      if(!temp)
+       return -1;
+
+      str->contents = temp;
+      str->reallen += EXPANDSIZE;
+    }
+
+  str->contents[str->len++] = c;
+  return 0;
+}
+
+
+#ifdef __STDC__
+static int
+rx_copy_string (struct rx_string *to, struct rx_string *from)
+#else
+static int
+rx_copy_string (to, from)
+       struct rx_string *to;
+       struct rx_string *from;
+#endif
+{
+  char *tmp;
+
+  if (from->len)
+    {
+      tmp = (char *) malloc (from->reallen);
+
+      if (!tmp)
+       return -1;
+    }
+
+  rx_free_string (to);
+  to->len      = from->len;
+  to->reallen  = from->reallen;
+  to->contents = tmp;
+
+  memcpy (to->contents, from->contents, from->reallen);
+
+  return 0;
+}
+
+
+#ifdef __STDC__
+static int
+rx_compare_rx_strings (struct rx_string *a, struct rx_string *b)
+#else
+static int
+rx_compare_rx_strings (a, b)
+     struct rx_string *a;
+     struct rx_string *b;
+#endif
+{
+  if (a->len != b->len)
+    return 0;
+
+  if (a->len)
+    return !memcmp (a->contents, b->contents, a->len);
+  else
+    return 1;                  /* trivial case: "" == "" */
+}
+
+
+#ifdef __STDC__
+static unsigned long
+rx_string_hash (unsigned long seed, struct rx_string *str)
+#else
+static unsigned long
+rx_string_hash (seed, str)
+     unsigned long seed;
+     struct rx_string *str;
+#endif
+{
+  /* From Tcl: */
+  unsigned long result;
+  int c;
+  char * string;
+  int len;
+
+  string = str->contents;
+  len = str->len;
+  result = seed;
+
+  while (len--)
+    {
+      c = *string;
+      string++;
+      result += (result<<3) + c;
+    }
+  return result;
+}
+
+
+\f
+
+#ifdef __STDC__
+struct rexp_node *
+rexp_node (int type)
+#else
+struct rexp_node *
+rexp_node (type)
+     int type;
+#endif
+{
+  struct rexp_node *n;
+
+  n = (struct rexp_node *) malloc (sizeof (*n));
+  rx_bzero ((char *)n, sizeof (*n));
+  if (n)
+    {
+      n->type = type;
+      n->id = -1;
+      n->refs = 1;
+    }
+  return n;
+}
+
+
+/* free_rexp_node assumes that the bitset passed to rx_mk_r_cset
+ * can be freed using rx_free_cset.
+ */
+
+#ifdef __STDC__
+struct rexp_node *
+rx_mk_r_cset (int type, int size, rx_Bitset b)
+#else
+struct rexp_node *
+rx_mk_r_cset (type, size, b)
+     int type;
+     int size;
+     rx_Bitset b;
+#endif
+{
+  struct rexp_node * n;
+  n = rexp_node (type);
+  if (n)
+    {
+      n->params.cset = b;
+      n->params.cset_size = size;
+    }
+  return n;
+}
+
+
+#ifdef __STDC__
+struct rexp_node *
+rx_mk_r_int (int type, int intval)
+#else
+struct rexp_node *
+rx_mk_r_int (type, intval)
+     int type;
+     int intval;
+#endif
+{
+  struct rexp_node * n;
+  n = rexp_node (type);
+  if (n)
+    n->params.intval = intval;
+  return n;
+}
+
+
+#ifdef __STDC__
+struct rexp_node *
+rx_mk_r_str (int type, char c)
+#else
+struct rexp_node *
+rx_mk_r_str (type, c)
+     int type;
+     char c;
+#endif
+{
+  struct rexp_node *n;
+  n = rexp_node (type);
+  if (n)
+    rx_init_string (&(n->params.cstr), c);
+  return n;
+}
+
+
+#ifdef __STDC__
+struct rexp_node *
+rx_mk_r_binop (int type, struct rexp_node * a, struct rexp_node * b)
+#else
+struct rexp_node *
+rx_mk_r_binop (type, a, b)
+     int type;
+     struct rexp_node * a;
+     struct rexp_node * b;
+#endif
+{
+  struct rexp_node * n = rexp_node (type);
+  if (n)
+    {
+      n->params.pair.left = a;
+      n->params.pair.right = b;
+    }
+  return n;
+}
+
+
+#ifdef __STDC__
+struct rexp_node *
+rx_mk_r_monop (int type, struct rexp_node * a)
+#else
+struct rexp_node *
+rx_mk_r_monop (type, a)
+     int type;
+     struct rexp_node * a;
+#endif
+{
+  return rx_mk_r_binop (type, a, 0);
+}
+
+
+#ifdef __STDC__
+void
+rx_free_rexp (struct rexp_node * node)
+#else
+void
+rx_free_rexp (node)
+     struct rexp_node * node;
+#endif
+{
+  if (node && !--node->refs)
+    {
+      if (node->params.cset)
+       rx_free_cset (node->params.cset);
+      if (node->params.cstr.reallen)
+       rx_free_string (&(node->params.cstr));
+      rx_free_rexp (node->params.pair.left);
+      rx_free_rexp (node->params.pair.right);
+      rx_free_rexp (node->simplified);
+      free ((char *)node);
+    }
+}
+
+#ifdef __STDC__
+void
+rx_save_rexp (struct rexp_node * node)
+#else
+void
+rx_save_rexp (node)
+     struct rexp_node * node;
+#endif
+{
+  if (node)
+    ++node->refs;
+}
+
+
+#ifdef __STDC__
+struct rexp_node * 
+rx_copy_rexp (int cset_size, struct rexp_node *node)
+#else
+struct rexp_node * 
+rx_copy_rexp (cset_size, node)
+     int cset_size;
+     struct rexp_node *node;
+#endif
+{
+  if (!node)
+    return 0;
+  else
+    {
+      struct rexp_node *n;
+      n = rexp_node (node->type);
+      if (!n)
+       return 0;
+
+      if (node->params.cset)
+       {
+         n->params.cset = rx_copy_cset (cset_size,
+                                        node->params.cset);
+         if (!n->params.cset)
+           {
+             rx_free_rexp (n);
+             return 0;
+           }
+       }
+
+      if (node->params.cstr.reallen)
+       if (rx_copy_string (&(n->params.cstr), &(node->params.cstr)))
+         {
+           rx_free_rexp(n);
+           return 0;
+         }
+
+      n->params.intval = node->params.intval;
+      n->params.intval2 = node->params.intval2;
+      n->params.pair.left = rx_copy_rexp (cset_size, node->params.pair.left);
+      n->params.pair.right = rx_copy_rexp (cset_size, node->params.pair.right);
+      if (   (node->params.pair.left && !n->params.pair.left)
+         || (node->params.pair.right && !n->params.pair.right))
+       {
+         rx_free_rexp  (n);
+         return 0;
+       }
+      n->id = node->id;
+      n->len = node->len;
+      n->observed = node->observed;
+      return n;
+    }
+}
+
+\f
+
+#ifdef __STDC__
+struct rexp_node * 
+rx_shallow_copy_rexp (int cset_size, struct rexp_node *node)
+#else
+struct rexp_node * 
+rx_shallow_copy_rexp (cset_size, node)
+     int cset_size;
+     struct rexp_node *node;
+#endif
+{
+  if (!node)
+    return 0;
+  else
+    {
+      struct rexp_node *n;
+      n = rexp_node (node->type);
+      if (!n)
+       return 0;
+
+      if (node->params.cset)
+       {
+         n->params.cset = rx_copy_cset (cset_size,
+                                        node->params.cset);
+         if (!n->params.cset)
+           {
+             rx_free_rexp (n);
+             return 0;
+           }
+       }
+
+      if (node->params.cstr.reallen)
+       if (rx_copy_string (&(n->params.cstr), &(node->params.cstr)))
+         {
+           rx_free_rexp(n);
+           return 0;
+         }
+
+      n->params.intval = node->params.intval;
+      n->params.intval2 = node->params.intval2;
+      n->params.pair.left = node->params.pair.left;
+      rx_save_rexp (node->params.pair.left);
+      n->params.pair.right = node->params.pair.right;
+      rx_save_rexp (node->params.pair.right);
+      n->id = node->id;
+      n->len = node->len;
+      n->observed = node->observed;
+      return n;
+    }
+}
+
+\f
+
+
+#ifdef __STDC__
+int
+rx_rexp_equal (struct rexp_node * a, struct rexp_node * b)
+#else
+int
+rx_rexp_equal (a, b)
+     struct rexp_node * a;
+     struct rexp_node * b;
+#endif
+{
+  int ret;
+
+  if (a == b)
+    return 1;
+
+  if ((a == 0) || (b == 0))
+    return 0;
+
+  if (a->type != b->type)
+    return 0;
+
+  switch (a->type)
+    {
+    case r_cset:
+      ret = (   (a->params.cset_size == b->params.cset_size)
+            && rx_bitset_is_equal (a->params.cset_size,
+                                   a->params.cset,
+                                   b->params.cset));
+      break;
+
+    case r_string:
+      ret = rx_compare_rx_strings (&(a->params.cstr), &(b->params.cstr));
+      break;
+
+    case r_cut:
+      ret = (a->params.intval == b->params.intval);
+      break;
+
+    case r_concat:
+    case r_alternate:
+      ret = (   rx_rexp_equal (a->params.pair.left, b->params.pair.left)
+            && rx_rexp_equal (a->params.pair.right, b->params.pair.right));
+      break;
+    case r_opt:
+    case r_star:
+    case r_plus:
+      ret = rx_rexp_equal (a->params.pair.left, b->params.pair.left);
+      break;
+    case r_interval:
+      ret = (   (a->params.intval == b->params.intval)
+            && (a->params.intval2 == b->params.intval2)
+            && rx_rexp_equal (a->params.pair.left, b->params.pair.left));
+      break;
+    case r_parens:
+      ret = (   (a->params.intval == b->params.intval)
+            && rx_rexp_equal (a->params.pair.left, b->params.pair.left));
+      break;
+
+    case r_context:
+      ret = (a->params.intval == b->params.intval);
+      break;
+    default:
+      return 0;
+    }
+  return ret;
+}
+
+
+\f
+
+
+#ifdef __STDC__
+unsigned long
+rx_rexp_hash (struct rexp_node * node, unsigned long seed)
+#else
+     unsigned long
+     rx_rexp_hash (node, seed)
+     struct rexp_node * node;
+     unsigned long seed;
+#endif
+{
+  if (!node)
+    return seed;
+
+  seed = rx_rexp_hash (node->params.pair.left, seed);
+  seed = rx_rexp_hash (node->params.pair.right, seed);
+  seed = rx_bitset_hash (node->params.cset_size, node->params.cset);
+  seed = rx_string_hash (seed, &(node->params.cstr));
+  seed += (seed << 3) + node->params.intval;
+  seed += (seed << 3) + node->params.intval2;
+  seed += (seed << 3) + node->type;
+  seed += (seed << 3) + node->id;
+#if 0
+  seed += (seed << 3) + node->len;
+  seed += (seed << 3) + node->observed;
+#endif
+  return seed;
+}
diff --git a/rx/rxnode.h b/rx/rxnode.h
new file mode 100644 (file)
index 0000000..ea3a1fc
--- /dev/null
@@ -0,0 +1,125 @@
+/* classes: h_files */
+
+#ifndef RXNODEH
+#define RXNODEH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+/*
+ * Tom Lord (lord@cygnus.com, lord@gnu.ai.mit.edu)
+ */
+
+\f
+#include "rxbitset.h"
+#include "rxcset.h"
+
+\f
+
+enum rexp_node_type
+{
+  r_cset = 0,                  /* Match from a character set. `a' or `[a-z]'*/
+  r_concat = 1,                        /* Concat two subexpressions.   `ab' */
+  r_alternate = 2,             /* Choose one of two subexpressions. `a\|b' */
+  r_opt = 3,                   /* Optional subexpression. `a?' */
+  r_star = 4,                  /* Repeated subexpression. `a*' */
+  r_plus = 5,                  /* Nontrivially repeated subexpression. `a+' */
+  r_string = 6,                        /* Shorthand for a concatenation of characters */
+  r_cut = 7,                   /* Generates a tagged, final nfa state. */
+
+  /* see RX_regular_node_type */
+
+  r_interval = 8,              /* Counted subexpression.  `a{4, 1000}' */
+  r_parens = 9,                        /* Parenthesized subexpression */
+  r_context = 10               /* Context-sensative operator such as "^" */
+};
+
+#define RX_regular_node_type(T)  ((T) <= r_interval)
+
+
+
+struct rx_string
+{
+  unsigned long len;
+  unsigned long reallen;
+  unsigned char *contents;
+};
+
+struct rexp_node
+{
+  int refs;
+  enum rexp_node_type type;
+  struct
+  {
+    int cset_size;
+    rx_Bitset cset;
+    int intval;
+    int intval2;
+    struct
+      {
+       struct rexp_node *left;
+       struct rexp_node *right;
+      } pair;
+    struct rx_string cstr;
+  } params;
+  int id;
+  int len;
+  int observed;
+  struct rexp_node * simplified;
+  struct rx_cached_rexp * cr;
+};
+
+
+\f
+#ifdef __STDC__
+extern int rx_adjoin_string (struct rx_string *str, char c);
+extern struct rexp_node * rexp_node (int type);
+extern struct rexp_node * rx_mk_r_cset (int type, int size, rx_Bitset b);
+extern struct rexp_node * rx_mk_r_int (int type, int intval);
+extern struct rexp_node * rx_mk_r_str (int type, char c);
+extern struct rexp_node * rx_mk_r_binop (int type, struct rexp_node * a, struct rexp_node * b);
+extern struct rexp_node * rx_mk_r_monop (int type, struct rexp_node * a);
+extern void rx_free_rexp (struct rexp_node * node);
+extern void rx_save_rexp (struct rexp_node * node);
+extern struct rexp_node * rx_copy_rexp (int cset_size, struct rexp_node *node);
+extern struct rexp_node * rx_shallow_copy_rexp (int cset_size, struct rexp_node *node);
+extern int rx_rexp_equal (struct rexp_node * a, struct rexp_node * b);
+extern unsigned long rx_rexp_hash (struct rexp_node * node, unsigned long seed);
+
+#else /* STDC */
+extern int rx_adjoin_string ();
+extern struct rexp_node * rexp_node ();
+extern struct rexp_node * rx_mk_r_cset ();
+extern struct rexp_node * rx_mk_r_int ();
+extern struct rexp_node * rx_mk_r_str ();
+extern struct rexp_node * rx_mk_r_binop ();
+extern struct rexp_node * rx_mk_r_monop ();
+extern void rx_free_rexp ();
+extern void rx_save_rexp ();
+extern struct rexp_node * rx_copy_rexp ();
+extern struct rexp_node * rx_shallow_copy_rexp ();
+extern int rx_rexp_equal ();
+extern unsigned long rx_rexp_hash ();
+
+#endif /* STDC */
+
+
+
+
+#endif  /* RXNODEH */
diff --git a/rx/rxposix.c b/rx/rxposix.c
new file mode 100644 (file)
index 0000000..17a7e10
--- /dev/null
@@ -0,0 +1,484 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include "rxall.h"
+#include "rxposix.h"
+#include "rxgnucomp.h"
+#include "rxbasic.h"
+#include "rxsimp.h"
+\f
+/* regcomp takes a regular expression as a string and compiles it.
+ *
+ * PATTERN is the address of the pattern string.
+ *
+ * CFLAGS is a series of bits which affect compilation.
+ *
+ *   If REG_EXTENDED is set, we use POSIX extended syntax; otherwise, we
+ *   use POSIX basic syntax.
+ *
+ *   If REG_NEWLINE is set, then . and [^...] don't match newline.
+ *   Also, regexec will try a match beginning after every newline.
+ *
+ *   If REG_ICASE is set, then we considers upper- and lowercase
+ *   versions of letters to be equivalent when matching.
+ *
+ *   If REG_NOSUB is set, then when PREG is passed to regexec, that
+ *   routine will report only success or failure, and nothing about the
+ *   registers.
+ *
+ * It returns 0 if it succeeds, nonzero if it doesn't.  (See regex.h for
+ * the return codes and their meanings.)  
+ */
+
+
+#ifdef __STDC__
+int
+regncomp (regex_t * preg, const char * pattern, int len, int cflags)
+#else
+int
+regncomp (preg, pattern, len, cflags)
+     regex_t * preg;
+     const char * pattern;
+     int len;
+     int cflags;
+#endif
+{
+  int ret;
+  unsigned int syntax;
+
+  rx_bzero ((char *)preg, sizeof (*preg));
+  syntax = ((cflags & REG_EXTENDED)
+           ? RE_SYNTAX_POSIX_EXTENDED
+           : RE_SYNTAX_POSIX_BASIC);
+
+  if (!(cflags & REG_ICASE))
+    preg->translate = 0;
+  else
+    {
+      unsigned i;
+
+      preg->translate = (unsigned char *) malloc (256);
+      if (!preg->translate)
+        return (int) REG_ESPACE;
+
+      /* Map uppercase characters to corresponding lowercase ones.  */
+      for (i = 0; i < CHAR_SET_SIZE; i++)
+        preg->translate[i] = isupper (i) ? tolower (i) : i;
+    }
+
+
+  /* If REG_NEWLINE is set, newlines are treated differently.  */
+  if (!(cflags & REG_NEWLINE))
+    preg->newline_anchor = 0;
+  else
+    {
+      /* REG_NEWLINE implies neither . nor [^...] match newline.  */
+      syntax &= ~RE_DOT_NEWLINE;
+      syntax |= RE_HAT_LISTS_NOT_NEWLINE;
+      /* It also changes the matching behavior.  */
+      preg->newline_anchor = 1;
+    }
+
+  preg->no_sub = !!(cflags & REG_NOSUB);
+
+  ret = rx_parse (&preg->pattern,
+                 pattern, len,
+                 syntax,
+                 256,
+                 preg->translate);
+
+  /* POSIX doesn't distinguish between an unmatched open-group and an
+   * unmatched close-group: both are REG_EPAREN.
+   */
+  if (ret == REG_ERPAREN)
+    ret = REG_EPAREN;
+
+  if (!ret)
+    {
+      preg->re_nsub = 1;
+      preg->subexps = 0;
+      rx_posix_analyze_rexp (&preg->subexps,
+                            &preg->re_nsub,
+                            preg->pattern,
+                            0);
+      preg->is_nullable = rx_fill_in_fastmap (256,
+                                             preg->fastmap,
+                                             preg->pattern);
+
+      preg->is_anchored = rx_is_anchored_p (preg->pattern);
+    }
+
+  return (int) ret;
+}
+
+
+#ifdef __STDC__
+int
+regcomp (regex_t * preg, const char * pattern, int cflags)
+#else
+int
+regcomp (preg, pattern, cflags)
+     regex_t * preg;
+     const char * pattern;
+     int cflags;
+#endif
+{
+  /* POSIX says a null character in the pattern terminates it, so we
+   * can use strlen here in compiling the pattern.  
+   */
+
+  return regncomp (preg, pattern, strlen (pattern), cflags);
+}
+
+
+\f
+
+/* Returns a message corresponding to an error code, ERRCODE, returned
+   from either regcomp or regexec.   */
+
+#ifdef __STDC__
+size_t
+regerror (int errcode, const regex_t *preg,
+         char *errbuf, size_t errbuf_size)
+#else
+size_t
+regerror (errcode, preg, errbuf, errbuf_size)
+    int errcode;
+    const regex_t *preg;
+    char *errbuf;
+    size_t errbuf_size;
+#endif
+{
+  const char *msg;
+  size_t msg_size;
+
+  msg = rx_error_msg[errcode] == 0 ? "Success" : rx_error_msg[errcode];
+  msg_size = strlen (msg) + 1; /* Includes the 0.  */
+  if (errbuf_size != 0)
+    {
+      if (msg_size > errbuf_size)
+        {
+          strncpy (errbuf, msg, errbuf_size - 1);
+          errbuf[errbuf_size - 1] = 0;
+        }
+      else
+        strcpy (errbuf, msg);
+    }
+  return msg_size;
+}
+\f
+
+
+#ifdef __STDC__
+int
+rx_regmatch (regmatch_t pmatch[], const regex_t *preg, struct rx_context_rules * rules, int start, int end, const char *string)
+#else
+int
+rx_regmatch (pmatch, preg, rules, start, end, string)
+     regmatch_t pmatch[];
+     const regex_t *preg;
+     struct rx_context_rules * rules;
+     int start;
+     int end;
+     const char *string;
+#endif
+{
+  struct rx_solutions * solutions;
+  enum rx_answers answer;
+  struct rx_context_rules local_rules;
+  int orig_end;
+  int end_lower_bound;
+  int end_upper_bound;
+  
+  local_rules = *rules;
+  orig_end = end;
+
+  if (!preg->pattern)
+    {
+      end_lower_bound = start;
+      end_upper_bound = start;
+    }
+  else if (preg->pattern->len >= 0)
+    {
+      end_lower_bound = start + preg->pattern->len;
+      end_upper_bound = start + preg->pattern->len;
+    }
+  else
+    {
+      end_lower_bound = start;
+      end_upper_bound = end;
+    }
+  end = end_upper_bound;
+  while (end >= end_lower_bound)
+    {
+      local_rules.not_eol = (rules->not_eol
+                            ? (   (end == orig_end)
+                               || !local_rules.newline_anchor
+                               || (string[end] != '\n'))
+                            : (   (end != orig_end)
+                               && (!local_rules.newline_anchor
+                                   || (string[end] != '\n'))));
+      solutions = rx_basic_make_solutions (pmatch, preg->pattern, preg->subexps,
+                                          start, end, &local_rules, string);
+      if (!solutions)
+       return REG_ESPACE;
+      
+      answer = rx_next_solution (solutions);
+
+      if (answer == rx_yes)
+       {
+         if (pmatch)
+           {
+             pmatch[0].rm_so = start;
+             pmatch[0].rm_eo = end;
+             pmatch[0].final_tag = solutions->final_tag;
+           }
+         rx_basic_free_solutions (solutions);
+         return 0;
+       }
+      else
+       rx_basic_free_solutions (solutions);
+
+      --end;
+    }
+
+  switch (answer)
+    {
+    default:
+    case rx_bogus:
+      return REG_ESPACE;
+
+    case rx_no:
+      return REG_NOMATCH;
+    }
+}
+
+
+#ifdef __STDC__
+int
+rx_regexec (regmatch_t pmatch[], const regex_t *preg, struct rx_context_rules * rules, int start, int end, const char *string)
+#else
+int
+rx_regexec (pmatch, preg, rules, start, end, string)
+     regmatch_t pmatch[];
+     const regex_t *preg;
+     struct rx_context_rules * rules;
+     int start;
+     int end;
+     const char *string;
+#endif
+{
+  int x;
+  int stat;
+  int anchored;
+  struct rexp_node * simplified;
+  struct rx_unfa * unfa;
+  struct rx_classical_system machine;
+
+  anchored = preg->is_anchored;
+
+  unfa = 0;
+  if ((end - start) > RX_MANY_CASES)
+    {
+      if (0 > rx_simple_rexp (&simplified, 256, preg->pattern, preg->subexps))
+       return REG_ESPACE;
+      unfa = rx_unfa (rx_basic_unfaniverse (), simplified, 256);
+      if (!unfa)
+       {
+         rx_free_rexp (simplified);
+         return REG_ESPACE;
+       }
+      rx_init_system (&machine, unfa->nfa);
+      rx_free_rexp (simplified);
+    }
+
+  for (x = start; x <= end; ++x)
+    {
+      if (preg->is_nullable
+         || ((x < end)
+             && (preg->fastmap[((unsigned char *)string)[x]])))
+       {
+         if ((end - start) > RX_MANY_CASES)
+           {
+             int amt;
+             if (rx_start_superstate (&machine) != rx_yes)
+               {
+                 rx_free_unfa (unfa);
+                 return REG_ESPACE;
+               }
+             amt = rx_advance_to_final (&machine, string + x, end - start - x);
+             if (!machine.final_tag && (amt < (end - start - x)))
+               goto nomatch;
+           }
+         stat = rx_regmatch (pmatch, preg, rules, x, end, string);
+         if (!stat || (stat != REG_NOMATCH))
+           {
+             rx_free_unfa (unfa);
+             return stat;
+           }
+       }
+    nomatch:
+      if (anchored)
+       if (!preg->newline_anchor)
+         {
+           rx_free_unfa (unfa);
+           return REG_NOMATCH;
+         }
+       else
+         while (x < end)
+           if (string[x] == '\n')
+             break;
+           else
+             ++x;
+    }
+  rx_free_unfa (unfa);
+  return REG_NOMATCH;
+}
+
+\f
+
+/* regexec searches for a given pattern, specified by PREG, in the
+ * string STRING.
+ *
+ * If NMATCH is zero or REG_NOSUB was set in the cflags argument to
+ * `regcomp', we ignore PMATCH.  Otherwise, we assume PMATCH has at
+ * least NMATCH elements, and we set them to the offsets of the
+ * corresponding matched substrings.
+ *
+ * EFLAGS specifies `execution flags' which affect matching: if
+ * REG_NOTBOL is set, then ^ does not match at the beginning of the
+ * string; if REG_NOTEOL is set, then $ does not match at the end.
+ *
+ * We return 0 if we find a match and REG_NOMATCH if not.  
+ */
+
+#ifdef __STDC__
+int
+regnexec (const regex_t *preg, const char *string, int len, size_t nmatch, regmatch_t **pmatch, int eflags)
+#else
+int
+regnexec (preg, string, len, nmatch, pmatch, eflags)
+     const regex_t *preg;
+     const char *string;
+     int len;
+     size_t nmatch;
+     regmatch_t **pmatch;
+     int eflags;
+#endif
+{
+  int want_reg_info;
+  struct rx_context_rules rules;
+  regmatch_t * regs;
+  size_t nregs;
+  int stat;
+
+  want_reg_info = (!preg->no_sub && (nmatch > 0));
+
+  rules.newline_anchor = preg->newline_anchor;
+  rules.not_bol = !!(eflags & REG_NOTBOL);
+  rules.not_eol = !!(eflags & REG_NOTEOL);
+  rules.case_indep = !!(eflags & REG_ICASE);
+
+  if (nmatch >= preg->re_nsub)
+    {
+      regs = *pmatch;
+      nregs = nmatch;
+    }
+  else
+    {
+      regs = (regmatch_t *)malloc (preg->re_nsub * sizeof (*regs));
+      if (!regs)
+       return REG_ESPACE;
+      nregs = preg->re_nsub;
+    }
+
+  {
+    int x;
+    for (x = 0; x < nregs; ++x)
+      regs[x].rm_so = regs[x].rm_eo = -1;
+  }
+
+
+  stat = rx_regexec (regs, preg, &rules, 0, len, string);
+
+  if (!stat && want_reg_info && pmatch && (regs != *pmatch))
+    {
+      size_t x;
+      for (x = 0; x < nmatch; ++x)
+       (*pmatch)[x] = regs[x];
+    }
+
+  if (!stat && (eflags & REG_ALLOC_REGS))
+    *pmatch = regs;
+  else if (regs && (!pmatch || (regs != *pmatch)))
+    free (regs);
+  
+  return stat;
+}
+
+#ifdef __STDC__
+int
+regexec (const regex_t *preg, const char *string, size_t nmatch, regmatch_t pmatch[], int eflags)
+#else
+int
+regexec (preg, string, nmatch, pmatch, eflags)
+     const regex_t *preg;
+     const char *string;
+     size_t nmatch;
+     regmatch_t pmatch[];
+     int eflags;
+#endif
+{
+  return regnexec (preg,
+                  string,
+                  strlen (string),
+                  nmatch,
+                  &pmatch,
+                  (eflags & ~REG_ALLOC_REGS));
+}
+
+
+/* Free dynamically allocated space used by PREG.  */
+
+#ifdef __STDC__
+void
+regfree (regex_t *preg)
+#else
+void
+regfree (preg)
+    regex_t *preg;
+#endif
+{
+  if (preg->pattern)
+    {
+      rx_free_rexp (preg->pattern);
+      preg->pattern = 0;
+    }
+  if (preg->subexps)
+    {
+      free (preg->subexps);
+      preg->subexps = 0;
+    }
+  if (preg->translate != 0)
+    {
+      free (preg->translate);
+      preg->translate = 0;
+    }
+}
diff --git a/rx/rxposix.h b/rx/rxposix.h
new file mode 100644 (file)
index 0000000..9edcfb5
--- /dev/null
@@ -0,0 +1,51 @@
+/* classes: h_files */
+
+#ifndef RXPOSIXH
+#define RXPOSIXH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+#include "rxspencer.h"
+#include "rxcontext.h"
+#include "inst-rxposix.h"
+
+#ifdef __STDC__
+extern int regncomp (regex_t * preg, const char * pattern, int len, int cflags);
+extern int regcomp (regex_t * preg, const char * pattern, int cflags);
+extern size_t regerror (int errcode, const regex_t *preg,
+                       char *errbuf, size_t errbuf_size);
+extern int rx_regmatch (regmatch_t pmatch[], const regex_t *preg, struct rx_context_rules * rules, int start, int end, const char *string);
+extern int rx_regexec (regmatch_t pmatch[], const regex_t *preg, struct rx_context_rules * rules, int start, int end, const char *string);
+extern int regnexec (const regex_t *preg, const char *string, int len, size_t nmatch, regmatch_t **pmatch, int eflags);
+extern int regexec (const regex_t *preg, const char *string, size_t nmatch, regmatch_t pmatch[], int eflags);
+extern void regfree (regex_t *preg);
+
+#else /* STDC */
+extern int regncomp ();
+extern int regcomp ();
+extern size_t regerror ();
+extern int rx_regmatch ();
+extern int rx_regexec ();
+extern int regnexec ();
+extern int regexec ();
+extern void regfree ();
+
+#endif /* STDC */
+
+#endif /* RXPOSIXH */
diff --git a/rx/rxproto.h b/rx/rxproto.h
new file mode 100644 (file)
index 0000000..25f5b57
--- /dev/null
@@ -0,0 +1,41 @@
+/* classes: h_files */
+
+#ifndef RXPROTOH
+#define RXPROTOH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+
+#ifdef __STDC__
+#define P(X) X
+#else
+#define P(X) ()
+#endif
+
+
+\f
+#ifdef __STDC__
+
+#else /* STDC */
+
+#endif /* STDC */
+
+
+#endif  /* RXPROTOH */
diff --git a/rx/rxsimp.c b/rx/rxsimp.c
new file mode 100644 (file)
index 0000000..5041c35
--- /dev/null
@@ -0,0 +1,147 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include "rxall.h"
+#include "rxsimp.h"
+
+\f
+
+
+/* Could reasonably hashcons instead of in rxunfa.c */
+
+#ifdef __STDC__
+int
+rx_simple_rexp (struct rexp_node ** answer,
+               int cset_size,
+               struct rexp_node *node,
+               struct rexp_node ** subexps)
+#else
+int
+rx_simple_rexp (answer, cset_size, node, subexps)
+     struct rexp_node ** answer;
+     int cset_size;
+     struct rexp_node *node;
+     struct rexp_node ** subexps;
+#endif
+{
+  int stat;
+
+  if (!node)
+    {
+      *answer = 0;
+      return 0;
+    }
+
+  if (!node->observed)
+    {
+      rx_save_rexp (node);
+      *answer = node;
+      return 0;
+    }
+
+  if (node->simplified)
+    {
+      rx_save_rexp (node->simplified);
+      *answer = node->simplified;
+      return 0;
+    }
+
+  switch (node->type)
+    {
+    default:
+    case r_cset:
+    case r_string:
+    case r_cut:
+      return -2;               /* an internal error, really */
+
+    case r_parens:
+      stat = rx_simple_rexp (answer, cset_size,
+                            node->params.pair.left,
+                            subexps);
+      break;
+
+    case r_context:
+      if (isdigit (node->params.intval))
+        stat = rx_simple_rexp (answer, cset_size,
+                               subexps [node->params.intval - '0'],
+                               subexps);
+      else
+       {
+         *answer = 0;
+         stat = 0;
+       }
+      break;
+
+    case r_concat:
+    case r_alternate:
+    case r_opt:
+    case r_star:
+    case r_plus:
+    case r_interval:
+      {
+       struct rexp_node *n;
+       n = rexp_node (node->type);
+       if (!n)
+         return -1;
+
+       if (node->params.cset)
+         {
+           n->params.cset = rx_copy_cset (cset_size,
+                                          node->params.cset);
+           if (!n->params.cset)
+             {
+               rx_free_rexp (n);
+               return -1;
+             }
+         }
+       n->params.intval = node->params.intval;
+       n->params.intval2 = node->params.intval2;
+       {
+         int s;
+    
+         s = rx_simple_rexp (&n->params.pair.left, cset_size,
+                             node->params.pair.left, subexps);
+         if (!s)
+           s = rx_simple_rexp (&n->params.pair.right, cset_size,
+                               node->params.pair.right, subexps);
+         if (!s)
+           {
+             *answer = n;
+             stat = 0;
+           }
+         else
+           {
+             rx_free_rexp  (n);
+             stat = s;
+           }
+       }
+      }      
+      break;
+    }
+
+  if (!stat)
+    {
+      node->simplified = *answer;
+      rx_save_rexp (node->simplified);
+    }
+  return stat;
+}  
+
+
diff --git a/rx/rxsimp.h b/rx/rxsimp.h
new file mode 100644 (file)
index 0000000..2b16737
--- /dev/null
@@ -0,0 +1,44 @@
+/* classes: h_files */
+
+#ifndef RXSIMPH
+#define RXSIMPH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include "rxcset.h"
+#include "rxnode.h"
+
+\f
+#ifdef __STDC__
+extern int rx_simple_rexp (struct rexp_node ** answer,
+                          int cset_size,
+                          struct rexp_node *node,
+                          struct rexp_node ** subexps);
+
+#else /* STDC */
+extern int rx_simple_rexp ();
+
+#endif /* STDC */
+
+
+
+
+
+#endif  /* RXSIMPH */
diff --git a/rx/rxspencer.c b/rx/rxspencer.c
new file mode 100644 (file)
index 0000000..0e2805c
--- /dev/null
@@ -0,0 +1,1191 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include <stdio.h>
+#include "rxall.h"
+#include "rxspencer.h"
+#include "rxsimp.h"
+
+\f
+
+static char * silly_hack_2 = 0;
+
+struct rx_solutions rx_no_solutions;
+
+#ifdef __STDC__
+struct rx_solutions *
+rx_make_solutions (struct rx_registers * regs, struct rx_unfaniverse * verse, struct rexp_node * expression, struct rexp_node ** subexps, int cset_size, int start, int end, rx_vmfn vmfn, rx_contextfn contextfn, void * closure)
+#else
+struct rx_solutions *
+rx_make_solutions (regs, verse, expression, subexps, cset_size, 
+                  start, end, vmfn, contextfn, closure)
+     struct rx_registers * regs;
+     struct rx_unfaniverse * verse;
+     struct rexp_node * expression;
+     struct rexp_node ** subexps;
+     int cset_size;
+     int start;
+     int end;
+     rx_vmfn vmfn;
+     rx_contextfn contextfn;
+     void * closure;
+#endif
+{
+  struct rx_solutions * solns;
+
+  if (   expression
+      && (expression->len >= 0)
+      && (expression->len != (end - start)))
+    return &rx_no_solutions;
+
+  if (silly_hack_2)
+    {
+      solns = (struct rx_solutions *)silly_hack_2;
+      silly_hack_2 = 0;
+    }
+  else
+    solns = (struct rx_solutions *)malloc (sizeof (*solns));
+  if (!solns)
+    return 0;
+  rx_bzero ((char *)solns, sizeof (*solns));
+
+  solns->step = 0;
+  solns->cset_size = cset_size;
+  solns->subexps = subexps;
+  solns->exp = expression;
+  rx_save_rexp (expression);
+  solns->verse = verse;
+  solns->regs = regs;
+  solns->start = start;
+  solns->end = end;
+  solns->vmfn = vmfn;
+  solns->contextfn = contextfn;
+  solns->closure = closure;
+
+  if (!solns->exp || !solns->exp->observed)
+    {
+      solns->dfa = rx_unfa (verse, expression, cset_size);
+      if (!solns->dfa)
+       goto err_return;
+      rx_init_system (&solns->match_engine, solns->dfa->nfa);
+
+      if (rx_yes != rx_start_superstate (&solns->match_engine))
+       goto err_return;
+    }
+  else
+    {
+      struct rexp_node * simplified;
+      int status;
+      status = rx_simple_rexp (&simplified, cset_size, solns->exp, subexps);
+      if (status)
+       goto err_return;
+      solns->dfa = rx_unfa (verse, simplified, cset_size);
+      if (!solns->dfa)
+       {
+         rx_free_rexp (simplified);
+         goto err_return;
+       }
+      rx_init_system (&solns->match_engine, solns->dfa->nfa);
+      if (rx_yes != rx_start_superstate (&solns->match_engine))
+       goto err_return;
+      rx_free_rexp (simplified);
+    }
+
+  if (expression && (   (expression->type == r_concat)
+                    || (expression->type == r_plus)
+                    || (expression->type == r_star)
+                    || (expression->type == r_interval)))
+    {
+      struct rexp_node * subexp;
+
+      subexp = solns->exp->params.pair.left;
+
+      if (!subexp || !subexp->observed)
+       {
+         solns->left_dfa = rx_unfa (solns->verse, subexp, solns->cset_size);
+       }
+      else
+       {
+         struct rexp_node * simplified;
+         int status;
+         status = rx_simple_rexp (&simplified, solns->cset_size, subexp, solns->subexps);
+         if (status)
+           goto err_return;
+         solns->left_dfa = rx_unfa (solns->verse, simplified, solns->cset_size);
+         rx_free_rexp (simplified);
+       }
+      
+      if (!solns->left_dfa)
+       goto err_return;
+      rx_bzero ((char *)&solns->left_match_engine, sizeof (solns->left_match_engine));
+      rx_init_system (&solns->left_match_engine, solns->left_dfa->nfa);
+    }
+  
+  return solns;
+
+ err_return:
+  rx_free_rexp (solns->exp);
+  free (solns);
+  return 0;
+}
+
+
+\f
+#ifdef __STDC__
+void
+rx_free_solutions (struct rx_solutions * solns)
+#else
+void
+rx_free_solutions (solns)
+     struct rx_solutions * solns;
+#endif
+{
+  if (!solns)
+    return;
+
+  if (solns == &rx_no_solutions)
+    return;
+
+  if (solns->left)
+    {
+      rx_free_solutions (solns->left);
+      solns->left = 0;
+    }
+
+  if (solns->right)
+    {
+      rx_free_solutions (solns->right);
+      solns->right = 0;
+    }
+
+  if (solns->dfa)
+    {
+      rx_free_unfa (solns->dfa);
+      solns->dfa = 0;
+    }
+  if (solns->left_dfa)
+    {
+      rx_terminate_system (&solns->left_match_engine);
+      rx_free_unfa (solns->left_dfa);
+      solns->left_dfa = 0;
+    }
+
+  rx_terminate_system (&solns->match_engine);
+
+  if (solns->exp)
+    {
+      rx_free_rexp (solns->exp);
+      solns->exp = 0;
+    }
+
+  if (!silly_hack_2)
+    silly_hack_2 = (char *)solns;
+  else
+    free (solns);
+}
+
+\f
+#ifdef __STDC__
+static enum rx_answers
+rx_solution_fit_p (struct rx_solutions * solns)
+#else
+static enum rx_answers
+rx_solution_fit_p (solns)
+     struct rx_solutions * solns;
+#endif
+{
+  unsigned const char * burst;
+  int burst_addr;
+  int burst_len;
+  int burst_end_addr;
+  int rel_pos_in_burst;
+  enum rx_answers vmstat;
+  int current_pos;
+         
+  current_pos = solns->start;
+ next_burst:
+  vmstat = solns->vmfn (solns->closure,
+                       &burst, &burst_len, &burst_addr,
+                       current_pos, solns->end,
+                       current_pos);
+
+  if (vmstat != rx_yes)
+    return vmstat;
+
+  rel_pos_in_burst = current_pos - burst_addr;
+  burst_end_addr = burst_addr + burst_len;
+
+  if (burst_end_addr >= solns->end)
+    {
+      enum rx_answers fit_status;
+      fit_status = rx_fit_p (&solns->match_engine,
+                            burst + rel_pos_in_burst,
+                            solns->end - current_pos);
+      return fit_status;
+    }
+  else
+    {
+      enum rx_answers fit_status;
+      fit_status = rx_advance (&solns->match_engine,
+                              burst + rel_pos_in_burst,
+                              burst_len - rel_pos_in_burst);
+      if (fit_status != rx_yes)
+       {
+         return fit_status;
+       }
+      else
+       {
+         current_pos += burst_len - rel_pos_in_burst;
+         goto next_burst;
+       }
+    }
+}
+\f
+
+#ifdef __STDC__
+static enum rx_answers
+rx_solution_fit_str_p (struct rx_solutions * solns)
+#else
+static enum rx_answers
+rx_solution_fit_str_p (solns)
+     struct rx_solutions * solns;
+#endif
+{
+  int current_pos;
+  unsigned const char * burst;
+  int burst_addr;
+  int burst_len;
+  int burst_end_addr;
+  int rel_pos_in_burst;
+  enum rx_answers vmstat;
+  int count;
+  unsigned char * key;
+
+
+  current_pos = solns->start;
+  count = solns->exp->params.cstr.len;
+  key = (unsigned char *)solns->exp->params.cstr.contents;
+
+ next_burst:
+  vmstat = solns->vmfn (solns->closure,
+                       &burst, &burst_len, &burst_addr,
+                       current_pos, solns->end,
+                       current_pos);
+
+  if (vmstat != rx_yes)
+    return vmstat;
+
+  rel_pos_in_burst = current_pos - burst_addr;
+  burst_end_addr = burst_addr + burst_len;
+
+  {
+    unsigned const char * pos;
+
+    pos = burst + rel_pos_in_burst;
+
+    if (burst_end_addr >= solns->end)
+      {
+       while (count)
+         {
+           if (*pos != *key)
+             return rx_no;
+           ++pos;
+           ++key;
+           --count;
+         }
+       return rx_yes;
+      }
+    else
+      {
+       int part_count;
+       int part_count_init;
+
+       part_count_init = burst_len - rel_pos_in_burst;
+       part_count = part_count_init;
+       while (part_count)
+         {
+           if (*pos != *key)
+             return rx_no;
+           ++pos;
+           ++key;
+           --part_count;
+         }
+       count -= part_count_init;
+       current_pos += burst_len - rel_pos_in_burst;
+       goto next_burst;
+      }
+  }
+}
+
+
+\f
+
+#if 0
+#ifdef __STDC__
+int
+rx_best_end_guess (struct rx_solutions * solns, struct rexp_node *  exp, int bound)
+#else
+int
+rx_best_end_guess (solns, exp, bound)
+     struct rx_solutions * solns;
+     struct rexp_node *  exp;
+     int bound;
+#endif
+{
+  int current_pos;
+  unsigned const char * burst;
+  int burst_addr;
+  int burst_len;
+  int burst_end_addr;
+  int rel_pos_in_burst;
+  int best_guess;
+  enum rx_answers vmstat;
+
+#if 0
+  unparse_print_rexp (256, solns->exp);
+  printf ("\n");
+  unparse_print_rexp (256, exp);
+  printf ("\nbound %d \n", bound);
+#endif
+
+  if (rx_yes != rx_start_superstate (&solns->left_match_engine))
+    {
+      return bound - 1;
+    }
+  best_guess = current_pos = solns->start;
+
+ next_burst:
+#if 0
+  printf ("  best_guess %d\n", best_guess);
+#endif
+  vmstat = solns->vmfn (solns->closure,
+                       &burst, &burst_len, &burst_addr,
+                       current_pos, bound,
+                       current_pos);
+#if 0
+  printf ("  str>%s\n", burst);
+#endif
+  if (vmstat != rx_yes)
+    {
+      return bound - 1;
+    }
+
+  rel_pos_in_burst = current_pos - burst_addr;
+  burst_end_addr = burst_addr + burst_len;
+
+  if (burst_end_addr > bound)
+    {
+      burst_end_addr = bound;
+      burst_len = bound - burst_addr;
+    }
+
+  {
+    int amt_advanced;
+
+#if 0
+    printf ("  rel_pos_in_burst %d burst_len %d\n", rel_pos_in_burst, burst_len);
+#endif
+    while (rel_pos_in_burst < burst_len)
+      {
+       amt_advanced= rx_advance_to_final (&solns->left_match_engine,
+                                          burst + rel_pos_in_burst,
+                                          burst_len - rel_pos_in_burst);
+#if 0
+       printf ("  amt_advanced %d", amt_advanced);
+#endif
+       if (amt_advanced < 0)
+         {
+           return bound - 1;
+         }
+       current_pos += amt_advanced;
+       rel_pos_in_burst += amt_advanced;
+       if (solns->left_match_engine.final_tag)
+         best_guess = current_pos;
+#if 0
+       printf ("  best_guess %d\n", best_guess);
+       printf ("  current_pos %d\n", current_pos);
+#endif
+       if (amt_advanced == 0)
+         {
+           return best_guess;
+         }
+      }
+    if (current_pos == bound)
+      {
+       return best_guess;
+      }
+    goto next_burst;
+  }
+}
+#endif
+
+
+\f
+#ifdef __STDC__
+enum rx_answers
+rx_next_solution (struct rx_solutions * solns)
+#else
+enum rx_answers
+rx_next_solution (solns)
+     struct rx_solutions * solns;
+#endif
+{
+  if (!solns)
+    return rx_bogus;
+
+  if (solns == &rx_no_solutions)
+    {
+      return rx_no;
+    }
+
+  if (!solns->exp)
+    {
+      if (solns->step != 0)
+       {
+         return rx_no;
+       }
+      else
+       {
+         solns->step = 1;
+         solns->final_tag = 1;
+         return (solns->start == solns->end
+                 ? rx_yes
+                 : rx_no);
+       }
+    }
+  else if (   (solns->exp->len >= 0)
+          && (solns->exp->len != (solns->end - solns->start)))
+    {
+      return rx_no;
+    }
+  else if (!solns->exp->observed)
+    {
+      if (solns->step != 0)
+       {
+         return rx_no;
+       }
+      else if (solns->exp->type == r_string)
+       {
+         enum rx_answers ans;
+         ans = rx_solution_fit_str_p (solns);
+         solns->final_tag = 1;
+         solns->step = -1;
+         return ans;
+       }
+      else
+       {
+         enum rx_answers ans;
+         ans = rx_solution_fit_p (solns);
+         solns->final_tag = solns->match_engine.final_tag;
+         solns->step = -1;
+         return ans;
+       }
+    }
+  else if (solns->exp->observed)
+    {
+      enum rx_answers fit_p;
+      switch (solns->step)
+       {
+       case -2:
+         if (solns->exp->params.intval)
+           {
+             solns->regs[solns->exp->params.intval].rm_so = solns->saved_rm_so;
+             solns->regs[solns->exp->params.intval].rm_eo = solns->saved_rm_eo;
+           }
+         return rx_no;
+
+       case -1:
+         return rx_no;
+
+       case 0:
+         fit_p = rx_solution_fit_p (solns);
+         /* Set final_tag here because this rough fit test
+          * may be all the matching that gets done.
+          * For example, consider a paren node containing
+          * a true regular expression ending with a cut
+          * operator.
+          */
+         solns->final_tag = solns->match_engine.final_tag;
+         switch (fit_p)
+           {
+           case rx_no:
+             solns->step = -1;
+             return rx_no;
+           case rx_yes:
+             solns->step = 1;
+             goto resolve_fit;
+           case rx_bogus:
+           default:
+             solns->step = -1;
+             return fit_p;
+           }
+
+       default:
+       resolve_fit:
+         switch (solns->exp->type)
+           {
+           case r_cset:
+           case r_string:
+           case r_cut:
+             solns->step = -1;
+             return rx_bogus;
+             
+           case r_parens:
+             {
+               enum rx_answers paren_stat;
+               switch (solns->step)
+                 {
+                 case 1:
+                   if (solns->exp->params.intval)
+                     {
+                       solns->saved_rm_so = solns->regs[solns->exp->params.intval].rm_so;
+                       solns->saved_rm_eo = solns->regs[solns->exp->params.intval].rm_eo;
+                     }
+
+                   if (   !solns->exp->params.pair.left
+                       || !solns->exp->params.pair.left->observed)
+                     {
+                       if (solns->exp->params.intval)
+                         {
+                           solns->regs[solns->exp->params.intval].rm_so = solns->start;
+                           solns->regs[solns->exp->params.intval].rm_eo = solns->end;
+                         }
+                       solns->step = -2;
+                       /* Keep the final_tag from the fit_p test. */
+                       return rx_yes;
+                     }
+                   else
+                     {
+                       solns->left = rx_make_solutions (solns->regs,
+                                                        solns->verse,
+                                                        solns->exp->params.pair.left,
+                                                        solns->subexps,
+                                                        solns->cset_size,
+                                                        solns->start,
+                                                        solns->end,
+                                                        solns->vmfn,
+                                                        solns->contextfn,
+                                                        solns->closure);
+                       if (!solns->left)
+                         {
+                           solns->step = -1;
+                           return rx_bogus;
+                         }
+                     }
+                   solns->step = 2;
+                   /* fall through */
+
+                 case 2:
+                   if (solns->exp->params.intval)
+                     {
+                       solns->regs[solns->exp->params.intval].rm_so = solns->saved_rm_so;
+                       solns->regs[solns->exp->params.intval].rm_eo = solns->saved_rm_eo;
+                     }
+
+                   paren_stat = rx_next_solution (solns->left);
+                   if (paren_stat == rx_yes)
+                     {
+                       if (solns->exp->params.intval)
+                         {
+                           solns->regs[solns->exp->params.intval].rm_so = solns->start;
+                           solns->regs[solns->exp->params.intval].rm_eo = solns->end;
+                         }
+                       solns->final_tag = solns->left->final_tag;
+                       return rx_yes;
+                     }
+                   else 
+                     {
+                       solns->step = -1;
+                       rx_free_solutions (solns->left);
+                       solns->left = 0;
+                       if (solns->exp->params.intval)
+                         {
+                           solns->regs[solns->exp->params.intval].rm_so = solns->saved_rm_so;
+                           solns->regs[solns->exp->params.intval].rm_eo = solns->saved_rm_eo;
+                         }
+                       return paren_stat;
+                     }
+                 }
+             }
+
+
+           case r_opt:
+             {
+               enum rx_answers opt_stat;
+               switch (solns->step)
+                 {
+                 case 1:
+                   solns->left = rx_make_solutions (solns->regs,
+                                                    solns->verse,
+                                                    solns->exp->params.pair.left,
+                                                    solns->subexps,
+                                                    solns->cset_size,
+                                                    solns->start,
+                                                    solns->end,
+                                                    solns->vmfn,
+                                                    solns->contextfn,  
+                                                    solns->closure);
+                   if (!solns->left)
+                     {
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+                   solns->step = 2;
+                   /* fall through */
+                   
+                 case 2:
+                   opt_stat = rx_next_solution (solns->left);
+                   if (opt_stat == rx_yes)
+                     {
+                       solns->final_tag = solns->left->final_tag;
+                       return rx_yes;
+                     }
+                   else 
+                     {
+                       solns->step = -1;
+                       rx_free_solutions (solns->left);
+                       solns->left = 0;
+                       return ((solns->start == solns->end)
+                               ? rx_yes
+                               : rx_no);
+                     }
+
+                 }
+            }
+
+           case r_alternate:
+             {
+               enum rx_answers alt_stat;
+               switch (solns->step)
+                 {
+                 case 1:
+                   solns->left = rx_make_solutions (solns->regs,
+                                                    solns->verse,
+                                                    solns->exp->params.pair.left,
+                                                    solns->subexps,
+                                                    solns->cset_size,
+                                                    solns->start,
+                                                    solns->end,
+                                                    solns->vmfn,
+                                                    solns->contextfn,
+                                                    solns->closure);
+                   if (!solns->left)
+                     {
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+                   solns->step = 2;
+                   /* fall through */
+                   
+                 case 2:
+                   alt_stat = rx_next_solution (solns->left);
+
+                   if (alt_stat == rx_yes)
+                     {
+                       solns->final_tag = solns->left->final_tag;
+                       return alt_stat;
+                     }
+                   else 
+                     {
+                       solns->step = 3;
+                       rx_free_solutions (solns->left);
+                       solns->left = 0;
+                       /* fall through */
+                     }
+
+                 case 3:
+                   solns->right = rx_make_solutions (solns->regs,
+                                                     solns->verse,
+                                                     solns->exp->params.pair.right,
+                                                     solns->subexps,
+                                                     solns->cset_size,
+                                                     solns->start,
+                                                     solns->end,
+                                                     solns->vmfn,
+                                                     solns->contextfn,
+                                                     solns->closure);
+                   if (!solns->right)
+                     {
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+                   solns->step = 4;
+                   /* fall through */
+                   
+                 case 4:
+                   alt_stat = rx_next_solution (solns->right);
+
+                   if (alt_stat == rx_yes)
+                     {
+                       solns->final_tag = solns->right->final_tag;
+                       return alt_stat;
+                     }
+                   else 
+                     {
+                       solns->step = -1;
+                       rx_free_solutions (solns->right);
+                       solns->right = 0;
+                       return alt_stat;
+                     }
+                 }
+            }
+
+           case r_concat:
+             {
+               switch (solns->step)
+                 {
+                   enum rx_answers concat_stat;
+                 case 1:
+                   solns->split_guess = solns->end;
+#if 0
+                   solns->split_guess = ((solns->end - solns->start) > RX_MANY_CASES
+                                         ? rx_best_end_guess (solns,
+                                                              solns->exp->params.pair.left, solns->end)
+                                         : solns->end);
+#endif
+
+                 concat_split_guess_loop:
+                   solns->left = rx_make_solutions (solns->regs,
+                                                    solns->verse,
+                                                    solns->exp->params.pair.left,
+                                                    solns->subexps,
+                                                    solns->cset_size,
+                                                    solns->start,
+                                                    solns->split_guess,
+                                                    solns->vmfn,
+                                                    solns->contextfn,
+                                                    solns->closure);
+                   if (!solns->left)
+                     {
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+                   solns->step = 2;
+
+                 case 2:
+                 concat_try_next_left_match:
+
+                   concat_stat = rx_next_solution (solns->left);
+                   if (concat_stat != rx_yes)
+                     {
+                       rx_free_solutions (solns->left);
+                       rx_free_solutions (solns->right);
+                       solns->left = solns->right = 0;
+                       solns->split_guess = solns->split_guess - 1;
+#if 0
+                       solns->split_guess = ((solns->split_guess - solns->start) > RX_MANY_CASES
+                                             ? rx_best_end_guess (solns,
+                                                                  solns->exp->params.pair.left,
+                                                                  solns->split_guess - 1)
+                                             : solns->split_guess - 1);
+#endif
+                       if (solns->split_guess >= solns->start)
+                         goto concat_split_guess_loop;
+                       else
+                         {
+                           solns->step = -1;
+                           return concat_stat;
+                         }
+                     }
+                   else
+                     {
+                       solns->step = 3;
+                       /* fall through */
+                     }
+
+                 case 3:
+                   solns->right = rx_make_solutions (solns->regs,
+                                                     solns->verse,
+                                                     solns->exp->params.pair.right,
+                                                     solns->subexps,
+                                                     solns->cset_size,
+                                                     solns->split_guess,
+                                                     solns->end,
+                                                     solns->vmfn,
+                                                     solns->contextfn,
+                                                     solns->closure);
+                   if (!solns->right)
+                     {
+                       rx_free_solutions (solns->left);
+                       solns->left = 0;
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+
+                   solns->step = 4;
+                   /* fall through */
+
+                 case 4:
+                 /* concat_try_next_right_match: */
+
+                   concat_stat = rx_next_solution (solns->right);
+                   if (concat_stat == rx_yes)
+                     {
+                       solns->final_tag = solns->right->final_tag;
+                       return concat_stat;
+                     }
+                   else if (concat_stat == rx_no)
+                     {
+                       rx_free_solutions (solns->right);
+                       solns->right = 0;
+                       solns->step = 2;
+                       goto concat_try_next_left_match;
+                     }
+                   else /*  concat_stat == rx_bogus */
+                     {
+                       rx_free_solutions (solns->left);
+                       solns->left = 0;
+                       rx_free_solutions (solns->right);
+                       solns->right = 0;
+                       solns->step = -1;
+                       return concat_stat;
+                     }
+                 }
+             }
+
+
+           case r_plus:
+           case r_star:
+             {
+               switch (solns->step)
+                 {
+                   enum rx_answers star_stat;
+                 case 1:
+                   solns->split_guess = solns->end;
+#if 0
+                   solns->split_guess = ((solns->end - solns->start) > RX_MANY_CASES
+                                         ? rx_best_end_guess (solns,
+                                                              solns->exp->params.pair.left, solns->end)
+                                         : solns->end);
+#endif
+
+                 star_split_guess_loop:
+                   solns->left = rx_make_solutions (solns->regs,
+                                                    solns->verse,
+                                                    solns->exp->params.pair.left,
+                                                    solns->subexps,
+                                                    solns->cset_size,
+                                                    solns->start,
+                                                    solns->split_guess,
+                                                    solns->vmfn,
+                                                    solns->contextfn,
+                                                    solns->closure);
+                   if (!solns->left)
+                     {
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+                   solns->step = 2;
+
+                 case 2:
+                 star_try_next_left_match:
+
+                   star_stat = rx_next_solution (solns->left);
+                   if (star_stat != rx_yes)
+                     {
+                       rx_free_solutions (solns->left);
+                       rx_free_solutions (solns->right);
+                       solns->left = solns->right = 0;
+                       solns->split_guess = solns->split_guess - 1;
+#if 0
+                       solns->split_guess = ((solns->split_guess - solns->start) > RX_MANY_CASES
+                                             ? rx_best_end_guess (solns,
+                                                                  solns->exp->params.pair.left,
+                                                                  solns->split_guess - 1)
+                                             : solns->split_guess - 1);
+#endif
+                       if (solns->split_guess >= solns->start)
+                         goto star_split_guess_loop;
+                       else
+                         {
+                           solns->step = -1;
+
+                           if (   (solns->exp->type == r_star)
+                               && (solns->start == solns->end)
+                               && (star_stat == rx_no))
+                             {
+                               solns->final_tag = 1;
+                               return rx_yes;
+                             }
+                           else
+                             return star_stat;
+                         }
+                     }
+                   else
+                     {
+                       solns->step = 3;
+                       /* fall through */
+                     }
+
+
+                   if (solns->split_guess == solns->end)
+                     {
+                       solns->final_tag = solns->left->final_tag;
+                       return rx_yes;
+                     }
+                   
+                 case 3:
+                   solns->right = rx_make_solutions (solns->regs,
+                                                     solns->verse,
+                                                     solns->exp,
+                                                     solns->subexps,
+                                                     solns->cset_size,
+                                                     solns->split_guess,
+                                                     solns->end,
+                                                     solns->vmfn,
+                                                     solns->contextfn,
+                                                     solns->closure);
+                   if (!solns->right)
+                     {
+                       rx_free_solutions (solns->left);
+                       solns->left = 0;
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+
+                   solns->step = 4;
+                   /* fall through */
+
+                 case 4:
+                 /* star_try_next_right_match: */
+                   
+                   star_stat = rx_next_solution (solns->right);
+                   if (star_stat == rx_yes)
+                     {
+                       solns->final_tag = solns->right->final_tag;
+                       return star_stat;
+                     }
+                   else if (star_stat == rx_no)
+                     {
+                       rx_free_solutions (solns->right);
+                       solns->right = 0;
+                       solns->step = 2;
+                       goto star_try_next_left_match;
+                     }
+                   else /*  star_stat == rx_bogus */
+                     {
+                       rx_free_solutions (solns->left);
+                       solns->left = 0;
+                       rx_free_solutions (solns->right);
+                       solns->right = 0;
+                       solns->step = -1;
+                       return star_stat;
+                     }
+                 }
+             }
+
+           case r_interval:
+             {
+               switch (solns->step)
+                 {
+                   enum rx_answers interval_stat;
+
+                 case 1:
+                   /* If the interval permits nothing, 
+                    * return immediately.
+                    */
+                   if (solns->exp->params.intval2 < solns->interval_x)
+                     {
+                       solns->step = -1;
+                       return rx_no;
+                     }
+
+                   /* If the interval permits only 0 iterations,
+                    * return immediately.  Success depends on the
+                    * emptiness of the match.
+                    */
+                   if (   (solns->exp->params.intval2 == solns->interval_x)
+                       && (solns->exp->params.intval <= solns->interval_x))
+                     {
+                       solns->step = -1;
+                       solns->final_tag = 1;
+                       return ((solns->start == solns->end)
+                               ? rx_yes
+                               : rx_no);
+                     }
+
+                   /* The interval permits at most 0 iterations, 
+                    * but also requires more.  A bug.
+                    */
+                   if (solns->exp->params.intval2 == solns->interval_x)
+                     {
+                       /* indicates a regexp compilation error, actually */
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+
+                   solns->split_guess = solns->end;
+#if 0
+                   solns->split_guess = ((solns->end - solns->start) > RX_MANY_CASES
+                                         ? rx_best_end_guess (solns,
+                                                              solns->exp->params.pair.left, solns->end)
+                                         : solns->end);
+#endif
+
+                   /* The interval permits more than 0 iterations.
+                    * If it permits 0 and the match is to be empty, 
+                    * the trivial match is the most preferred answer. 
+                    */
+                   if (solns->exp->params.intval <= solns->interval_x)
+                     {
+                       solns->step = 2;
+                       if (solns->start == solns->end)
+                         {
+                           solns->final_tag = 1;
+                           return rx_yes;
+                         }
+                       /* If this isn't a trivial match, or if the trivial match
+                        * is rejected, look harder. 
+                        */
+                     }
+                   
+                 case 2:
+                 interval_split_guess_loop:
+                   /* The match requires at least one iteration, either because
+                    * there are characters to match, or because the interval starts
+                    * above 0.
+                    *
+                    * Look for the first iteration:
+                    */
+                   solns->left = rx_make_solutions (solns->regs,
+                                                    solns->verse,
+                                                    solns->exp->params.pair.left,
+                                                    solns->subexps,
+                                                    solns->cset_size,
+                                                    solns->start,
+                                                    solns->split_guess,
+                                                    solns->vmfn,
+                                                    solns->contextfn,
+                                                    solns->closure);
+                   if (!solns->left)
+                     {
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+                   solns->step = 3;
+
+                 case 3:
+                 interval_try_next_left_match:
+
+                   interval_stat = rx_next_solution (solns->left);
+                   if (interval_stat != rx_yes)
+                     {
+                       rx_free_solutions (solns->left);
+                       rx_free_solutions (solns->right);
+                       solns->left = solns->right = 0;
+                       solns->split_guess = solns->split_guess - 1;
+#if 0
+                       solns->split_guess = ((solns->split_guess - solns->start) > RX_MANY_CASES
+                                             ? rx_best_end_guess (solns,
+                                                                  solns->exp->params.pair.left,
+                                                                  solns->split_guess - 1)
+                                             : solns->split_guess - 1);
+#endif
+                       if (solns->split_guess >= solns->start)
+                         goto interval_split_guess_loop;
+                       else
+                         {
+                           solns->step = -1;
+                           return interval_stat;
+                         }
+                     }
+                   else
+                     {
+                       solns->step = 4;
+                       /* fall through */
+                     }
+
+                 case 4:
+                   {
+                     /* After matching one required iteration, construct a smaller
+                      * interval and try to match that against the rest.
+                      *
+                      * To avoid thwarting unfa caching, instead of building a new
+                      * rexp node with different interval extents, we keep interval_x
+                      * in each solns structure to keep track of the number of 
+                      * iterations matched so far.
+                      */
+                     solns->right = rx_make_solutions (solns->regs,
+                                                       solns->verse,
+                                                       solns->exp,
+                                                       solns->subexps,
+                                                       solns->cset_size,
+                                                       solns->split_guess,
+                                                       solns->end,
+                                                       solns->vmfn,
+                                                       solns->contextfn,
+                                                       solns->closure);
+                     solns->right->interval_x = solns->interval_x + 1;
+                   }
+                   if (!solns->right)
+                     {
+                       rx_free_solutions (solns->left);
+                       solns->left = 0;
+                       solns->step = -1;
+                       return rx_bogus;
+                     }
+
+                   solns->step = 5;
+                   /* fall through */
+
+                 case 5:
+                 /* interval_try_next_right_match: */
+                   
+                   interval_stat = rx_next_solution (solns->right);
+                   if (interval_stat == rx_yes)
+                     {
+                       solns->final_tag = solns->right->final_tag;
+                       return interval_stat;
+                     }
+                   else if (interval_stat == rx_no)
+                     {
+                       rx_free_solutions (solns->right);
+                       solns->right = 0;
+                       solns->step = 2;
+                       goto interval_try_next_left_match;
+                     }
+                   else /*  interval_stat == rx_bogus */
+                     {
+                       rx_free_solutions (solns->left);
+                       solns->left = 0;
+                       rx_free_solutions (solns->right);
+                       solns->right = 0;
+                       solns->step = -1;
+                       return interval_stat;
+                     }
+                 }
+             }
+
+           case r_context:
+             {
+               solns->step = -1;
+               solns->final_tag = 1;
+               return solns->contextfn (solns->closure,
+                                        solns->exp,
+                                        solns->start, solns->end,
+                                        solns->regs);
+             }
+
+
+           }
+       }
+      return rx_bogus;
+    }
+}
diff --git a/rx/rxspencer.h b/rx/rxspencer.h
new file mode 100644 (file)
index 0000000..0c4a46b
--- /dev/null
@@ -0,0 +1,99 @@
+/* classes: h_files */
+
+#ifndef RXSPENCERH
+#define RXSPENCERH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include "rxproto.h"
+#include "rxnode.h"
+#include "rxunfa.h"
+#include "rxanal.h"
+#include "inst-rxposix.h"
+
+\f
+
+#define RX_MANY_CASES 30
+
+
+typedef enum rx_answers (*rx_vmfn)
+     P((void * closure,
+       unsigned const char ** burst, int * len, int * offset,
+       int start, int end, int need));
+
+typedef enum rx_answers (*rx_contextfn)
+     P((void * closure,
+       struct rexp_node * node,
+       int start, int end,
+       struct rx_registers * regs));
+
+
+struct rx_solutions
+{
+  int step;
+
+  int cset_size;
+  struct rexp_node * exp;
+  struct rexp_node ** subexps;
+  struct rx_registers * regs;
+
+  int start;
+  int end;
+
+  rx_vmfn vmfn;
+  rx_contextfn contextfn;
+  void * closure;
+
+  struct rx_unfaniverse * verse;
+  struct rx_unfa * dfa;
+  struct rx_classical_system match_engine;
+  struct rx_unfa * left_dfa;
+  struct rx_classical_system left_match_engine;
+
+  int split_guess;
+  struct rx_solutions * left;
+  struct rx_solutions * right;
+
+  int interval_x;
+
+  int saved_rm_so;
+  int saved_rm_eo;
+
+  int final_tag;
+};
+
+extern struct rx_solutions rx_no_solutions;
+
+\f
+#ifdef __STDC__
+extern struct rx_solutions * rx_make_solutions (struct rx_registers * regs, struct rx_unfaniverse * verse, struct rexp_node * expression, struct rexp_node ** subexps, int cset_size, int start, int end, rx_vmfn vmfn, rx_contextfn contextfn, void * closure);
+extern void rx_free_solutions (struct rx_solutions * solns);
+extern int rx_best_end_guess (struct rx_solutions * solns, struct rexp_node *  exp, int bound);
+extern enum rx_answers rx_next_solution (struct rx_solutions * solns);
+
+#else /* STDC */
+extern struct rx_solutions * rx_make_solutions ();
+extern void rx_free_solutions ();
+extern int rx_best_end_guess ();
+extern enum rx_answers rx_next_solution ();
+
+#endif /* STDC */
+
+#endif  /* RXSPENCERH */
diff --git a/rx/rxstr.c b/rx/rxstr.c
new file mode 100644 (file)
index 0000000..c3ccb03
--- /dev/null
@@ -0,0 +1,130 @@
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include "rxall.h"
+#include "rxstr.h"
+
+\f
+
+#ifdef __STDC__
+enum rx_answers
+rx_str_vmfn (void * closure, unsigned const char ** burstp, int * lenp, int * offsetp, int start, int end, int need)
+#else
+enum rx_answers
+rx_str_vmfn (closure, burstp, lenp, offsetp, start, end, need)
+     void * closure;
+     unsigned const char ** burstp;
+     int * lenp;
+     int * offsetp;
+     int start;
+     int end;
+     int need;
+#endif
+{
+  struct rx_str_closure * strc;
+  strc = (struct rx_str_closure *)closure;
+
+  if (   (need < 0)
+      || (need > strc->len))
+    return rx_no;
+
+  *burstp = strc->str;
+  *lenp = strc->len;
+  *offsetp = 0;
+  return rx_yes;
+}
+
+#ifdef __STDC__
+enum rx_answers
+rx_str_contextfn (void * closure, struct rexp_node * node, int start, int end, struct rx_registers * regs)
+#else
+enum rx_answers
+rx_str_contextfn (closure, node, start, end, regs)
+     void * closure;
+     struct rexp_node * node;
+     int start;
+     int end;
+     struct rx_registers * regs;
+#endif
+{
+  struct rx_str_closure * strc;
+
+  strc = (struct rx_str_closure *)closure;
+  switch (node->params.intval)
+    {
+    case '1': case '2': case '3': case '4': case '5':
+    case '6': case '7': case '8': case '9':
+      {
+       int cmp;
+       int regn;
+       regn = node->params.intval - '0';
+       if (   (regs[regn].rm_so == -1)
+           || ((end - start) != (regs[regn].rm_eo - regs[regn].rm_so)))
+         return rx_no;
+       else
+         {
+           if (strc->rules.case_indep)
+             cmp = strncasecmp (strc->str + start,
+                                strc->str + regs[regn].rm_so,
+                                end - start);
+           else
+             cmp = strncmp (strc->str + start,
+                            strc->str + regs[regn].rm_so,
+                            end - start);
+
+           return (!cmp
+                   ? rx_yes
+                   : rx_no);
+         }
+      }
+
+    case '^':
+      {
+       return ((   (start == end)
+                && (   ((start == 0) && !strc->rules.not_bol)
+                    || (   (start > 0)
+                        && strc->rules.newline_anchor
+                        && (strc->str[start - 1] == '\n'))))
+               ? rx_yes
+               : rx_no);
+      }
+
+    case '$':
+      {
+       return ((   (start == end)
+                && (   ((start == strc->len) && !strc->rules.not_eol)
+                    || (   (start < strc->len)
+                        && strc->rules.newline_anchor
+                        && (strc->str[start] == '\n'))))
+               ? rx_yes
+               : rx_no);
+      }
+
+    case '<':
+    case '>':
+
+    case 'B':
+    case 'b':
+
+
+    default:
+      return rx_bogus;
+    }
+}
diff --git a/rx/rxstr.h b/rx/rxstr.h
new file mode 100644 (file)
index 0000000..991f53b
--- /dev/null
@@ -0,0 +1,57 @@
+/* classes: h_files */
+
+#ifndef RXSTRH
+#define RXSTRH
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+
+#include "rxspencer.h"
+#include "rxcontext.h"
+
+\f
+
+  
+
+struct rx_str_closure
+{
+  struct rx_context_rules rules;
+  const unsigned char * str;
+  int len;
+};
+
+
+\f
+#ifdef __STDC__
+extern enum rx_answers rx_str_vmfn (void * closure, unsigned const char ** burstp, int * lenp, int * offsetp, int start, int end, int need);
+extern enum rx_answers rx_str_contextfn (void * closure, struct rexp_node * node, int start, int end, struct rx_registers * regs);
+
+#else /* STDC */
+extern enum rx_answers rx_str_vmfn ();
+extern enum rx_answers rx_str_contextfn ();
+
+#endif /* STDC */
+
+
+
+
+
+
+#endif  /* RXSTRH */
diff --git a/rx/rxsuper.c b/rx/rxsuper.c
new file mode 100644 (file)
index 0000000..46e745a
--- /dev/null
@@ -0,0 +1,1416 @@
+
+
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+\f
+
+#include "rxall.h"
+#include "rxsuper.h"
+
+\f
+/* The functions in the next several pages define the lazy-NFA-conversion used
+ * by matchers.  The input to this construction is an NFA such as 
+ * is built by compactify_nfa (rx.c).  The output is the superNFA.
+ */
+
+/* Match engines can use arbitrary values for opcodes.  So, the parse tree 
+ * is built using instructions names (enum rx_opcode), but the superstate
+ * nfa is populated with mystery opcodes (void *).
+ *
+ * For convenience, here is an id table.  The opcodes are == to their inxs
+ *
+ * The lables in re_search_2 would make good values for instructions.
+ */
+
+void * rx_id_instruction_table[rx_num_instructions] =
+{
+  (void *) rx_backtrack_point,
+  (void *) rx_do_side_effects,
+  (void *) rx_cache_miss,
+  (void *) rx_next_char,
+  (void *) rx_backtrack,
+  (void *) rx_error_inx
+};
+
+\f
+
+
+/* The partially instantiated superstate graph has a transition 
+ * table at every node.  There is one entry for every character.
+ * This fills in the transition for a set.
+ */
+#ifdef __STDC__
+static void 
+install_transition (struct rx_superstate *super,
+                   struct rx_inx *answer, rx_Bitset trcset) 
+#else
+static void 
+install_transition (super, answer, trcset)
+     struct rx_superstate *super;
+     struct rx_inx *answer;
+     rx_Bitset trcset;
+#endif
+{
+  struct rx_inx * transitions = super->transitions;
+  int chr;
+  for (chr = 0; chr < 256; )
+    if (!*trcset)
+      {
+       ++trcset;
+       chr += 32;
+      }
+    else
+      {
+       RX_subset sub = *trcset;
+       RX_subset mask = 1;
+       int bound = chr + 32;
+       while (chr < bound)
+         {
+           if (sub & mask)
+             transitions [chr] = *answer;
+           ++chr;
+           mask <<= 1;
+         }
+       ++trcset;
+      }
+}
+
+
+#ifdef __STDC__
+static int
+qlen (struct rx_superstate * q)
+#else
+static int
+qlen (q)
+     struct rx_superstate * q;
+#endif
+{
+  int count = 1;
+  struct rx_superstate * it;
+  if (!q)
+    return 0;
+  for (it = q->next_recyclable; it != q; it = it->next_recyclable)
+    ++count;
+  return count;
+}
+
+#ifdef __STDC__
+static void
+check_cache (struct rx_cache * cache)
+#else
+static void
+check_cache (cache)
+     struct rx_cache * cache;
+#endif
+{
+  struct rx_cache * you_fucked_up = 0;
+  int total = cache->superstates;
+  int semi = cache->semifree_superstates;
+  if (semi != qlen (cache->semifree_superstate))
+    check_cache (you_fucked_up);
+  if ((total - semi) != qlen (cache->lru_superstate))
+    check_cache (you_fucked_up);
+}
+#ifdef __STDC__
+char *
+rx_cache_malloc (struct rx_cache * cache, int size)
+#else
+char *
+rx_cache_malloc (cache, size)
+     struct rx_cache * cache;
+     int size;
+#endif
+{
+  char * answer;
+  answer = (char *)malloc (size);
+  if (answer)
+    cache->bytes_used += size;
+  return answer;
+}
+
+
+#ifdef __STDC__
+void
+rx_cache_free (struct rx_cache * cache, int size, char * mem)
+#else
+void
+rx_cache_free (cache, size, mem)
+     struct rx_cache * cache;
+     int size;
+     char * mem;
+#endif
+{
+  free (mem);
+  cache->bytes_used -= size;
+}
+
+
+
+/* When a superstate is old and neglected, it can enter a 
+ * semi-free state.  A semi-free state is slated to die.
+ * Incoming transitions to a semi-free state are re-written
+ * to cause an (interpreted) fault when they are taken.
+ * The fault handler revives the semi-free state, patches
+ * incoming transitions back to normal, and continues.
+ *
+ * The idea is basicly to free in two stages, aborting 
+ * between the two if the state turns out to be useful again.
+ * When a free is aborted, the rescued superstate is placed
+ * in the most-favored slot to maximize the time until it
+ * is next semi-freed.
+ *
+ * Overall, this approximates truly freeing superstates in
+ * lru order, but does not involve the cost of updating perfectly 
+ * accurate timestamps every time a superstate is touched.
+ */
+
+#ifdef __STDC__
+static void
+semifree_superstate (struct rx_cache * cache)
+#else
+static void
+semifree_superstate (cache)
+     struct rx_cache * cache;
+#endif
+{
+  int disqualified = cache->semifree_superstates;
+  if (disqualified == cache->superstates)
+    return;
+  while (cache->lru_superstate->locks)
+    {
+      cache->lru_superstate = cache->lru_superstate->next_recyclable;
+      ++disqualified;
+      if (disqualified == cache->superstates)
+       return;
+    }
+  {
+    struct rx_superstate * it = cache->lru_superstate;
+    it->next_recyclable->prev_recyclable = it->prev_recyclable;
+    it->prev_recyclable->next_recyclable = it->next_recyclable;
+    cache->lru_superstate = (it == it->next_recyclable
+                            ? 0
+                            : it->next_recyclable);
+    if (!cache->semifree_superstate)
+      {
+       cache->semifree_superstate = it;
+       it->next_recyclable = it;
+       it->prev_recyclable = it;
+      }
+    else
+      {
+       it->prev_recyclable = cache->semifree_superstate->prev_recyclable;
+       it->next_recyclable = cache->semifree_superstate;
+       it->prev_recyclable->next_recyclable = it;
+       it->next_recyclable->prev_recyclable = it;
+      }
+    {
+      struct rx_distinct_future *df;
+      it->is_semifree = 1;
+      ++cache->semifree_superstates;
+      df = it->transition_refs;
+      if (df)
+       {
+         df->prev_same_dest->next_same_dest = 0;
+         for (df = it->transition_refs; df; df = df->next_same_dest)
+           {
+             df->future_frame.inx = cache->instruction_table[rx_cache_miss];
+             df->future_frame.data = 0;
+             df->future_frame.data_2 = (void *) df;
+             /* If there are any NEXT-CHAR instruction frames that
+              * refer to this state, we convert them to CACHE-MISS frames.
+              */
+             if (!df->effects
+                 && (df->edge->options->next_same_super_edge[0]
+                     == df->edge->options))
+               install_transition (df->present, &df->future_frame,
+                                   df->edge->cset);
+           }
+         df = it->transition_refs;
+         df->prev_same_dest->next_same_dest = df;
+       }
+    }
+  }
+}
+
+
+#ifdef __STDC__
+static void 
+refresh_semifree_superstate (struct rx_cache * cache,
+                            struct rx_superstate * super)
+#else
+static void 
+refresh_semifree_superstate (cache, super)
+     struct rx_cache * cache;
+     struct rx_superstate * super;
+#endif
+{
+  struct rx_distinct_future *df;
+
+  if (super->transition_refs)
+    {
+      super->transition_refs->prev_same_dest->next_same_dest = 0; 
+      for (df = super->transition_refs; df; df = df->next_same_dest)
+       {
+         df->future_frame.inx = cache->instruction_table[rx_next_char];
+         df->future_frame.data = (void *) super->transitions;
+         df->future_frame.data_2 = (void *)(super->contents->is_final);
+         /* CACHE-MISS instruction frames that refer to this state,
+          * must be converted to NEXT-CHAR frames.
+          */
+         if (!df->effects
+             && (df->edge->options->next_same_super_edge[0]
+                 == df->edge->options))
+           install_transition (df->present, &df->future_frame,
+                               df->edge->cset);
+       }
+      super->transition_refs->prev_same_dest->next_same_dest
+       = super->transition_refs;
+    }
+  if (cache->semifree_superstate == super)
+    cache->semifree_superstate = (super->prev_recyclable == super
+                                 ? 0
+                                 : super->prev_recyclable);
+  super->next_recyclable->prev_recyclable = super->prev_recyclable;
+  super->prev_recyclable->next_recyclable = super->next_recyclable;
+
+  if (!cache->lru_superstate)
+    (cache->lru_superstate
+     = super->next_recyclable
+     = super->prev_recyclable
+     = super);
+  else
+    {
+      super->next_recyclable = cache->lru_superstate;
+      super->prev_recyclable = cache->lru_superstate->prev_recyclable;
+      super->next_recyclable->prev_recyclable = super;
+      super->prev_recyclable->next_recyclable = super;
+    }
+  super->is_semifree = 0;
+  --cache->semifree_superstates;
+}
+
+#ifdef __STDC__
+void
+rx_refresh_this_superstate (struct rx_cache * cache,
+                           struct rx_superstate * superstate)
+#else
+void
+rx_refresh_this_superstate (cache, superstate)
+     struct rx_cache * cache;
+     struct rx_superstate * superstate;
+#endif
+{
+  if (superstate->is_semifree)
+    refresh_semifree_superstate (cache, superstate);
+  else if (cache->lru_superstate == superstate)
+    cache->lru_superstate = superstate->next_recyclable;
+  else if (superstate != cache->lru_superstate->prev_recyclable)
+    {
+      superstate->next_recyclable->prev_recyclable
+       = superstate->prev_recyclable;
+      superstate->prev_recyclable->next_recyclable
+       = superstate->next_recyclable;
+      superstate->next_recyclable = cache->lru_superstate;
+      superstate->prev_recyclable = cache->lru_superstate->prev_recyclable;
+      superstate->next_recyclable->prev_recyclable = superstate;
+      superstate->prev_recyclable->next_recyclable = superstate;
+    }
+}
+
+#ifdef __STDC__
+static void 
+release_superset_low (struct rx_cache * cache,
+                    struct rx_superset *set)
+#else
+static void 
+release_superset_low (cache, set)
+     struct rx_cache * cache;
+     struct rx_superset *set;
+#endif
+{
+  if (!--set->refs)
+    {
+      if (set->starts_for)
+       set->starts_for->start_set = 0;
+
+      if (set->cdr)
+       release_superset_low (cache, set->cdr);
+
+      rx_hash_free (&set->hash_item, &cache->superset_hash_rules);
+      rx_cache_free (cache,
+                    sizeof (struct rx_superset),
+                    (char *)set);
+    }
+}
+
+#ifdef __STDC__
+void 
+rx_release_superset (struct rx *rx,
+                    struct rx_superset *set)
+#else
+void 
+rx_release_superset (rx, set)
+     struct rx *rx;
+     struct rx_superset *set;
+#endif
+{
+  release_superset_low (rx->cache, set);
+}
+
+/* This tries to add a new superstate to the superstate freelist.
+ * It might, as a result, free some edge pieces or hash tables.
+ * If nothing can be freed because too many locks are being held, fail.
+ */
+
+#ifdef __STDC__
+static int
+rx_really_free_superstate (struct rx_cache * cache)
+#else
+static int
+rx_really_free_superstate (cache)
+     struct rx_cache * cache;
+#endif
+{
+  int locked_superstates = 0;
+  struct rx_superstate * it;
+
+  if (!cache->superstates)
+    return 0;
+
+  /* scale these */
+  while ((cache->hits + cache->misses) > cache->superstates)
+    {
+      cache->hits >>= 1;
+      cache->misses >>= 1;
+    }
+
+  /* semifree superstates faster than they are freed
+   * so popular states can be rescued.
+   */
+  semifree_superstate (cache);
+  semifree_superstate (cache);
+  semifree_superstate (cache);
+
+#if 0
+  Redundant because semifree_superstate already 
+    makes this check;
+
+
+  while (cache->semifree_superstate && cache->semifree_superstate->locks)
+    {
+      refresh_semifree_superstate (cache, cache->semifree_superstate);
+      ++locked_superstates;
+      if (locked_superstates == cache->superstates)
+       return 0;
+    }
+
+
+  if (cache->semifree_superstate)
+    insert the "it =" block which follows this "if 0" code;
+  else
+    {
+      while (cache->lru_superstate->locks)
+       {
+         cache->lru_superstate = cache->lru_superstate->next_recyclable;
+         ++locked_superstates;
+         if (locked_superstates == cache->superstates)
+           return 0;
+       }
+      it = cache->lru_superstate;
+      it->next_recyclable->prev_recyclable = it->prev_recyclable;
+      it->prev_recyclable->next_recyclable = it->next_recyclable;
+      cache->lru_superstate = ((it == it->next_recyclable)
+                                   ? 0
+                                   : it->next_recyclable);
+    }
+#endif
+
+    if (!cache->semifree_superstate)
+      return 0;
+
+    {
+      it = cache->semifree_superstate;
+      it->next_recyclable->prev_recyclable = it->prev_recyclable;
+      it->prev_recyclable->next_recyclable = it->next_recyclable;
+      cache->semifree_superstate = ((it == it->next_recyclable)
+                                   ? 0
+                                   : it->next_recyclable);
+      --cache->semifree_superstates;
+    }
+
+
+  if (it->transition_refs)
+    {
+      struct rx_distinct_future *df;
+      for (df = it->transition_refs,
+          df->prev_same_dest->next_same_dest = 0;
+          df;
+          df = df->next_same_dest)
+       {
+         df->future_frame.inx = cache->instruction_table[rx_cache_miss];
+         df->future_frame.data = 0;
+         df->future_frame.data_2 = (void *) df;
+         df->future = 0;
+       }
+      it->transition_refs->prev_same_dest->next_same_dest =
+       it->transition_refs;
+    }
+  {
+    struct rx_super_edge *tc = it->edges;
+    while (tc)
+      {
+       struct rx_distinct_future * df;
+       struct rx_super_edge *tct = tc->next;
+       df = tc->options;
+       df->next_same_super_edge[1]->next_same_super_edge[0] = 0;
+       while (df)
+         {
+           struct rx_distinct_future *dft = df;
+           df = df->next_same_super_edge[0];
+           
+           
+           if (dft->future && dft->future->transition_refs == dft)
+             {
+               dft->future->transition_refs = dft->next_same_dest;
+               if (dft->future->transition_refs == dft)
+                 dft->future->transition_refs = 0;
+             }
+           dft->next_same_dest->prev_same_dest = dft->prev_same_dest;
+           dft->prev_same_dest->next_same_dest = dft->next_same_dest;
+           rx_cache_free (cache,
+                          sizeof (struct rx_distinct_future),
+                          (char *)dft);
+         }
+       rx_cache_free (cache,
+                      sizeof (struct rx_super_edge),
+                      (char *)tc);
+       tc = tct;
+      }
+  }
+  
+  if (it->contents->superstate == it)
+    it->contents->superstate = 0;
+  release_superset_low (cache, it->contents);
+  rx_cache_free (cache,
+                (sizeof (struct rx_superstate)
+                 + cache->local_cset_size * sizeof (struct rx_inx)),
+                (char *)it);
+  --cache->superstates;
+  return 1;
+}
+
+
+#ifdef __STDC__
+static char *
+rx_cache_malloc_or_get (struct rx_cache * cache, int size)
+#else
+static char *
+rx_cache_malloc_or_get (cache, size)
+     struct rx_cache * cache;
+     int size;
+#endif
+{
+  while (   (cache->bytes_used + size > cache->bytes_allowed)
+        && rx_really_free_superstate (cache))
+    ;
+
+  return rx_cache_malloc (cache, size);
+}
+
+\f
+
+#ifdef __STDC__
+static int
+supersetcmp (void * va, void * vb)
+#else
+static int
+supersetcmp (va, vb)
+     void * va;
+     void * vb;
+#endif
+{
+  struct rx_superset * a = (struct rx_superset *)va;
+  struct rx_superset * b = (struct rx_superset *)vb;
+  return (   (a == b)
+         || (a && b && (a->id == b->id) && (a->car == b->car) && (a->cdr == b->cdr)));
+}
+
+#define rx_abs(A) (((A) > 0) ? (A) : -(A))
+
+
+#ifdef __STDC__
+static struct rx_hash_item *
+superset_allocator (struct rx_hash_rules * rules, void * val)
+#else
+static struct rx_hash_item *
+superset_allocator (rules, val)
+     struct rx_hash_rules * rules;
+     void * val;
+#endif
+{
+  struct rx_cache * cache;
+  struct rx_superset * template;
+  struct rx_superset * newset;
+
+  cache = ((struct rx_cache *)
+          ((char *)rules
+           - (unsigned long)(&((struct rx_cache *)0)->superset_hash_rules)));
+  template = (struct rx_superset *)val;
+  newset = ((struct rx_superset *)
+           rx_cache_malloc (cache, sizeof (*template)));
+  if (!newset)
+    return 0;
+  {
+    int cdrfinal;
+    int cdredges;
+
+    cdrfinal = (template->cdr
+               ? template->cdr->is_final
+               : 0);
+    cdredges = (template->cdr
+               ? template->cdr->has_cset_edges
+               : 0);
+    
+    newset->is_final = (rx_abs (template->car->is_final) > rx_abs(cdrfinal)
+                       ? template->car->is_final
+                       : template->cdr->is_final);
+    newset->has_cset_edges = (template->car->has_cset_edges || cdredges);
+  }
+  newset->refs = 0;
+  newset->id = template->id;
+  newset->car = template->car;
+  newset->cdr = template->cdr;
+  rx_protect_superset (rx, template->cdr);
+  newset->superstate = 0;
+  newset->starts_for = 0;
+  newset->hash_item.data = (void *)newset;
+  newset->hash_item.binding = 0;
+  return &newset->hash_item;
+}
+
+#ifdef __STDC__
+static struct rx_hash * 
+super_hash_allocator (struct rx_hash_rules * rules)
+#else
+static struct rx_hash * 
+super_hash_allocator (rules)
+     struct rx_hash_rules * rules;
+#endif
+{
+  struct rx_cache * cache;
+  cache = ((struct rx_cache *)
+          ((char *)rules
+           - (unsigned long)(&((struct rx_cache *)0)->superset_hash_rules)));
+  return ((struct rx_hash *)
+         rx_cache_malloc (cache, sizeof (struct rx_hash)));
+}
+
+
+#ifdef __STDC__
+static void
+super_hash_liberator (struct rx_hash * hash, struct rx_hash_rules * rules)
+#else
+static void
+super_hash_liberator (hash, rules)
+     struct rx_hash * hash;
+     struct rx_hash_rules * rules;
+#endif
+{
+  struct rx_cache * cache
+    = ((struct rx_cache *)
+       (char *)rules - (long)(&((struct rx_cache *)0)->superset_hash_rules));
+  rx_cache_free (cache, sizeof (struct rx_hash), (char *)hash);
+}
+
+#ifdef __STDC__
+static void
+superset_hash_item_liberator (struct rx_hash_item * it,
+                             struct rx_hash_rules * rules)
+#else
+static void
+superset_hash_item_liberator (it, rules)
+     struct rx_hash_item * it;
+     struct rx_hash_rules * rules;
+#endif
+{
+}
+
+int rx_cache_bound = 3;
+static int rx_default_cache_got = 0;
+
+static struct rx_cache default_cache = 
+{
+  {
+    supersetcmp,
+    super_hash_allocator,
+    super_hash_liberator,
+    superset_allocator,
+    superset_hash_item_liberator,
+  },
+  0,
+  0,
+  0,
+  0,
+  0,
+  0,
+  0,
+  RX_DEFAULT_DFA_CACHE_SIZE,
+  0,
+  256,
+  rx_id_instruction_table,
+  {
+    0,
+    0,
+    0,
+    0,
+    0
+  }
+};
+struct rx_cache * rx_default_cache = &default_cache;
+
+/* This adds an element to a superstate set.  These sets are lists, such
+ * that lists with == elements are ==.  The empty set is returned by
+ * superset_cons (rx, 0, 0) and is NOT equivelent to 
+ * (struct rx_superset)0.
+ */
+
+#ifdef __STDC__
+struct rx_superset *
+rx_superset_cons (struct rx * rx,
+                 struct rx_nfa_state *car, struct rx_superset *cdr)
+#else
+struct rx_superset *
+rx_superset_cons (rx, car, cdr)
+     struct rx * rx;
+     struct rx_nfa_state *car;
+     struct rx_superset *cdr;
+#endif
+{
+  struct rx_cache * cache = rx->cache;
+  if (!car && !cdr)
+    {
+      if (!cache->empty_superset)
+       {
+         cache->empty_superset
+           = ((struct rx_superset *)
+              rx_cache_malloc (cache, sizeof (struct rx_superset)));
+         if (!cache->empty_superset)
+           return 0;
+         rx_bzero ((char *)cache->empty_superset, sizeof (struct rx_superset));
+         cache->empty_superset->refs = 1000;
+       }
+      return cache->empty_superset;
+    }
+  {
+    struct rx_superset template;
+    struct rx_hash_item * hit;
+    template.car = car;
+    template.cdr = cdr;
+    template.id = rx->rx_id;
+    rx_protect_superset (rx, template.cdr);
+    hit = rx_hash_store (&cache->superset_table,
+                        (unsigned long)car ^ car->id ^ (unsigned long)cdr,
+                        (void *)&template,
+                        &cache->superset_hash_rules);
+    rx_protect_superset (rx, template.cdr);
+    return (hit
+           ?  (struct rx_superset *)hit->data
+           : 0);
+  }
+}
+
+/* This computes a union of two NFA state sets.  The sets do not have the
+ * same representation though.  One is a RX_SUPERSET structure (part
+ * of the superstate NFA) and the other is an NFA_STATE_SET (part of the NFA).
+ */
+
+#ifdef __STDC__
+struct rx_superset *
+rx_superstate_eclosure_union (struct rx * rx, struct rx_superset *set, struct rx_nfa_state_set *ecl) 
+#else
+struct rx_superset *
+rx_superstate_eclosure_union (rx, set, ecl)
+     struct rx * rx;
+     struct rx_superset *set;
+     struct rx_nfa_state_set *ecl;
+#endif
+{
+  if (!ecl)
+    return set;
+
+  if (!set->car)
+    return rx_superset_cons (rx, ecl->car,
+                            rx_superstate_eclosure_union (rx, set, ecl->cdr));
+  if (set->car == ecl->car)
+    return rx_superstate_eclosure_union (rx, set, ecl->cdr);
+
+  {
+    struct rx_superset * tail;
+    struct rx_nfa_state * first;
+
+    if (set->car->id < ecl->car->id)
+      {
+       tail = rx_superstate_eclosure_union (rx, set->cdr, ecl);
+       first = set->car;
+      }
+    else
+      {
+       tail = rx_superstate_eclosure_union (rx, set, ecl->cdr);
+       first = ecl->car;
+      }
+    if (!tail)
+      return 0;
+    else
+      {
+       struct rx_superset * answer;
+       answer = rx_superset_cons (rx, first, tail);
+       if (!answer)
+         {
+           rx_protect_superset (rx, tail);
+           rx_release_superset (rx, tail);
+           return 0;
+         }
+       else
+         return answer;
+      }
+  }
+}
+
+
+\f
+
+/*
+ * This makes sure that a list of rx_distinct_futures contains
+ * a future for each possible set of side effects in the eclosure
+ * of a given state.  This is some of the work of filling in a
+ * superstate transition. 
+ */
+
+#ifdef __STDC__
+static struct rx_distinct_future *
+include_futures (struct rx *rx,
+                struct rx_distinct_future *df, struct rx_nfa_state
+                *state, struct rx_superstate *superstate) 
+#else
+static struct rx_distinct_future *
+include_futures (rx, df, state, superstate)
+     struct rx *rx;
+     struct rx_distinct_future *df;
+     struct rx_nfa_state *state;
+     struct rx_superstate *superstate;
+#endif
+{
+  struct rx_possible_future *future;
+  struct rx_cache * cache = rx->cache;
+  for (future = rx_state_possible_futures (rx, state); future; future = future->next)
+    {
+      struct rx_distinct_future *dfp;
+      struct rx_distinct_future *insert_before = 0;
+      if (df)
+       df->next_same_super_edge[1]->next_same_super_edge[0] = 0;
+      for (dfp = df; dfp; dfp = dfp->next_same_super_edge[0])
+       if (dfp->effects == future->effects)
+         break;
+       else
+         {
+           int order = rx->se_list_cmp (rx, dfp->effects, future->effects);
+           if (order > 0)
+             {
+               insert_before = dfp;
+               dfp = 0;
+               break;
+             }
+         }
+      if (df)
+       df->next_same_super_edge[1]->next_same_super_edge[0] = df;
+      if (!dfp)
+       {
+         dfp
+           = ((struct rx_distinct_future *)
+              rx_cache_malloc (cache,
+                               sizeof (struct rx_distinct_future)));
+         if (!dfp)
+           return 0;
+         if (!df)
+           {
+             df = insert_before = dfp;
+             df->next_same_super_edge[0] = df->next_same_super_edge[1] = df;
+           }
+         else if (!insert_before)
+           insert_before = df;
+         else if (insert_before == df)
+           df = dfp;
+
+         dfp->next_same_super_edge[0] = insert_before;
+         dfp->next_same_super_edge[1]
+           = insert_before->next_same_super_edge[1];
+         dfp->next_same_super_edge[1]->next_same_super_edge[0] = dfp;
+         dfp->next_same_super_edge[0]->next_same_super_edge[1] = dfp;
+         dfp->next_same_dest = dfp->prev_same_dest = dfp;
+         dfp->future = 0;
+         dfp->present = superstate;
+         dfp->future_frame.inx = rx->instruction_table[rx_cache_miss];
+         dfp->future_frame.data = 0;
+         dfp->future_frame.data_2 = (void *) dfp;
+         dfp->side_effects_frame.inx
+           = rx->instruction_table[rx_do_side_effects];
+         dfp->side_effects_frame.data = 0;
+         dfp->side_effects_frame.data_2 = (void *) dfp;
+         dfp->effects = future->effects;
+       }
+    }
+  return df;
+}
+\f
+
+
+/* This constructs a new superstate from its state set.  The only 
+ * complexity here is memory management.
+ */
+#ifdef __STDC__
+struct rx_superstate *
+rx_superstate (struct rx *rx,
+              struct rx_superset *set)
+#else
+struct rx_superstate *
+rx_superstate (rx, set)
+     struct rx *rx;
+     struct rx_superset *set;
+#endif
+{
+  struct rx_cache * cache = rx->cache;
+  struct rx_superstate * superstate = 0;
+
+  /* Does the superstate already exist in the cache? */
+  if (set->superstate)
+    {
+      if (set->superstate->rx_id != rx->rx_id)
+       {
+         /* Aha.  It is in the cache, but belongs to a superstate
+          * that refers to an NFA that no longer exists.
+          * (We know it no longer exists because it was evidently
+          *  stored in the same region of memory as the current nfa
+          *  yet it has a different id.)
+          */
+         superstate = set->superstate;
+         if (!superstate->is_semifree)
+           {
+             if (cache->lru_superstate == superstate)
+               {
+                 cache->lru_superstate = superstate->next_recyclable;
+                 if (cache->lru_superstate == superstate)
+                   cache->lru_superstate = 0;
+               }
+             {
+               superstate->next_recyclable->prev_recyclable
+                 = superstate->prev_recyclable;
+               superstate->prev_recyclable->next_recyclable
+                 = superstate->next_recyclable;
+               if (!cache->semifree_superstate)
+                 {
+                   (cache->semifree_superstate
+                    = superstate->next_recyclable
+                    = superstate->prev_recyclable
+                    = superstate);
+                 }
+               else
+                 {
+                   superstate->next_recyclable = cache->semifree_superstate;
+                   superstate->prev_recyclable
+                     = cache->semifree_superstate->prev_recyclable;
+                   superstate->next_recyclable->prev_recyclable
+                     = superstate;
+                   superstate->prev_recyclable->next_recyclable
+                     = superstate;
+                   cache->semifree_superstate = superstate;
+                 }
+               ++cache->semifree_superstates;
+             }
+           }
+         set->superstate = 0;
+         goto handle_cache_miss;
+       }
+      ++cache->hits;
+      superstate = set->superstate;
+
+      rx_refresh_this_superstate (cache, superstate);
+      return superstate;
+    }
+
+ handle_cache_miss:
+
+  /* This point reached only for cache misses. */
+  ++cache->misses;
+#if RX_DEBUG
+  if (rx_debug_trace > 1)
+    {
+      struct rx_superset * setp = set;
+      fprintf (stderr, "Building a superstet %d(%d): ", rx->rx_id, set);
+      while (setp)
+       {
+         fprintf (stderr, "%d ", setp->id);
+         setp = setp->cdr;
+       }
+      fprintf (stderr, "(%d)\n", set);
+    }
+#endif
+
+  {
+    int superstate_size;
+    superstate_size = (  sizeof (*superstate)
+                      + (  sizeof (struct rx_inx)
+                         * rx->local_cset_size));
+    superstate = ((struct rx_superstate *)
+                 rx_cache_malloc_or_get (cache, superstate_size));
+    ++cache->superstates;
+  }                                                           
+
+  if (!superstate)
+    return 0;
+
+  if (!cache->lru_superstate)
+    (cache->lru_superstate
+     = superstate->next_recyclable
+     = superstate->prev_recyclable
+     = superstate);
+  else
+    {
+      superstate->next_recyclable = cache->lru_superstate;
+      superstate->prev_recyclable = cache->lru_superstate->prev_recyclable;
+      (  superstate->prev_recyclable->next_recyclable
+       = superstate->next_recyclable->prev_recyclable
+       = superstate);
+    }
+  superstate->rx_id = rx->rx_id;
+  superstate->transition_refs = 0;
+  superstate->locks = 0;
+  superstate->is_semifree = 0;
+  set->superstate = superstate;
+  superstate->contents = set;
+  rx_protect_superset (rx, set);
+  superstate->edges = 0;
+  {
+    int x;
+    /* None of the transitions from this superstate are known yet. */
+    for (x = 0; x < rx->local_cset_size; ++x)
+      {
+       struct rx_inx * ifr = &superstate->transitions[x];
+       ifr->inx = rx->instruction_table [rx_cache_miss];
+       ifr->data = ifr->data_2 = 0;
+      }
+  }
+  return superstate;
+}
+\f
+
+/* This computes the destination set of one edge of the superstate NFA.
+ * Note that a RX_DISTINCT_FUTURE is a superstate edge.
+ * Returns 0 on an allocation failure.
+ */
+
+#ifdef __STDC__
+static int 
+solve_destination (struct rx *rx, struct rx_distinct_future *df)
+#else
+static int 
+solve_destination (rx, df)
+     struct rx *rx;
+     struct rx_distinct_future *df;
+#endif
+{
+  struct rx_super_edge *tc = df->edge;
+  struct rx_superset *nfa_state;
+  struct rx_superset *nil_set = rx_superset_cons (rx, 0, 0);
+  struct rx_superset *solution = nil_set;
+  struct rx_superstate *dest;
+
+  rx_protect_superset (rx, solution);
+  /* Iterate over all NFA states in the state set of this superstate. */
+  for (nfa_state = df->present->contents;
+       nfa_state->car;
+       nfa_state = nfa_state->cdr)
+    {
+      struct rx_nfa_edge *e;
+      /* Iterate over all edges of each NFA state. */
+      for (e = nfa_state->car->edges; e; e = e->next)
+        /* If we find an edge that is labeled with 
+        * the characters we are solving for.....
+        */
+       if ((e->type == ne_cset)
+           && rx_bitset_is_subset (rx->local_cset_size,
+                                   tc->cset,
+                                   e->params.cset))
+         {
+           struct rx_nfa_state *n = e->dest;
+           struct rx_possible_future *pf;
+           /* ....search the partial epsilon closures of the destination
+            * of that edge for a path that involves the same set of
+            * side effects we are solving for.
+            * If we find such a RX_POSSIBLE_FUTURE, we add members to the
+            * stateset we are computing.
+            */
+           for (pf = rx_state_possible_futures (rx, n); pf; pf = pf->next)
+             if (pf->effects == df->effects)
+               {
+                 struct rx_superset * old_sol;
+                 old_sol = solution;
+                 solution = rx_superstate_eclosure_union (rx, solution,
+                                                          pf->destset);
+                 if (!solution)
+                   return 0;
+                 rx_protect_superset (rx, solution);
+                 rx_release_superset (rx, old_sol);
+               }
+         }
+    }
+  /* It is possible that the RX_DISTINCT_FUTURE we are working on has 
+   * the empty set of NFA states as its definition.  In that case, this
+   * is a failure point.
+   */
+  if (solution == nil_set)
+    {
+      df->future_frame.inx = (void *) rx_backtrack;
+      df->future_frame.data = 0;
+      df->future_frame.data_2 = 0;
+      return 1;
+    }
+  dest = rx_superstate (rx, solution);
+  rx_release_superset (rx, solution);
+  if (!dest)
+    return 0;
+
+  {
+    struct rx_distinct_future *dft;
+    dft = df;
+    df->prev_same_dest->next_same_dest = 0;
+    while (dft)
+      {
+       dft->future = dest;
+       dft->future_frame.inx = rx->instruction_table[rx_next_char];
+       dft->future_frame.data = (void *) dest->transitions;
+       dft->future_frame.data_2 = (void *) dest->contents->is_final;
+       dft = dft->next_same_dest;
+      }
+    df->prev_same_dest->next_same_dest = df;
+  }
+  if (!dest->transition_refs)
+    dest->transition_refs = df;
+  else
+    {
+      struct rx_distinct_future *dft;
+      dft = dest->transition_refs->next_same_dest;
+      dest->transition_refs->next_same_dest = df->next_same_dest;
+      df->next_same_dest->prev_same_dest = dest->transition_refs;
+      df->next_same_dest = dft;
+      dft->prev_same_dest = df;
+    }
+  return 1;
+}
+
+
+/* This takes a superstate and a character, and computes some edges
+ * from the superstate NFA.  In particular, this computes all edges
+ * that lead from SUPERSTATE given CHR.   This function also 
+ * computes the set of characters that share this edge set.
+ * This returns 0 on allocation error.
+ * The character set and list of edges are returned through 
+ * the paramters CSETOUT and DFOUT.
+ */
+
+#ifdef __STDC__
+static int 
+compute_super_edge (struct rx *rx, struct rx_distinct_future **dfout,
+                         rx_Bitset csetout, struct rx_superstate *superstate,
+                         unsigned char chr)  
+#else
+static int 
+compute_super_edge (rx, dfout, csetout, superstate, chr)
+     struct rx *rx;
+     struct rx_distinct_future **dfout;
+     rx_Bitset csetout;
+     struct rx_superstate *superstate;
+     unsigned char chr;
+#endif
+{
+  struct rx_superset *stateset = superstate->contents;
+
+  /* To compute the set of characters that share edges with CHR, 
+   * we start with the full character set, and subtract.
+   */
+  rx_bitset_universe (rx->local_cset_size, csetout);
+  *dfout = 0;
+
+  /* Iterate over the NFA states in the superstate state-set. */
+  while (stateset->car)
+    {
+      struct rx_nfa_edge *e;
+      for (e = stateset->car->edges; e; e = e->next)
+       if (e->type == ne_cset)
+         {
+           if (!RX_bitset_member (e->params.cset, chr))
+             /* An edge that doesn't apply at least tells us some characters
+              * that don't share the same edge set as CHR.
+              */
+             rx_bitset_difference (rx->local_cset_size, csetout, e->params.cset);
+           else
+             {
+               /* If we find an NFA edge that applies, we make sure there
+                * are corresponding edges in the superstate NFA.
+                */
+               {
+                 struct rx_distinct_future * saved;
+                 saved = *dfout;
+                 *dfout = include_futures (rx, *dfout, e->dest, superstate);
+                 if (!*dfout)
+                   {
+                     struct rx_distinct_future * df;
+                     df = saved;
+                     if (df)
+                       df->next_same_super_edge[1]->next_same_super_edge[0] = 0;
+                     while (df)
+                       {
+                         struct rx_distinct_future *dft;
+                         dft = df;
+                         df = df->next_same_super_edge[0];
+
+                         if (dft->future && dft->future->transition_refs == dft)
+                           {
+                             dft->future->transition_refs = dft->next_same_dest;
+                             if (dft->future->transition_refs == dft)
+                               dft->future->transition_refs = 0;
+                           }
+                         dft->next_same_dest->prev_same_dest = dft->prev_same_dest;
+                         dft->prev_same_dest->next_same_dest = dft->next_same_dest;
+                         rx_cache_free (rx->cache, sizeof (*dft), (char *)dft);
+                       }
+                     return 0;
+                   }
+               }
+               /* We also trim the character set a bit. */
+               rx_bitset_intersection (rx->local_cset_size,
+                                       csetout, e->params.cset);
+             }
+         }
+      stateset = stateset->cdr;
+    }
+  return 1;
+}
+\f
+
+/* This is a constructor for RX_SUPER_EDGE structures.  These are
+ * wrappers for lists of superstate NFA edges that share character sets labels.
+ * If a transition class contains more than one rx_distinct_future (superstate
+ * edge), then it represents a non-determinism in the superstate NFA.
+ */
+
+#ifdef __STDC__
+static struct rx_super_edge *
+rx_super_edge (struct rx *rx,
+              struct rx_superstate *super, rx_Bitset cset,
+              struct rx_distinct_future *df) 
+#else
+static struct rx_super_edge *
+rx_super_edge (rx, super, cset, df)
+     struct rx *rx;
+     struct rx_superstate *super;
+     rx_Bitset cset;
+     struct rx_distinct_future *df;
+#endif
+{
+  struct rx_super_edge *tc;
+  int tc_size;
+
+  tc_size = (  sizeof (struct rx_super_edge)
+            + rx_sizeof_bitset (rx->local_cset_size));
+
+  tc = ((struct rx_super_edge *)
+       rx_cache_malloc (rx->cache, tc_size));
+
+  if (!tc)
+    return 0;
+
+  tc->next = super->edges;
+  super->edges = tc;
+  tc->rx_backtrack_frame.inx = rx->instruction_table[rx_backtrack_point];
+  tc->rx_backtrack_frame.data = 0;
+  tc->rx_backtrack_frame.data_2 = (void *) tc;
+  tc->options = df;
+  tc->cset = (rx_Bitset) ((char *) tc + sizeof (*tc));
+  rx_bitset_assign (rx->local_cset_size, tc->cset, cset);
+  if (df)
+    {
+      struct rx_distinct_future * dfp = df;
+      df->next_same_super_edge[1]->next_same_super_edge[0] = 0;
+      while (dfp)
+       {
+         dfp->edge = tc;
+         dfp = dfp->next_same_super_edge[0];
+       }
+      df->next_same_super_edge[1]->next_same_super_edge[0] = df;
+    }
+  return tc;
+}
+
+
+/* There are three kinds of cache miss.  The first occurs when a
+ * transition is taken that has never been computed during the
+ * lifetime of the source superstate.  That cache miss is handled by
+ * calling COMPUTE_SUPER_EDGE.  The second kind of cache miss
+ * occurs when the destination superstate of a transition doesn't
+ * exist.  SOLVE_DESTINATION is used to construct the destination superstate.
+ * Finally, the third kind of cache miss occurs when the destination
+ * superstate of a transition is in a `semi-free state'.  That case is
+ * handled by UNFREE_SUPERSTATE.
+ *
+ * The function of HANDLE_CACHE_MISS is to figure out which of these
+ * cases applies.
+ */
+
+#ifdef __STDC__
+static void
+install_partial_transition  (struct rx_superstate *super,
+                            struct rx_inx *answer,
+                            RX_subset set, int offset)
+#else
+static void
+install_partial_transition  (super, answer, set, offset)
+     struct rx_superstate *super;
+     struct rx_inx *answer;
+     RX_subset set;
+     int offset;
+#endif
+{
+  int start = offset;
+  int end = start + 32;
+  RX_subset pos = 1;
+  struct rx_inx * transitions = super->transitions;
+  
+  while (start < end)
+    {
+      if (set & pos)
+       transitions[start] = *answer;
+      pos <<= 1;
+      ++start;
+    }
+}
+
+
+#ifdef __STDC__
+struct rx_inx *
+rx_handle_cache_miss (struct rx *rx, struct rx_superstate *super, unsigned char chr, void *data) 
+#else
+struct rx_inx *
+rx_handle_cache_miss (rx, super, chr, data)
+     struct rx *rx;
+     struct rx_superstate *super;
+     unsigned char chr;
+     void *data;
+#endif
+{
+  int offset = chr / RX_subset_bits;
+  struct rx_distinct_future *df = data;
+
+  if (!df)                     /* must be the shared_cache_miss_frame */
+    {
+      /* Perhaps this is just a transition waiting to be filled. */
+      struct rx_super_edge *tc;
+      RX_subset mask = rx_subset_singletons [chr % RX_subset_bits];
+
+      for (tc = super->edges; tc; tc = tc->next)
+       if (tc->cset[offset] & mask)
+         {
+           struct rx_inx * answer;
+           df = tc->options;
+           answer = ((tc->options->next_same_super_edge[0] != tc->options)
+                     ? &tc->rx_backtrack_frame
+                     : (df->effects
+                        ? &df->side_effects_frame
+                        : &df->future_frame));
+           install_partial_transition (super, answer,
+                                       tc->cset [offset], offset * 32);
+           return answer;
+         }
+      /* Otherwise, it's a flushed or  newly encountered edge. */
+      {
+       char cset_space[1024];  /* this limit is far from unreasonable */
+       rx_Bitset trcset;
+       struct rx_inx *answer;
+
+       if (rx_sizeof_bitset (rx->local_cset_size) > sizeof (cset_space))
+         return 0;             /* If the arbitrary limit is hit, always fail */
+                               /* cleanly. */
+       trcset = (rx_Bitset)cset_space;
+       rx_lock_superstate (rx, super);
+       if (!compute_super_edge (rx, &df, trcset, super, chr))
+         {
+           rx_unlock_superstate (rx, super);
+           return 0;
+         }
+       if (!df)                /* We just computed the fail transition. */
+         {
+           static struct rx_inx
+             shared_fail_frame = { 0, 0, (void *)rx_backtrack, 0 };
+           answer = &shared_fail_frame;
+         }
+       else
+         {
+           tc = rx_super_edge (rx, super, trcset, df);
+           if (!tc)
+             {
+               rx_unlock_superstate (rx, super);
+               return 0;
+             }
+           answer = ((tc->options->next_same_super_edge[0] != tc->options)
+                     ? &tc->rx_backtrack_frame
+                     : (df->effects
+                        ? &df->side_effects_frame
+                        : &df->future_frame));
+         }
+       install_partial_transition (super, answer,
+                                   trcset[offset], offset * 32);
+       rx_unlock_superstate (rx, super);
+       return answer;
+      }
+    }
+  else if (df->future) /* A cache miss on an edge with a future? Must be
+                       * a semi-free destination. */
+    {                          
+      if (df->future->is_semifree)
+       refresh_semifree_superstate (rx->cache, df->future);
+      return &df->future_frame;
+    }
+  else
+    /* no future superstate on an existing edge */
+    {
+      rx_lock_superstate (rx, super);
+      if (!solve_destination (rx, df))
+       {
+         rx_unlock_superstate (rx, super);
+         return 0;
+       }
+      if (!df->effects
+         && (df->edge->options->next_same_super_edge[0] == df->edge->options))
+       install_partial_transition (super, &df->future_frame,
+                                   df->edge->cset[offset], offset * 32);
+      rx_unlock_superstate (rx, super);
+      return &df->future_frame;
+    }
+}
+
+
diff --git a/rx/rxsuper.h b/rx/rxsuper.h
new file mode 100644 (file)
index 0000000..59e1ed9
--- /dev/null
@@ -0,0 +1,446 @@
+/* classes: h_files */
+
+#ifndef RXSUPERH
+#define RXSUPERH
+
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+/*  lord       Sun May  7 12:40:17 1995        */
+
+\f
+
+#include "rxnfa.h"
+
+\f
+
+/* This begins the description of the superstate NFA.
+ *
+ * The superstate NFA corresponds to the NFA in these ways:
+ *
+ * Superstate states correspond to sets of NFA states (nfa_states(SUPER)),
+ *
+ * Superstate edges correspond to NFA paths.
+ *
+ * The superstate has no epsilon transitions;
+ * every edge has a character label, and a (possibly empty) side
+ * effect label.   The side effect label corresponds to a list of
+ * side effects that occur in the NFA.  These parts are referred
+ * to as:   superedge_character(EDGE) and superedge_sides(EDGE).
+ *
+ * For a superstate edge EDGE starting in some superstate SUPER,
+ * the following is true (in pseudo-notation :-):
+ *
+ *       exists DEST in nfa_states s.t. 
+ *         exists nfaEDGE in nfa_edges s.t.
+ *                 origin (nfaEDGE) == DEST
+ *              && origin (nfaEDGE) is a member of nfa_states(SUPER)
+ *              && exists PF in possible_futures(dest(nfaEDGE)) s.t.
+ *                     sides_of_possible_future (PF) == superedge_sides (EDGE)
+ *
+ * also:
+ *
+ *      let SUPER2 := superedge_destination(EDGE)
+ *          nfa_states(SUPER2)
+ *           == union of all nfa state sets S s.t.
+ *                          exists PF in possible_futures(dest(nfaEDGE)) s.t.
+ *                            sides_of_possible_future (PF) == superedge_sides (EDGE)
+ *                          && S == dests_of_possible_future (PF) }
+ *
+ * Or in english, every superstate is a set of nfa states.  A given
+ * character and a superstate implies many transitions in the NFA --
+ * those that begin with an edge labeled with that character from a
+ * state in the set corresponding to the superstate.
+ * 
+ * The destinations of those transitions each have a set of possible
+ * futures.  A possible future is a list of side effects and a set of
+ * destination NFA states.  Two sets of possible futures can be
+ * `merged' by combining all pairs of possible futures that have the
+ * same side effects.  A pair is combined by creating a new future
+ * with the same side effect but the union of the two destination sets.
+ * In this way, all the possible futures suggested by a superstate
+ * and a character can be merged into a set of possible futures where
+ * no two elements of the set have the same set of side effects.
+ *
+ * The destination of a possible future, being a set of NFA states, 
+ * corresponds to a supernfa state.  So, the merged set of possible
+ * futures we just created can serve as a set of edges in the
+ * supernfa.
+ *
+ * The representation of the superstate nfa and the nfa is critical.
+ * The nfa has to be compact, but has to facilitate the rapid
+ * computation of missing superstates.  The superstate nfa has to 
+ * be fast to interpret, lazilly constructed, and bounded in space.
+ *
+ * To facilitate interpretation, the superstate data structures are 
+ * peppered with `instruction frames'.  There is an instruction set
+ * defined below which matchers using the supernfa must be able to
+ * interpret.
+ *
+ * We'd like to make it possible but not mandatory to use code
+ * addresses to represent instructions (c.f. gcc's computed goto).
+ * Therefore, we define an enumerated type of opcodes, and when
+ * writing one of these instructions into a data structure, use
+ * the opcode as an index into a table of instruction values.
+ * 
+ * Below are the opcodes that occur in the superstate nfa.
+ *
+ * The descriptions of the opcodes refer to data structures that are
+ * described further below. 
+ */
+
+enum rx_opcode
+{
+  /* 
+   * BACKTRACK_POINT is invoked when a character transition in 
+   * a superstate leads to more than one edge.  In that case,
+   * the edges have to be explored independently using a backtracking
+   * strategy.
+   *
+   * A BACKTRACK_POINT instruction is stored in a superstate's 
+   * transition table for some character when it is known that that
+   * character crosses more than one edge.  On encountering this
+   * instruction, the matcher saves enough state to backtrack to this
+   * point later in the match.
+   */
+  rx_backtrack_point = 0,      /* data is (struct transition_class *) */
+
+  /* 
+   * RX_DO_SIDE_EFFECTS evaluates the side effects of an epsilon path.
+   * There is one occurence of this instruction per rx_distinct_future.
+   * This instruction is skipped if a rx_distinct_future has no side effects.
+   */
+  rx_do_side_effects = rx_backtrack_point + 1,
+
+  /* data is (struct rx_distinct_future *) */
+
+  /* 
+   * RX_CACHE_MISS instructions are stored in rx_distinct_futures whose
+   * destination superstate has been reclaimed (or was never built).
+   * It recomputes the destination superstate.
+   * RX_CACHE_MISS is also stored in a superstate transition table before
+   * any of its edges have been built.
+   */
+  rx_cache_miss = rx_do_side_effects + 1,
+  /* data is (struct rx_distinct_future *) */
+
+  /* 
+   * RX_NEXT_CHAR is called to consume the next character and take the
+   * corresponding transition.  This is the only instruction that uses 
+   * the DATA field of the instruction frame instead of DATA_2.
+   * The comments about rx_inx explain this further.
+   */
+  rx_next_char = rx_cache_miss + 1, /* data is (struct superstate *) */
+
+  /* RX_BACKTRACK indicates that a transition fails.  Don't
+   * confuse this with rx_backtrack_point.
+   */
+  rx_backtrack = rx_next_char + 1, /* no data */
+
+  /* 
+   * RX_ERROR_INX is stored only in places that should never be executed.
+   */
+  rx_error_inx = rx_backtrack + 1, /* Not supposed to occur. */
+
+  rx_num_instructions = rx_error_inx + 1
+};
+
+/* The heart of the matcher is a `word-code-interpreter' 
+ * (like a byte-code interpreter, except that instructions
+ * are a full word wide).
+ *
+ * Instructions are not stored in a vector of code, instead,
+ * they are scattered throughout the data structures built
+ * by the regexp compiler and the matcher.  One word-code instruction,
+ * together with the arguments to that instruction, constitute
+ * an instruction frame (struct rx_inx).
+ *
+ * This structure type is padded by hand to a power of 2 because
+ * in one of the dominant cases, we dispatch by indexing a table
+ * of instruction frames.  If that indexing can be accomplished
+ * by just a shift of the index, we're happy.
+ *
+ * Instructions take at most one argument, but there are two
+ * slots in an instruction frame that might hold that argument.
+ * These are called data and data_2.  The data slot is only
+ * used for one instruction (RX_NEXT_CHAR).  For all other 
+ * instructions, data should be set to 0.
+ *
+ * RX_NEXT_CHAR is the most important instruction by far.
+ * By reserving the data field for its exclusive use, 
+ * instruction dispatch is sped up in that case.  There is
+ * no need to fetch both the instruction and the data,
+ * only the data is needed.  In other words, a `cycle' begins
+ * by fetching the field data.  If that is non-0, then it must
+ * be the destination state of a next_char transition, so
+ * make that value the current state, advance the match position
+ * by one character, and start a new cycle.  On the other hand,
+ * if data is 0, fetch the instruction and do a more complicated
+ * dispatch on that.
+ */
+
+struct rx_inx 
+{
+  void * data;
+  void * data_2;
+  void * inx;
+  void * fnord;
+};
+
+#ifndef RX_TAIL_ARRAY
+#define RX_TAIL_ARRAY  1
+#endif
+
+/* A superstate corresponds to a set of nfa states.  Those sets are
+ * represented by STRUCT RX_SUPERSET.  The constructors
+ * guarantee that only one (shared) structure is created for a given set.
+ */
+struct rx_superset
+{
+  int refs;                    /* This is a reference counted structure. */
+
+  /* We keep these sets in a cache because (in an unpredictable way),
+   * the same set is often created again and again.  
+   *
+   * When an NFA is destroyed, some of the supersets for that NFA
+   * may still exist.  This can lead to false cache hits -- an apparent cache
+   * hit on a superset that properly belongs to an already free NFA.
+   *
+   * When a cache hit appears to occur, we will have in hand the
+   * nfa for which it may have happened.  Every nfa is given
+   * its own sequence number.  The cache is validated
+   * by comparing the nfa sequence number to this field:
+   */
+  int id;
+
+  struct rx_nfa_state * car;   /* May or may not be a valid addr. */
+  struct rx_superset * cdr;
+
+  /* If the corresponding superstate exists: */
+  struct rx_superstate * superstate;
+
+  /* That is_final field of the constiuent nfa states which has the greatest magnitude. */
+  int is_final;
+
+  /* The OR of the corresponding fields of the constiuent nfa states. */
+  int has_cset_edges;
+
+
+  /* There is another bookkeeping problem.  It is expensive to 
+   * compute the starting nfa state set for an nfa.  So, once computed,
+   * it is cached in the `struct rx'.
+   *
+   * But, the state set can be flushed from the superstate cache.
+   * When that happens, the cached value in the `struct rx' has
+   * to be flushed.
+   */
+  struct rx * starts_for;
+
+  /* This is used to link into a hash bucket so these objects can
+   * be `hash-consed'.
+   */
+  struct rx_hash_item hash_item;
+};
+
+#define rx_protect_superset(RX,CON) (++(CON)->refs)
+
+/* The terminology may be confusing (rename this structure?).
+ * Every character occurs in at most one rx_super_edge per super-state.
+ * But, that structure might have more than one option, indicating a point
+ * of non-determinism. 
+ *
+ * In other words, this structure holds a list of superstate edges
+ * sharing a common starting state and character label.  The edges
+ * are in the field OPTIONS.  All superstate edges sharing the same
+ * starting state and character are in this list.
+ */
+struct rx_super_edge
+{
+  struct rx_super_edge *next;
+  struct rx_inx rx_backtrack_frame;
+  int cset_size;
+  rx_Bitset cset;
+  struct rx_distinct_future *options;
+};
+
+/* A superstate is a set of nfa states (RX_SUPERSET) along
+ * with a transition table.  Superstates are built on demand and reclaimed
+ * without warning.  To protect a superstate from this ghastly fate,
+ * use LOCK_SUPERSTATE. 
+ */
+struct rx_superstate
+{
+  int rx_id;                   /* c.f. the id field of rx_superset */
+  int locks;                   /* protection from reclamation */
+
+  /* Within a superstate cache, all the superstates are kept in a big
+   * queue.  The tail of the queue is the state most likely to be
+   * reclaimed.  The *recyclable fields hold the queue position of 
+   * this state.
+   */
+  struct rx_superstate * next_recyclable;
+  struct rx_superstate * prev_recyclable;
+
+  /* The supernfa edges that exist in the cache and that have
+   * this state as their destination are kept in this list:
+   */
+  struct rx_distinct_future * transition_refs;
+
+  /* The list of nfa states corresponding to this superstate: */
+  struct rx_superset * contents;
+
+  /* The list of edges in the cache beginning from this state. */
+  struct rx_super_edge * edges;
+
+  /* A tail of the recyclable queue is marked as semifree.  A semifree
+   * state has no incoming next_char transitions -- any transition
+   * into a semifree state causes a complex dispatch with the side
+   * effect of rescuing the state from its semifree state into a 
+   * fully free state at the head of the queue.
+   *
+   * An alternative to this might be to make next_char more expensive,
+   * and to move a state to the head of the recyclable queue whenever
+   * it is entered.  That way, popular states would never be recycled.
+   *
+   * But unilaterally making next_char more expensive actually loses.
+   * So, incoming transitions are only made expensive for states near
+   * the tail of the recyclable queue.  The more cache contention
+   * there is, the more frequently a state will have to prove itself
+   * and be moved back to the front of the queue.  If there is less 
+   * contention, then popular states just aggregate in the front of 
+   * the queue and stay there.
+   */
+  int is_semifree;
+
+
+  /* This keeps track of the size of the transition table for this
+   * state.  There is a half-hearted attempt to support variable sized
+   * superstates.
+   */
+  int trans_size;
+
+  /* Indexed by characters... */
+  struct rx_inx transitions[RX_TAIL_ARRAY];
+};
+
+
+/* A list of distinct futures define the edges that leave from a 
+ * given superstate on a given character.  c.f. rx_super_edge.
+ */
+struct rx_distinct_future
+{
+  struct rx_distinct_future * next_same_super_edge[2];
+  struct rx_distinct_future * next_same_dest;
+  struct rx_distinct_future * prev_same_dest;
+  struct rx_superstate * present;      /* source state */
+  struct rx_superstate * future;       /* destination state */
+  struct rx_super_edge * edge;
+
+
+  /* The future_frame holds the instruction that should be executed
+   * after all the side effects are done, when it is time to complete
+   * the transition to the next state.
+   *
+   * Normally this is a next_char instruction, but it may be a
+   * cache_miss instruction as well, depending on whether or not
+   * the superstate is in the cache and semifree.
+   * 
+   * If this is the only future for a given superstate/char, and
+   * if there are no side effects to be performed, this frame is
+   * not used (directly) at all.  Instead, its contents are copied
+   * into the transition table of the starting state of this dist. future
+   * (a sort of goto elimination).
+   */
+  struct rx_inx future_frame;
+
+  struct rx_inx side_effects_frame;
+  struct rx_se_list * effects;
+};
+
+#define rx_lock_superstate(R,S)  ((S)->locks++)
+#define rx_unlock_superstate(R,S) (--(S)->locks)
+\f
+struct rx_cache;
+
+#ifdef __STDC__
+typedef void (*rx_morecore_fn)(struct rx_cache *);
+#else
+typedef void (*rx_morecore_fn)();
+#endif
+
+struct rx_cache
+{
+  struct rx_hash_rules superset_hash_rules;
+
+  struct rx_superstate * lru_superstate;
+  struct rx_superstate * semifree_superstate;
+
+  struct rx_superset * empty_superset;
+
+  int superstates;
+  int semifree_superstates;
+  int hits;
+  int misses;
+
+  int bytes_allowed;
+  int bytes_used;
+
+  int local_cset_size;
+  void ** instruction_table;
+
+  struct rx_hash superset_table;
+};
+
+#ifndef RX_DEFAULT_DFA_CACHE_SIZE
+/* This is an upper bound on the number of bytes that may (normally)
+ * be allocated for DFA states.  If this threshold would be exceeded,
+ * Rx tries to flush some DFA states from the cache.
+ *
+ * This value is used whenever the rx_default_cache is used (for example,
+ * with the Posix entry points).
+ */
+#define RX_DEFAULT_DFA_CACHE_SIZE (1 << 19)
+#endif
+
+extern struct rx_cache * rx_default_cache;
+
+\f
+#ifdef __STDC__
+extern char * rx_cache_malloc (struct rx_cache * cache, int size);
+extern void rx_cache_free (struct rx_cache * cache, int size, char * mem);
+extern void rx_release_superset (struct rx *rx,
+                                struct rx_superset *set);
+extern struct rx_superset * rx_superset_cons (struct rx * rx,
+                                             struct rx_nfa_state *car, struct rx_superset *cdr);
+extern struct rx_superset * rx_superstate_eclosure_union (struct rx * rx, struct rx_superset *set, struct rx_nfa_state_set *ecl) ;
+extern struct rx_superstate * rx_superstate (struct rx *rx,
+                                            struct rx_superset *set);
+extern struct rx_inx * rx_handle_cache_miss (struct rx *rx, struct rx_superstate *super, unsigned char chr, void *data) ;
+
+#else /* STDC */
+extern char * rx_cache_malloc ();
+extern void rx_cache_free ();
+extern void rx_release_superset ();
+extern struct rx_superset * rx_superset_cons ();
+extern struct rx_superset * rx_superstate_eclosure_union ();
+extern struct rx_superstate * rx_superstate ();
+extern struct rx_inx * rx_handle_cache_miss ();
+
+#endif /* STDC */
+
+#endif  /* RXSUPERH */
diff --git a/rx/rxunfa.c b/rx/rxunfa.c
new file mode 100644 (file)
index 0000000..f43181d
--- /dev/null
@@ -0,0 +1,269 @@
+/* classes: src_files */
+
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include "rxall.h"
+#include "rx.h"
+#include "rxunfa.h"
+#include "rxnfa.h"
+\f
+
+#ifdef __STDC__
+static int
+unfa_equal (void * va, void * vb)
+#else
+static int
+unfa_equal (va, vb)
+     void * va;
+     void * vb;
+#endif
+{
+  return rx_rexp_equal ((struct rexp_node *)va, (struct rexp_node *)vb);
+}
+
+
+struct rx_hash_rules unfa_rules = { unfa_equal, 0, 0, 0, 0 };
+
+
+#ifdef __STDC__
+static struct rx_cached_rexp *
+canonical_unfa (struct rx_hash * table, struct rexp_node * rexp, int cset_size)
+#else
+static struct rx_cached_rexp *
+canonical_unfa (table, rexp, cset_size)
+     struct rx_hash * table;
+     struct rexp_node * rexp;
+     int cset_size;
+#endif
+{
+  struct rx_hash_item * it;
+  it = rx_hash_store (table, rx_rexp_hash (rexp, 0), rexp, &unfa_rules);
+
+  if (it->binding == 0)
+    {
+      struct rx_cached_rexp * cr;
+
+      if (it->data == (void *)rexp)
+       rx_save_rexp (rexp);
+      
+      cr = (struct rx_cached_rexp *)malloc (sizeof (*cr));
+      rx_bzero ((char *)cr, sizeof (*cr));
+      if (!cr)
+       return 0;
+      it->binding = (void *)cr;
+      cr->unfa.nfa = 0;
+      cr->unfa.exp = rexp;
+      cr->hash_item = it;
+      rx_save_rexp (rexp);
+    }
+  return (struct rx_cached_rexp *)it->binding;
+}
+
+
+#ifdef __STDC__
+static struct rx *
+rx_unfa_rx (struct rx_cached_rexp * cr, struct rexp_node * exp, int cset_size)
+#else
+static struct rx *
+rx_unfa_rx (cr, exp, cset_size)
+     struct rx_cached_rexp * cr;
+     struct rexp_node * exp;
+     int cset_size;
+#endif
+{
+  struct rx * new_rx;
+
+  if (cr->unfa.nfa)
+    return cr->unfa.nfa;
+
+  new_rx = rx_make_rx (cset_size);
+  if (!new_rx)
+    return 0;
+  {
+    struct rx_nfa_state * start;
+    struct rx_nfa_state * end;
+    start = end = 0;
+    if (!rx_build_nfa (new_rx, exp, &start, &end))
+      {
+       free (new_rx);
+       return 0;
+      }
+    new_rx->start_nfa_states = start;
+    end->is_final = 1;
+    start->is_start = 1;
+    {
+      struct rx_nfa_state * s;
+      int x;
+      x = 0;
+      for (s = new_rx->nfa_states; s; s = s->next)
+       s->id = x++;
+    }
+  }
+  cr->unfa.nfa = new_rx;
+  return new_rx;
+}
+
+
+\f
+
+#ifdef __STDC__
+struct rx_unfaniverse *
+rx_make_unfaniverse (int delay)
+#else
+struct rx_unfaniverse *
+rx_make_unfaniverse (delay)
+     int delay;
+#endif
+{
+  struct rx_unfaniverse * it;
+  it = (struct rx_unfaniverse *)malloc (sizeof (*it));
+  if (!it) return 0;
+  rx_bzero ((char *)it, sizeof (*it));
+  it->delay = delay;
+  return it;
+}
+
+
+#ifdef __STDC__
+void
+rx_free_unfaniverse (struct rx_unfaniverse * it)
+#else
+void
+rx_free_unfaniverse (it)
+     struct rx_unfaniverse * it;
+#endif
+{
+}
+
+
+
+\f
+
+#ifdef __STDC__
+struct rx_unfa *
+rx_unfa (struct rx_unfaniverse * unfaniverse, struct rexp_node * exp, int cset_size)
+#else
+struct rx_unfa *
+rx_unfa (unfaniverse, exp, cset_size)
+     struct rx_unfaniverse * unfaniverse;
+     struct rexp_node * exp;
+     int cset_size;
+#endif
+{
+  struct rx_cached_rexp * cr;
+  if (exp && exp->cr)
+    cr = exp->cr;
+  else
+    {
+      cr = canonical_unfa (&unfaniverse->table, exp, cset_size);
+      if (exp)
+       exp->cr = cr;
+    }
+  if (!cr)
+    return 0;
+  if (cr->next)
+    {
+      if (unfaniverse->free_queue == cr)
+       {
+         unfaniverse->free_queue = cr->next;
+         if (unfaniverse->free_queue == cr)
+           unfaniverse->free_queue = 0;
+       }
+      cr->next->prev = cr->prev;
+      cr->prev->next = cr->next;
+      cr->next = 0;
+      cr->prev = 0;
+      --unfaniverse->delayed;
+    }
+  ++cr->unfa.refs;
+  cr->unfa.cset_size = cset_size;
+  cr->unfa.verse = unfaniverse;
+  rx_unfa_rx (cr, exp, cset_size);
+  return &cr->unfa;
+}
+
+
+#ifdef __STDC__
+void
+rx_free_unfa (struct rx_unfa * unfa)
+#else
+void
+rx_free_unfa (unfa)
+     struct rx_unfa * unfa;
+#endif
+{
+  struct rx_cached_rexp * cr;
+  cr = (struct rx_cached_rexp *)unfa;
+  if (!cr)
+    return;
+  if (!--cr->unfa.refs)
+    {
+      if (!unfa->verse->free_queue)
+       {
+         unfa->verse->free_queue = cr;
+         cr->next = cr->prev = cr;
+       }
+      else
+       {
+         cr->next = unfa->verse->free_queue;
+         cr->prev = unfa->verse->free_queue->prev;
+         cr->next->prev = cr;
+         cr->prev->next = cr;
+       }
+
+      ++unfa->verse->delayed;
+      while (unfa->verse->delayed > unfa->verse->delay)
+       {
+         struct rx_cached_rexp * it;
+         it = unfa->verse->free_queue;
+         unfa->verse->free_queue = it->next;
+         if (!--unfa->verse->delayed)
+           unfa->verse->free_queue = 0;
+         it->prev->next = it->next;
+         it->next->prev = it->prev;
+         if (it->unfa.exp)
+           it->unfa.exp->cr = 0;
+         rx_free_rexp ((struct rexp_node *)it->hash_item->data);
+         rx_hash_free (it->hash_item, &unfa_rules);
+         rx_free_rx (it->unfa.nfa);
+         rx_free_rexp (it->unfa.exp);
+         free (it);
+         if (it == cr)
+           break;
+       }
+    }
+  else
+    return;
+}
+
+
+#ifdef __STDC__
+void
+rx_save_unfa (struct rx_unfa * unfa)
+#else
+void
+rx_save_unfa (unfa)
+     struct rx_unfa * unfa;
+#endif
+{
+  ++(unfa->refs);
+}
+
diff --git a/rx/rxunfa.h b/rx/rxunfa.h
new file mode 100644 (file)
index 0000000..aeb589a
--- /dev/null
@@ -0,0 +1,70 @@
+#ifndef RXUNFAH
+#define RXUNFAH
+
+/*     Copyright (C) 1995, 1996 Tom Lord
+ * 
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ * 
+ * This program 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 Library General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Library General Public License
+ * along with this software; see the file COPYING.  If not, write to
+ * the Free Software Foundation, 59 Temple Place - Suite 330, 
+ * Boston, MA 02111-1307, USA. 
+ */
+
+
+\f
+#include "_rx.h"
+
+\f
+struct rx_unfaniverse 
+{
+  int delay;
+  int delayed;
+  struct rx_hash table;
+  struct rx_cached_rexp * free_queue;
+};
+
+
+struct rx_unfa
+{
+  int refs;
+  struct rexp_node * exp;
+  struct rx * nfa;
+  int cset_size;
+  struct rx_unfaniverse * verse;
+};
+
+struct rx_cached_rexp
+{
+  struct rx_unfa unfa;
+  struct rx_cached_rexp * next;
+  struct rx_cached_rexp * prev;
+  struct rx_hash_item * hash_item;
+};
+
+
+\f
+#ifdef __STDC__
+extern struct rx_unfaniverse * rx_make_unfaniverse (int delay);
+extern void rx_free_unfaniverse (struct rx_unfaniverse * it);
+extern struct rx_unfa * rx_unfa (struct rx_unfaniverse * unfaniverse, struct rexp_node * exp, int cset_size);
+extern void rx_free_unfa (struct rx_unfa * unfa);
+extern void rx_save_unfa (struct rx_unfa * unfa);
+
+#else /* STDC */
+extern struct rx_unfaniverse * rx_make_unfaniverse ();
+extern void rx_free_unfaniverse ();
+extern struct rx_unfa * rx_unfa ();
+extern void rx_free_unfa ();
+extern void rx_save_unfa ();
+
+#endif /* STDC */
+#endif  /* RXUNFAH */
diff --git a/sockcheck.conf.example b/sockcheck.conf.example
new file mode 100644 (file)
index 0000000..a64dd7d
--- /dev/null
@@ -0,0 +1,53 @@
+/* This file describes what proxy tests to run: what ports to connect
+ * to, what to send to them, what to look for, and what to do if that
+ * is found.
+ */
+
+/* Connect on port 1080, sending "\5\1\0" as challenge.
+ * If we get "\5\0" as a response, it's an unsecured socks5. */
+"1080:050100" {
+       "0500" "reject:Unsecured socks5";
+};
+
+/* Connect on port 1080, sending "\4\1" followed by the port
+ * and IP of the client, followed by the (NUL-terminated) ident to
+ * use.  If we get a four byte response with '\x5a' as the second
+ * byte, it's an unsecured socks4 proxy.
+ *
+ * It would be generally wise to replace the $p$i with a hard-coded
+ * one; many insecure proxies refuse to connect to themselves.
+ */
+"1080:0401$i$p=p=r=o=x=y00" {
+       "..5a...." "reject:Unsecured socks4";
+};
+
+"23:" {
+        // This first test is interesting: multi-stage, and a default action is reject
+        // this crap at the front is the router trying to negotiate telnet options
+        "fffb01fffb03fffd18fffd1f0d0a0d0a=U=s=e=r= =A=c=c=e=s=s= =V=e=r=i=f=i=c=a=t=i=o=n0d0a0d0a=P=a=s=s=w=o=r=d3a= :=c=i=s=c=o0d0a" {
+                "0d0a=P=a=s=s=w=o=r=d3a= " "accept";
+                "other" "reject:[1 hour] Cisco router with default password, visit http://www.gamesnet.net/proxyglines.php for more information.";
+        };
+       "=W=i=n=G=a=t=e=>" "reject:Unsecured wingate";
+       "=T=o=o= =m=a=n=y" "reject:Unsecured wingate";
+       "=E=n=t=e=r= =h=o=s=t= =n=a=m=e" "reject:Unsecured wingate";
+        // the 3a is ':'; due to a parser glitch, =: isn't parsed like you might expect
+       "=E=n=t=e=r= 3a= =<=h=o=s=t=>" "reject:Unsecured wingate";
+};
+
+/* Connect on port 3128 (squid), trying to use a HTTP CONNECT
+ * proxy.  If we get a 200 response, it worked and should be
+ * booted.
+ * If you do this check on port 80, you might check for "200
+ * Connection" instead to reduce false positives; many servers
+ * send 200 OK responses for custom 404 Error pages.
+ * As with the SOCKS4 check, you may want to replace the $c:3128
+ * (client hostname and port) with a hard-coded one.
+ */
+"3128:=C=O=N=N=E=C=T= $c=:=3=1=2=8= =H=T=T=P=/=1=.=00d0a0d0a" {
+       "=H=T=T=P=/=1=.=0= =2=0=0" "reject:Unsecured proxy";
+};
+
+"27374:" {
+       "" "reject:Subseven detected";
+};
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644 (file)
index 0000000..205d4c6
--- /dev/null
@@ -0,0 +1,56 @@
+AM_CPPFLAGS = @RX_INCLUDES@
+LIBS = @LIBS@ @RX_LIBS@
+
+ARCH_REVISION=$(shell tla logs -f | tail -n 1)
+noinst_PROGRAMS = srvx expnhelp
+EXTRA_PROGRAMS = checkdb globtest
+noinst_DATA = chanserv.help global.help modcmd.help nickserv.help opserv.help saxdb.help sendmail.help mod-sockcheck.help mod-helpserv.help mod-memoserv.help
+EXTRA_DIST = nickserv.help.m4 $(noinst_DATA)
+BUILT_SOURCES = arch-version.h
+noinst_HEADERS = arch-version.h
+nickserv.help: nickserv.help.m4 expnhelp
+       ./expnhelp < $(srcdir)/nickserv.help.m4 > $@
+arch-version.h:
+       @if [ -e $@ ] ; then OLD_REVISION=`cat $@` ; else OLD_REVISION="" ; fi ; \
+       ARCH_REVISION=`tla logs -f | tail -n 1` ; \
+       VERSION_CONTENTS="#define ARCH_VERSION \"$$ARCH_REVISION\"" ; \
+       if [ "z" != "z$$ARCH_REVISION" -a "z$$OLD_REVISION" != "z$$VERSION_CONTENTS" ] ; then \
+           echo "Putting new arch version into $@" ; \
+           echo $$VERSION_CONTENTS > $@ ; \
+       fi
+
+EXTRA_srvx_SOURCES = proto-bahamut.c proto-common.c proto-p10.c mod-snoop.c mod-memoserv.c mod-helpserv.c mod-sockcheck.c
+srvx_LDADD = @MODULE_OBJS@
+srvx_DEPENDENCIES = @MODULE_OBJS@
+srvx_SOURCES = \
+       arch-version.h \
+       chanserv.c chanserv.h \
+       compat.c compat.h \
+       conf.c conf.h \
+       dict-splay.c dict.h \
+       getopt.c getopt.h \
+       getopt1.c getopt.h \
+       gline.c gline.h \
+       global.c global.h \
+       hash.c hash.h \
+       heap.c heap.h \
+       helpfile.c helpfile.h \
+       ioset.c ioset.h \
+       log.c log.h \
+       main.c common.h \
+       md5.c md5.h \
+       modcmd.c modcmd.h \
+       modules.c modules.h \
+       nickserv.c nickserv.h \
+       opserv.c opserv.h \
+       policer.c policer.h \
+       proto.h \
+       recdb.c recdb.h \
+       saxdb.c saxdb.h \
+       sendmail.c sendmail.h \
+       timeq.c timeq.h \
+       tools.c
+
+expnhelp_SOURCES = common.h compat.c compat.h dict-splay.c dict.h expnhelp.c log.h recdb.c recdb.h tools.c
+checkdb_SOURCES = checkdb.c common.h compat.c compat.h dict-splay.c dict.h recdb.c recdb.h saxdb.c saxdb.h tools.c conf.h log.h modcmd.h saxdb.h timeq.h
+globtest_SOURCES = common.h compat.c compat.h dict-splay.c dict.h globtest.c tools.c
diff --git a/src/chanserv.c b/src/chanserv.c
new file mode 100644 (file)
index 0000000..32e7c0b
--- /dev/null
@@ -0,0 +1,7062 @@
+/* chanserv.c - Channel service bot
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "chanserv.h"
+#include "conf.h"
+#include "global.h"
+#include "modcmd.h"
+#include "opserv.h" /* for opserv_bad_channel() */
+#include "saxdb.h"
+#include "timeq.h"
+
+#define CHANSERV_CONF_NAME     "services/chanserv"
+
+/* ChanServ options */
+#define KEY_SUPPORT_CHANNEL     "support_channel"
+#define KEY_SUPPORT_CHANNEL_MODES "support_channel_modes"
+#define KEY_DB_BACKUP_FREQ     "db_backup_freq"
+#define KEY_INFO_DELAY         "info_delay"
+#define KEY_MAX_GREETLEN               "max_greetlen"
+#define KEY_ADJUST_THRESHOLD           "adjust_threshold"
+#define KEY_ADJUST_DELAY       "adjust_delay"
+#define KEY_CHAN_EXPIRE_FREQ   "chan_expire_freq"
+#define KEY_CHAN_EXPIRE_DELAY  "chan_expire_delay"
+#define KEY_MAX_CHAN_USERS             "max_chan_users"
+#define KEY_MAX_CHAN_BANS      "max_chan_bans"
+#define KEY_NICK               "nick"
+#define KEY_OLD_CHANSERV_NAME  "old_chanserv_name"
+#define KEY_MAX_SWITCH_LOAD    "max_switch_load"
+#define KEY_SWITCH_TIMEOUT     "switch_timeout"
+#define KEY_8BALL_RESPONSES     "8ball"
+#define KEY_OLD_BAN_NAMES       "old_ban_names"
+#define KEY_REFRESH_PERIOD      "refresh_period"
+#define KEY_CTCP_SHORT_BAN_DURATION "ctcp_short_ban_duration"
+#define KEY_CTCP_LONG_BAN_DURATION  "ctcp_long_ban_duration"
+#define KEY_MAX_OWNED               "max_owned"
+#define KEY_IRC_OPERATOR_EPITHET    "irc_operator_epithet"
+#define KEY_NETWORK_HELPER_EPITHET  "network_helper_epithet"
+#define KEY_SUPPORT_HELPER_EPITHET  "support_helper_epithet"
+#define KEY_NODELETE_LEVEL      "nodelete_level"
+
+/* ChanServ database */
+#define KEY_CHANNELS           "channels"
+#define KEY_NOTE_TYPES          "note_types"
+
+/* Note type parameters */
+#define KEY_NOTE_OPSERV_ACCESS  "opserv_access"
+#define KEY_NOTE_CHANNEL_ACCESS "channel_access"
+#define KEY_NOTE_SETTER_ACCESS  "setter_access"
+#define KEY_NOTE_VISIBILITY     "visibility"
+#define KEY_NOTE_VIS_PRIVILEGED "privileged"
+#define KEY_NOTE_VIS_CHANNEL_USERS "channel_users"
+#define KEY_NOTE_VIS_ALL        "all"
+#define KEY_NOTE_MAX_LENGTH     "max_length"
+#define KEY_NOTE_SETTER         "setter"
+#define KEY_NOTE_NOTE           "note"
+
+/* Do-not-register channels */
+#define KEY_DNR                 "dnr"
+#define KEY_DNR_SET             "set"
+#define KEY_DNR_SETTER          "setter"
+#define KEY_DNR_REASON          "reason"
+
+/* Channel data */
+#define KEY_REGISTERED         "registered"
+#define KEY_REGISTRAR          "registrar"
+#define KEY_SUSPENDED           "suspended"
+#define KEY_PREVIOUS            "previous"
+#define KEY_SUSPENDER          "suspender"
+#define KEY_ISSUED              "issued"
+#define KEY_REVOKED             "revoked"
+#define KEY_SUSPEND_EXPIRES     "suspend_expires"
+#define KEY_SUSPEND_REASON      "suspend_reason"
+#define KEY_VISITED            "visited"
+#define KEY_TOPIC              "topic"
+#define KEY_GREETING           "greeting"
+#define KEY_USER_GREETING      "user_greeting"
+#define KEY_MODES              "modes"
+#define KEY_FLAGS              "flags"
+#define KEY_OPTIONS             "options"
+#define KEY_USERS              "users"
+#define KEY_BANS               "bans"
+#define KEY_MAX                        "max"
+#define KEY_NOTES               "notes"
+#define KEY_TOPIC_MASK          "topic_mask"
+
+/* User data */
+#define KEY_LEVEL              "level"
+#define KEY_INFO               "info"
+#define KEY_SEEN               "seen"
+
+/* Ban data */
+#define KEY_OWNER              "owner"
+#define KEY_REASON             "reason"
+#define KEY_SET                        "set"
+#define KEY_DURATION           "duration"
+#define KEY_EXPIRES             "expires"
+#define KEY_TRIGGERED          "triggered"
+
+#define CHANNEL_DEFAULT_FLAGS   (CHANNEL_INFO_LINES)
+#define CHANNEL_DEFAULT_OPTIONS "lmoooanpcnat"
+
+/* Administrative messages */
+static const struct message_entry msgtab[] = {
+    { "CSMSG_CHANNELS_EXPIRED", "%i channels expired." },
+
+/* Channel registration */
+    { "CSMSG_REG_SUCCESS", "You now have ownership of $b%s$b." },
+    { "CSMSG_PROXY_SUCCESS", "%s now has ownership of $b%s$b." },
+    { "CSMSG_ALREADY_REGGED", "$b%s$b is registered to someone else." },
+    { "CSMSG_MUST_BE_OPPED", "You must be a channel operator in $b%s$b to register it." },
+    { "CSMSG_PROXY_FORBIDDEN", "You may not register a channel for someone else." },
+    { "CSMSG_OWN_TOO_MANY", "%s already owns enough channels (at least %d); use FORCE to override." },
+
+/* Do-not-register channels */
+    { "CSMSG_NOT_DNR", "$b%s$b is not a valid channel name or *account." },
+    { "CSMSG_DNR_SEARCH_RESULTS", "The following do-not-registers were found:" },
+    { "CSMSG_DNR_INFO", "$b%s$b is do-not-register (by $b%s$b): %s" },
+    { "CSMSG_DNR_INFO_SET", "$b%s$b is do-not-register (set %s by $b%s$b): %s" },
+    { "CSMSG_MORE_DNRS", "%d more do-not-register entries skipped." },
+    { "CSMSG_DNR_CHANNEL", "Only network staff may register $b%s$b." },
+    { "CSMSG_DNR_CHANNEL_MOVE", "Only network staff may move $b%s$b." },
+    { "CSMSG_DNR_ACCOUNT", "Only network staff may register channels to $b%s$b." },
+    { "CSMSG_NOREGISTER_CHANNEL", "$b%s$b has been added to the do-not-register list." },
+    { "CSMSG_NO_SUCH_DNR", "$b%s$b is not in the do-not-register list." },
+    { "CSMSG_DNR_REMOVED", "$b%s$b has been removed from the do-not-register list." },
+
+/* Channel unregistration */
+    { "CSMSG_UNREG_SUCCESS", "$b%s$b has been unregistered." },
+    { "CSMSG_UNREG_NODELETE", "$b%s$b is protected from unregistration." },
+    { "CSMSG_CHAN_SUSPENDED", "$b$C$b access to $b%s$b has been temporarily suspended (%s)." },
+    { "CSMSG_CONFIRM_UNREG", "To confirm this unregistration, you must use 'unregister %s'." },
+
+/* Channel moving */
+    { "CSMSG_MOVE_SUCCESS", "Channel registration has been moved to $b%s$b." },
+    { "CSMSG_MOVE_NODELETE", "$b%s$b is protected from unregistration, and cannot be moved." },
+
+/* Channel merging */
+    { "CSMSG_MERGE_SUCCESS", "Channel successfully merged into $b%s$b." },
+    { "CSMSG_MERGE_SELF", "Merging cannot be performed if the source and target channels are the same." },
+    { "CSMSG_MERGE_NODELETE", "You may not merge a channel that is marked NoDelete." },
+    { "CSMSG_MERGE_SUSPENDED", "Merging cannot be performed if the source or target channel is suspended." },
+    { "CSMSG_MERGE_NOT_OWNER", "You must be the owner of the target channel (or a helper) to merge into the channel." },
+
+/* Handle unregistration */
+    { "CSMSG_HANDLE_UNREGISTERED", "As a result of your account unregistration, you have been deleted from all of your channels' userlists." },
+
+/* Error messages */
+    { "CSMSG_NOT_USER", "You lack access to $b%s$b." },
+    { "CSMSG_NO_CHAN_USER", "%s lacks access to $b%s$b." },
+    { "CSMSG_NO_ACCESS", "You lack sufficient access to use this command." },
+    { "CSMSG_NOT_REGISTERED", "$b%s$b has not been registered with $b$C$b." },
+    { "CSMSG_MAXIMUM_BANS", "This channel has reached the ban count limit of $b%d$b." },
+    { "CSMSG_MAXIMUM_USERS", "This channel has reached the user count limit of $b%d$b." },
+    { "CSMSG_ILLEGAL_CHANNEL", "$b%s$b is an illegal channel, and cannot be registered." },
+    { "CSMSG_GODMODE_UP", "You may not use $b%s$b to op yourself unless you are on the user list.  Use the $bop$b command instead." },
+    { "CSMSG_ALREADY_OPPED", "You are already opped in $b%s$b." },
+    { "CSMSG_ALREADY_VOICED", "You are already voiced in $b%s$b." },
+    { "CSMSG_ALREADY_DOWN", "You are not opped or voiced in $b%s$b." },
+    { "CSMSG_ALREADY_OPCHANNED", "There has been no net.join since the last opchan in $b%s$b." },
+    { "CSMSG_OPCHAN_DONE", "I have (re-)opped myself in $b%s$b." },
+
+/* Removing yourself from a channel. */
+    { "CSMSG_NO_OWNER_DELETEME", "You cannot delete your owner access in $b%s$b." },
+    { "CSMSG_CONFIRM_DELETEME", "To really remove yourself, you must use 'deleteme %s'." },
+    { "CSMSG_DELETED_YOU", "Your $b%d$b access has been deleted from $b%s$b." },
+
+/* User management */
+    { "CSMSG_ADDED_USER", "Added new %s to the %s user list with access %d." },
+    { "CSMSG_DELETED_USER", "Deleted %s (with access %d) from the %s user list." },
+    { "CSMSG_BAD_RANGE", "Invalid access range; minimum (%d) must be greater than maximum (%d)." },
+    { "CSMSG_DELETED_USERS", "Deleted accounts matching $b%s$b with access from $b%d$b to $b%d$b from the %s user list." },
+    { "CSMSG_TRIMMED_USERS", "Trimmed $b%d users$b with access from %d to %d from the %s user list who were inactive for at least %s." },
+    { "CSMSG_INCORRECT_ACCESS", "%s has access $b%d$b, not %s." },
+    { "CSMSG_USER_EXISTS", "%s is already on the $b%s$b user list (with access %d)." },
+    { "CSMSG_CANNOT_TRIM", "You must include a minimum inactivity duration of at least 60 seconds to trim." },
+
+    { "CSMSG_NO_SELF_CLVL", "You cannot change your own access." },
+    { "CSMSG_NO_BUMP_ACCESS", "You cannot give users access greater than or equal to your own." },
+    { "CSMSG_MULTIPLE_OWNERS", "There is more than one owner in %s; please use $bCLVL$b, $bDELOWNER$b and/or $bADDOWNER$b instead." },
+    { "CSMSG_NO_TRANSFER_SELF", "You cannot give ownership to your own account." },
+    { "CSMSG_OWNERSHIP_GIVEN", "Ownership of $b%s$b has been transferred to account $b%s$b." },
+
+/* Ban management */
+    { "CSMSG_BAN_ADDED", "Permanently banned $b%s$b from %s." },
+    { "CSMSG_TIMED_BAN_ADDED", "Banned $b%s$b from %s for %s." },
+    { "CSMSG_KICK_BAN_DONE", "Kickbanned $b%s$b from %s." },
+    { "CSMSG_BAN_DONE", "Banned $b%s$b from %s." },
+    { "CSMSG_REASON_CHANGE", "Reason for ban $b%s$b changed." },
+    { "CSMSG_BAN_EXTENDED", "Extended ban for $b%s$b expires in %s." },
+    { "CSMSG_BAN_REMOVED", "Matching ban(s) for $b%s$b removed." },
+    { "CSMSG_TRIMMED_BANS", "Trimmed $b%d bans$b from the %s ban list that were inactive for at least %s." },
+    { "CSMSG_REDUNDANT_BAN", "$b%s$b is already banned in %s." },
+    { "CSMSG_DURATION_TOO_LOW", "Timed bans must last for at least 15 seconds." },
+    { "CSMSG_DURATION_TOO_HIGH", "Timed bans must last for less than 2 years." },
+    { "CSMSG_LAME_MASK", "$b%s$b is a little too general. Try making it more specific." },
+    { "CSMSG_MASK_PROTECTED", "Sorry, ban for $b%s$b conflicts with a protected user's hostmask." },
+    { "CSMSG_NO_MATCHING_USERS", "No one in $b%s$b has a hostmask matching $b%s$b." },
+    { "CSMSG_BAN_NOT_FOUND", "Sorry, no ban found for $b%s$b." },
+    { "CSMSG_BANLIST_FULL", "The $b%s$b channel ban list is $bfull$b." },
+
+    { "CSMSG_INVALID_TRIM", "$b%s$b isn't a valid trim target." },
+
+/* Channel management */
+    { "CSMSG_CHANNEL_OPENED", "$b%s$b has been opened." },
+    { "CSMSG_WIPED_INFO_LINE", "Removed $b%s$b's infoline in $b%s$b." },
+    { "CSMSG_RESYNCED_USERS", "Synchronized users in $b%s$b with the userlist." },
+
+    { "CSMSG_TOPIC_SET", "Topic is now '%s'." },
+    { "CSMSG_NO_TOPIC", "$b%s$b does not have a default topic." },
+    { "CSMSG_TOPICMASK_CONFLICT1", "I do not know how to make that topic work with the current topic mask in $b%s$b, which is: %s" },
+    { "CSMSG_TOPICMASK_CONFLICT2", "Please make sure your topic at most %d characters and matches the topic mask pattern." },
+    { "CSMSG_TOPIC_LOCKED", "The %s topic is locked." },
+    { "CSMSG_MASK_BUT_NO_TOPIC", "Warning: $b%s$b does not have a default topic, but you just set the topic mask." },
+    { "CSMSG_TOPIC_MISMATCH", "Warning: The default topic for $b%s$b does not match the topic mask; changing it anyway." },
+
+    { "CSMSG_MODES_SET", "Channel modes are now $b%s$b." },
+    { "CSMSG_DEFAULTED_MODES", "Channel modes for $b%s$b are set to their defaults." },
+    { "CSMSG_NO_MODES", "$b%s$b does not have any default modes." },
+    { "CSMSG_MODE_LOCKED", "Modes conflicting with $b%s$b are not allowed in %s." },
+    { "CSMSG_CANNOT_SET", "That setting is above your current level, so you cannot change it." },
+    { "CSMSG_OWNER_DEFAULTS", "You must have access 500 in %s to reset it to the default options." },
+    { "CSMSG_CONFIRM_DEFAULTS", "To reset %s's settings to the defaults, you muse use 'set defaults %s'." },
+    { "CSMSG_SETTINGS_DEFAULTED", "All settings for %s have been reset to default values." },
+    { "CSMSG_BAD_SETLEVEL", "You cannot change any setting to above your level." },
+    { "CSMSG_INVALID_MODE_LOCK", "$b%s$b is an invalid mode lock." },
+    { "CSMSG_INVALID_NUMERIC",   "$b%d$b is not a valid choice.  Choose one:" },
+    { "CSMSG_SET_DEFAULT_TOPIC", "$bDefaultTopic$b %s" },
+    { "CSMSG_SET_TOPICMASK",     "$bTopicMask   $b %s" },
+    { "CSMSG_SET_GREETING",      "$bGreeting    $b %s" },
+    { "CSMSG_SET_USERGREETING",  "$bUserGreeting$b %s" },
+    { "CSMSG_SET_MODES",         "$bModes       $b %s" },
+    { "CSMSG_SET_NODELETE",      "$bNoDelete    $b %s" },
+    { "CSMSG_SET_USERINFO",      "$bUserInfo    $b %s" },
+    { "CSMSG_SET_VOICE",         "$bVoice       $b %s" },
+    { "CSMSG_SET_DYNLIMIT",      "$bDynLimit    $b %s" },
+    { "CSMSG_SET_TOPICSNARF",    "$bTopicSnarf  $b %s" },
+    { "CSMSG_SET_PEONINVITE",    "$bPeonInvite  $b %s" },
+    { "CSMSG_SET_ENFOPS",        "$bEnfOps      $b %d" },
+    { "CSMSG_SET_GIVE_OPS",      "$bGiveOps     $b %d" },
+    { "CSMSG_SET_ENFMODES",      "$bEnfModes    $b %d" },
+    { "CSMSG_SET_ENFTOPIC",      "$bEnfTopic    $b %d" },
+    { "CSMSG_SET_PUBCMD",        "$bPubCmd      $b %d" },
+    { "CSMSG_SET_SETTERS",       "$bSetters     $b %d" },
+    { "CSMSG_SET_CTCPUSERS",     "$bCTCPUsers   $b %d" },
+    { "CSMSG_SET_PROTECT",       "$bProtect     $b %d - %s" },
+    { "CSMSG_SET_TOYS",          "$bToys        $b %d - %s" },
+    { "CSMSG_SET_CTCPREACTION",  "$bCTCPReaction$b %d - %s" },
+    { "CSMSG_SET_TOPICREFRESH",  "$bTopicRefresh$b %d - %s" },
+    { "CSMSG_USET_NOAUTOOP",     "$bNoAutoOp    $b %s" },
+    { "CSMSG_USET_NOAUTOVOICE",  "$bNoAutoVoice $b %s" },
+    { "CSMSG_USET_AUTOINVITE",   "$bAutoInvite  $b %s" },
+    { "CSMSG_USET_INFO",         "$bInfo        $b %s" },
+
+    { "CSMSG_USER_PROTECTED", "Sorry, $b%s$b is protected." },
+    { "CSMSG_OPBY_LOCKED", "You may not op users who lack op or greater access." },
+    { "CSMSG_PROCESS_FAILED", "$b$C$b could not process some of the nicks you provided." },
+    { "CSMSG_OPPED_USERS", "Opped users in $b%s$b." },
+    { "CSMSG_DEOPPED_USERS", "Deopped users in $b%s$b." },
+    { "CSMSG_VOICED_USERS", "Voiced users in $b%s$b." },
+    { "CSMSG_DEVOICED_USERS", "Devoiced users in $b%s$b." },
+    { "CSMSG_PROTECT_ALL", "Non-users and users will be protected from those of equal or lower access." },
+    { "CSMSG_PROTECT_EQUAL", "Users will be protected from those of equal or lower access." },
+    { "CSMSG_PROTECT_LOWER", "Users will be protected from those of lower access." },
+    { "CSMSG_PROTECT_NONE", "No users will be protected." },
+    { "CSMSG_TOYS_DISABLED", "Toys are completely disabled." },
+    { "CSMSG_TOYS_PRIVATE", "Toys will only reply privately." },
+    { "CSMSG_TOYS_PUBLIC", "Toys will reply publicly." },
+    { "CSMSG_TOPICREFRESH_NEVER", "Never refresh topic." },
+    { "CSMSG_TOPICREFRESH_3_HOURS", "Refresh every 3 hours." },
+    { "CSMSG_TOPICREFRESH_6_HOURS", "Refresh every 6 hours." },
+    { "CSMSG_TOPICREFRESH_12_HOURS", "Refresh every 12 hours." },
+    { "CSMSG_TOPICREFRESH_24_HOURS", "Refresh every 24 hours." },
+    { "CSMSG_CTCPREACTION_KICK", "Kick on disallowed CTCPs" },
+    { "CSMSG_CTCPREACTION_KICKBAN", "Kickban on disallowed CTCPs" },
+    { "CSMSG_CTCPREACTION_SHORTBAN",  "Short timed ban on disallowed CTCPs" },
+    { "CSMSG_CTCPREACTION_LONGBAN", "Long timed ban on disallowed CTCPs" },
+
+    { "CSMSG_INVITED_USER", "Invited $b%s$b to join %s." },
+    { "CSMSG_INVITING_YOU", "$b%s$b invites you to join %s%s%s" },
+    { "CSMSG_ALREADY_PRESENT", "%s is $balready in %s$b." },
+    { "CSMSG_YOU_ALREADY_PRESENT", "You are already in $b%s$b." },
+    { "CSMSG_LOW_CHANNEL_ACCESS", "You lack sufficient access in %s to use this command." },
+
+    { "CSMSG_KICK_DONE", "Kicked $b%s$b from %s." },
+    { "CSMSG_NO_BANS", "No channel bans found on $b%s$b." },
+    { "CSMSG_BANS_REMOVED", "Removed all channel bans from $b%s$b." },
+
+/* Channel userlist */
+    { "CSMSG_ACCESS_ALL_HEADER", "%s users from level %d to %d:" },
+    { "CSMSG_ACCESS_SEARCH_HEADER", "%s users from level %d to %d matching %s:" },
+    { "CSMSG_INVALID_ACCESS", "$b%s$b is an invalid access level." },
+    { "CSMSG_CHANGED_ACCESS", "%s now has access $b%d$b in %s." },
+
+/* Channel note list */
+    { "CSMSG_NOTELIST_HEADER", "Notes for $b%s$b:" },
+    { "CSMSG_REPLACED_NOTE", "Replaced old $b%s$b note on %s (set by %s): %s" },
+    { "CSMSG_NOTE_FORMAT", "%s (set by %s): %s" },
+    { "CSMSG_NOTELIST_END", "End of notes for $b%s$b." },
+    { "CSMSG_NOTELIST_EMPTY", "There are no (visible) notes for $b%s$b." },
+    { "CSMSG_NO_SUCH_NOTE", "Channel $b%s$b does not have a note named $b%s$b." },
+    { "CSMSG_BAD_NOTE_TYPE", "Note type $b%s$b does not exist." },
+    { "CSMSG_NOTE_SET", "Note $b%s$b set in channel $b%s$b." },
+    { "CSMSG_NOTE_REMOVED", "Note $b%s$b removed in channel $b%s$b." },
+    { "CSMSG_BAD_NOTE_ACCESS", "$b%s$b is not a valid note access type." },
+    { "CSMSG_BAD_MAX_LENGTH", "$b%s$b is not a valid maximum length (must be between 20 and 450 inclusive)." },
+    { "CSMSG_NOTE_MODIFIED", "Note type $b%s$b modified." },
+    { "CSMSG_NOTE_CREATED", "Note type $b%s$b created." },
+    { "CSMSG_NOTE_TYPE_USED", "Note type $b%s$b is in use; give the FORCE argument to delete it." },
+    { "CSMSG_NOTE_DELETED", "Note type $b%s$b deleted." },
+
+/* Channel [un]suspension */
+    { "CSMSG_ALREADY_SUSPENDED", "$b%s$b is already suspended." },
+    { "CSMSG_NOT_SUSPENDED", "$b%s$b is not suspended." },
+    { "CSMSG_SUSPENDED", "$b$C$b access to $b%s$b has been temporarily suspended." },
+    { "CSMSG_UNSUSPENDED", "$b$C$b access to $b%s$b has been restored." },
+    { "CSMSG_SUSPEND_NODELETE", "$b%s$b is protected from unregistration, and cannot be suspended." },
+    { "CSMSG_USER_SUSPENDED", "$b%s$b's access to $b%s$b has been suspended." },
+    { "CSMSG_USER_UNSUSPENDED", "$b%s$b's access to $b%s$b has been restored." },
+
+/* Access information */
+    { "CSMSG_IS_CHANSERV", "$b$C$b is the $bchannel service bot$b." },
+    { "CSMSG_ACCESS_SELF_ONLY", "You may only see the list of infolines for yourself (by using $b%s$b with no arguments)." },
+    { "CSMSG_SQUAT_ACCESS", "You do not have access to any channels." },
+    { "CSMSG_INFOLINE_LIST", "Showing all channel entries for account $b%s$b:" },
+    { "CSMSG_USER_NO_ACCESS", "%s lacks access to %s." },
+    { "CSMSG_USER_HAS_ACCESS", "%s has access $b%d$b in %s." },
+    { "CSMSG_HELPER_NO_ACCESS", "%s lacks access to %s but has $bsecurity override$b enabled." },
+    { "CSMSG_HELPER_HAS_ACCESS", "%s has access $b%d$b in %s and has $bsecurity override$b enabled." },
+    { "CSMSG_LAZY_SMURF_TARGET", "%s is %s ($bIRCOp$b; not logged in)." },
+    { "CSMSG_SMURF_TARGET", "%s is %s ($b%s$b)." },
+    { "CSMSG_LAME_SMURF_TARGET", "%s is an IRC operator." },
+
+/* Seen information */
+    { "CSMSG_NEVER_SEEN", "%s has never been seen in $b%s$b." },
+    { "CSMSG_USER_SEEN", "%s was last seen in $b%s$b %s ago." },
+    { "CSMSG_USER_VACATION", "%s is currently on vacation." },
+    { "CSMSG_USER_PRESENT", "%s is in the channel $bright now$b." },
+
+/* Names information */
+    { "CSMSG_CHANNEL_NAMES", "Users in $b%s$b:%s" },
+    { "CSMSG_END_NAMES", "End of names in $b%s$b" },
+
+/* Channel information */
+    { "CSMSG_CHANNEL_INFO", "$b%s$b Information:" },
+    { "CSMSG_CHANNEL_TOPIC", "$bDefault Topic:       $b%s" },
+    { "CSMSG_CHANNEL_MODES", "$bMode Lock:           $b%s" },
+    { "CSMSG_CHANNEL_NOTE", "$b%s:%*s$b%s" },
+    { "CSMSG_CHANNEL_MAX", "$bRecord Visitors:     $b%i" },
+    { "CSMSG_CHANNEL_OWNER", "$bOwner:               $b%s" },
+    { "CSMSG_CHANNEL_BANS", "$bBan Count:           $b%i" },
+    { "CSMSG_CHANNEL_USERS", "$bTotal User Count:    $b%i" },
+    { "CSMSG_CHANNEL_REGISTRAR", "$bRegistrar:           $b%s" },
+    { "CSMSG_CHANNEL_SUSPENDED", "$b%s$b is suspended:" },
+    { "CSMSG_CHANNEL_HISTORY", "Suspension history for $b%s$b:" },
+    { "CSMSG_CHANNEL_SUSPENDED_0", " by %s: %s" },
+    { "CSMSG_CHANNEL_SUSPENDED_1", " by %s; expires in %s: %s" },
+    { "CSMSG_CHANNEL_SUSPENDED_2", " by %s; expired %s ago: %s" },
+    { "CSMSG_CHANNEL_SUSPENDED_3", " by %s; revoked %s ago: %s" },
+    { "CSMSG_CHANNEL_SUSPENDED_4", " %s ago by %s: %s" },
+    { "CSMSG_CHANNEL_SUSPENDED_5", " %s ago by %s; expires in %s: %s" },
+    { "CSMSG_CHANNEL_SUSPENDED_6", " %s ago by %s; expired %s ago: %s" },
+    { "CSMSG_CHANNEL_SUSPENDED_7", " %s ago by %s; revoked %s ago: %s" },
+    { "CSMSG_CHANNEL_REGISTERED", "$bRegistered:          $b%s ago." },
+    { "CSMSG_CHANNEL_VISITED", "$bVisited:             $b%s ago." },
+
+    { "CSMSG_PEEK_INFO", "$b%s$b Status:" },
+    { "CSMSG_PEEK_TOPIC", "$bTopic:          $b%s" },
+    { "CSMSG_PEEK_MODES", "$bModes:          $b%s" },
+    { "CSMSG_PEEK_USERS", "$bTotal users:    $b%d" },
+    { "CSMSG_PEEK_OPS", "$bOps:$b" },
+    { "CSMSG_PEEK_NO_OPS", "$bOps:            $bNone present" },
+
+/* Network information */
+    { "CSMSG_NETWORK_INFO", "Network Information:" },
+    { "CSMSG_NETWORK_SERVERS", "$bServers:             $b%i" },
+    { "CSMSG_NETWORK_USERS",   "$bTotal Users:         $b%i" },
+    { "CSMSG_NETWORK_BANS",    "$bTotal Ban Count:     $b%i" },
+    { "CSMSG_NETWORK_OPERS",   "$bIRC Operators:       $b%i" },
+    { "CSMSG_NETWORK_CHANNELS","$bRegistered Channels: $b%i" },
+    { "CSMSG_SERVICES_UPTIME", "$bServices Uptime:     $b%s" },
+    { "CSMSG_BURST_LENGTH",    "$bLast Burst Length:   $b%s" },
+
+/* Staff list */
+    { "CSMSG_NETWORK_STAFF", "$bOnline Network Staff:$b" },
+    { "CSMSG_STAFF_OPERS", "$bIRC Operators:$b" },
+    { "CSMSG_STAFF_HELPERS", "$bHelpers:$b" },
+
+/* Channel searches */
+    { "CSMSG_ACTION_INVALID", "$b%s$b is not a recognized search action." },
+    { "CSMSG_UNVISITED_HEADER", "Showing a maximum of %d channels unvisited for $b%s$b:" },
+    { "CSMSG_UNVISITED_DATA", "%s: $b%s$b" },
+    { "CSMSG_CHANNEL_SEARCH_RESULTS", "The following channels were found:" },
+
+/* Channel configuration */
+    { "CSMSG_INVALID_OPTION", "$b%s$b is not a valid %s option." },
+    { "CSMSG_CHANNEL_OPTIONS", "Channel Options:" },
+    { "CSMSG_GREETING_TOO_LONG", "Your greeting ($b%d$b characters) must be shorter than $b%d$b characters." },
+
+/* User settings */
+    { "CSMSG_USER_OPTIONS", "User Options:" },
+    { "CSMSG_USER_PROTECTED", "That user is protected." },
+
+/* Toys */
+    { "CSMSG_UNF_RESPONSE", "I don't want to be part of your sick fantasies!" },
+    { "CSMSG_PING_RESPONSE", "Pong!" },
+    { "CSMSG_WUT_RESPONSE", "wut" },
+    { "CSMSG_BAD_NUMBER", "$b%s$b is an invalid number.  Please use a number greater than 1 with this command." },
+    { "CSMSG_BAD_DIE_FORMAT", "I do not understand $b%s$b.  Please use either a single number or standard 4d6+3 format." },
+    { "CSMSG_BAD_DICE_COUNT", "%d is too many dice.  Please use at most %d." },
+    { "CSMSG_DICE_ROLL", "The total is $b%d$b from rolling %dd%d+%d." },
+    { "CSMSG_DIE_ROLL", "A $b%d$b shows on the %d-sided die." },
+    { "CSMSG_HUGGLES_HIM", "\001ACTION huggles %s\001" },
+    { "CSMSG_HUGGLES_YOU", "\001ACTION huggles you\001" },
+
+/* Other things */
+    { "CSMSG_EVENT_SEARCH_RESULTS", "The following channel events were found:" },
+    { NULL, NULL }
+};
+
+/* eject_user and unban_user flags */
+#define ACTION_KICK            0x0001
+#define ACTION_BAN             0x0002
+#define ACTION_ADD_BAN         0x0004
+#define ACTION_ADD_TIMED_BAN           0x0008
+#define ACTION_UNBAN           0x0010
+#define ACTION_DEL_BAN         0x0020
+
+/* The 40 allows for [+-ntlksimprD] and lots of fudge factor. */
+#define MODELEN                        40 + KEYLEN
+#define PADLEN                 21
+#define ACCESSLEN              10
+
+#define CSFUNC_ARGS            user, channel, argc, argv, cmd
+
+#define CHANSERV_FUNC(NAME) MODCMD_FUNC(NAME)
+#define CHANSERV_SYNTAX()      svccmd_send_help(user, chanserv, cmd)
+#define REQUIRE_PARAMS(N)      if(argc < (N)) {            \
+       reply("MSG_MISSING_PARAMS", argv[0]); \
+       CHANSERV_SYNTAX(); \
+       return 0; }
+
+DECLARE_LIST(dnrList, struct do_not_register *);
+DEFINE_LIST(dnrList, struct do_not_register *);
+
+static int eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, char *argv[], struct svccmd *cmd, int action);
+
+struct userNode *chanserv;
+dict_t note_types;
+static dict_t plain_dnrs, mask_dnrs, handle_dnrs;
+static struct log_type *CS_LOG;
+
+static struct
+{
+    struct channelList  support_channels;
+    struct mod_chanmode default_modes;
+
+    unsigned long      db_backup_frequency;
+    unsigned long      channel_expire_frequency;
+
+    long               info_delay;
+    unsigned int       adjust_delay;
+    long               channel_expire_delay;
+    unsigned int        nodelete_level;
+
+    unsigned int       adjust_threshold;
+    int                        join_flood_threshold;
+
+    unsigned int       greeting_length;
+    unsigned int        refresh_period;
+
+    unsigned int        max_owned;
+    unsigned int       max_chan_users;
+    unsigned int       max_chan_bans;
+
+    struct string_list  *set_shows;
+    struct string_list  *eightball;
+    struct string_list  *old_ban_names;
+
+    const char          *ctcp_short_ban_duration;
+    const char          *ctcp_long_ban_duration;
+
+    const char          *irc_operator_epithet;
+    const char          *network_helper_epithet;
+    const char          *support_helper_epithet;
+} chanserv_conf;
+
+struct listData
+{
+    struct userNode *user;
+    struct userNode *bot;
+    struct chanNode *channel;
+    const char      *search;
+    unsigned short  lowest;
+    unsigned short  highest;
+    struct userData **users;
+    struct helpfile_table table;
+};
+
+enum note_access_type
+{
+    NOTE_SET_CHANNEL_ACCESS,
+    NOTE_SET_CHANNEL_SETTER,
+    NOTE_SET_PRIVILEGED
+};
+
+enum note_visible_type
+{
+    NOTE_VIS_ALL,
+    NOTE_VIS_CHANNEL_USERS,
+    NOTE_VIS_PRIVILEGED
+};
+
+struct note_type
+{
+    enum note_access_type set_access_type;
+    union {
+        unsigned int     min_opserv;
+        unsigned short   min_ulevel;
+    } set_access;
+    enum note_visible_type visible_type;
+    unsigned int         max_length;
+    unsigned int         refs;
+    char                 name[1];
+};
+
+struct note
+{
+    struct note_type     *type;
+    char                 setter[NICKSERV_HANDLE_LEN+1];
+    char                 note[1];
+};
+
+static unsigned int registered_channels;
+static unsigned int banCount;
+
+static const struct {
+    char *name;
+    char *title;
+    unsigned short level;
+    char ch;
+} accessLevels[] = {
+    { "peon", "Peon", UL_PEON, '+' },
+    { "op", "Op", UL_OP, '@' },
+    { "master", "Master", UL_MASTER, '%' },
+    { "coowner", "Coowner", UL_COOWNER, '*' },
+    { "owner", "Owner", UL_OWNER, '!' },
+    { "helper", "BUG:", UL_HELPER, 'X' }
+};
+
+static const struct {
+    char *format_name;
+    char *db_name;
+    unsigned short default_value;
+    unsigned int old_idx;
+} levelOptions[] = {
+    { "CSMSG_SET_GIVE_OPS", "giveops", 200, 2 },
+    { "CSMSG_SET_ENFOPS", "enfops", 300, 1 },
+    { "CSMSG_SET_ENFMODES", "enfmodes", 200, 3 },
+    { "CSMSG_SET_ENFTOPIC", "enftopic", 200, 4 },
+    { "CSMSG_SET_PUBCMD", "pubcmd", 0, 5 },
+    { "CSMSG_SET_SETTERS", "setters", 400, 7 },
+    { "CSMSG_SET_CTCPUSERS", "ctcpusers", 0, 9 }
+};
+
+struct charOptionValues {
+    char value;
+    char *format_name;
+} protectValues[] = {
+    { 'a', "CSMSG_PROTECT_ALL" },
+    { 'e', "CSMSG_PROTECT_EQUAL" },
+    { 'l', "CSMSG_PROTECT_LOWER" },
+    { 'n', "CSMSG_PROTECT_NONE" }
+}, toysValues[] = {
+    { 'd', "CSMSG_TOYS_DISABLED" },
+    { 'n', "CSMSG_TOYS_PRIVATE" },
+    { 'p', "CSMSG_TOYS_PUBLIC" }
+}, topicRefreshValues[] = {
+    { 'n', "CSMSG_TOPICREFRESH_NEVER" },
+    { '1', "CSMSG_TOPICREFRESH_3_HOURS" },
+    { '2', "CSMSG_TOPICREFRESH_6_HOURS" },
+    { '3', "CSMSG_TOPICREFRESH_12_HOURS" },
+    { '4', "CSMSG_TOPICREFRESH_24_HOURS" }
+}, ctcpReactionValues[] = {
+    { 'k', "CSMSG_CTCPREACTION_KICK" },
+    { 'b', "CSMSG_CTCPREACTION_KICKBAN" },
+    { 't', "CSMSG_CTCPREACTION_SHORTBAN" },
+    { 'T', "CSMSG_CTCPREACTION_LONGBAN" }
+};
+
+static const struct {
+    char *format_name;
+    char *db_name;
+    char default_value;
+    unsigned int old_idx;
+    unsigned char count;
+    struct charOptionValues *values;
+} charOptions[] = {
+    { "CSMSG_SET_PROTECT", "protect", 'l', 0, ArrayLength(protectValues), protectValues },
+    { "CSMSG_SET_TOYS", "toys", 'p', 6, ArrayLength(toysValues), toysValues },
+    { "CSMSG_SET_TOPICREFRESH", "topicrefresh", 'n', 8, ArrayLength(topicRefreshValues), topicRefreshValues },
+    { "CSMSG_SET_CTCPREACTION", "ctcpreaction", 't', 10, ArrayLength(ctcpReactionValues), ctcpReactionValues }
+};
+
+struct userData *helperList;
+struct chanData *channelList;
+static struct module *chanserv_module;
+static unsigned int userCount;
+
+#define GetChannelUser(channel, handle) _GetChannelUser(channel, handle, 1, 0)
+#define GetChannelAccess(channel, handle) _GetChannelUser(channel, handle, 0, 0)
+#define GetTrueChannelAccess(channel, handle) _GetChannelUser(channel, handle, 0, 1)
+
+unsigned short
+user_level_from_name(const char *name, unsigned short clamp_level)
+{
+    unsigned int level = 0, ii;
+    if(isdigit(name[0]))
+        level = atoi(name);
+    else for(ii = 0; (ii < ArrayLength(accessLevels)) && !level; ++ii)
+        if(!irccasecmp(name, accessLevels[ii].name))
+            level = accessLevels[ii].level;
+    if(level > clamp_level)
+        return 0;
+    return level;
+}
+
+int
+parse_level_range(unsigned short *minl, unsigned short *maxl, const char *arg)
+{
+    char *sep;
+    *minl = strtoul(arg, &sep, 10);
+    if(*sep == '\0')
+    {
+        *maxl = *minl;
+        return 1;
+    }
+    else if(*sep == '-')
+    {
+        *maxl = strtoul(sep+1, &sep, 10);
+        return *sep == '\0';
+    }
+    else
+        return 0;
+}
+
+struct userData*
+_GetChannelUser(struct chanData *channel, struct handle_info *handle, int override, int allow_suspended)
+{
+    struct userData *uData, **head;
+
+    if(!channel || !handle)
+        return NULL;
+
+    if(override && HANDLE_FLAGGED(handle, HELPING)
+       && ((handle->opserv_level >= chanserv_conf.nodelete_level) || !IsProtected(channel)))
+    {
+       for(uData = helperList;
+           uData && uData->handle != handle;
+           uData = uData->next);
+
+       if(!uData)
+       {
+           uData = calloc(1, sizeof(struct userData));
+           uData->handle = handle;
+
+           uData->access = UL_HELPER;
+           uData->seen = 0;
+
+           uData->info = NULL;
+
+           uData->prev = NULL;
+           uData->next = helperList;
+           if(helperList)
+               helperList->prev = uData;
+           helperList = uData;
+       }
+
+       head = &helperList;
+    }
+    else
+    {
+       for(uData = channel->users; uData; uData = uData->next)
+            if((uData->handle == handle) && (allow_suspended || !IsUserSuspended(uData)))
+                break;
+
+       head = &(channel->users);
+    }
+
+    if(uData && (uData != *head))
+    {
+       /* Shuffle the user to the head of whatever list he was in. */
+       if(uData->next)
+            uData->next->prev = uData->prev;
+       if(uData->prev)
+            uData->prev->next = uData->next;
+
+       uData->prev = NULL;
+       uData->next = *head;
+
+       if(*head)
+           (**head).prev = uData;
+       *head = uData;
+    }
+
+    return uData;
+}
+
+/* Returns non-zero if user has at least the minimum access.
+ * exempt_owner is set when handling !set, so the owner can set things
+ * to/from >500.
+ */
+int check_user_level(struct chanNode *channel, struct userNode *user, enum levelOption opt, int allow_override, int exempt_owner)
+{
+    struct userData *uData;
+    struct chanData *cData = channel->channel_info;
+    unsigned short minimum = cData->lvlOpts[opt];
+    if(!minimum)
+        return 1;
+    uData = _GetChannelUser(cData, user->handle_info, allow_override, 0);
+    if(!uData)
+        return 0;
+    if(minimum <= uData->access)
+        return 1;
+    if((minimum > UL_OWNER) && (uData->access == UL_OWNER) && exempt_owner)
+        return 1;
+    return 0;
+}
+
+/* Scan for other users authenticated to the same handle
+   still in the channel. If so, keep them listed as present.
+
+   user is optional, if not null, it skips checking that userNode
+   (for the handle_part function) */
+static void
+scan_handle_presence(struct chanNode *channel, struct handle_info *handle, struct userNode *user)
+{
+    struct userData *uData;
+
+    if(!channel->channel_info || IsSuspended(channel->channel_info))
+        return;
+
+    uData = GetTrueChannelAccess(channel->channel_info, handle);
+    if(uData)
+    {
+        struct modeNode *mn = find_handle_in_channel(channel, handle, user);
+
+       if(mn)
+       {
+           uData->present = 1;
+           uData->seen = now;
+       }
+       else
+           uData->present = 0;
+    }
+}
+
+static void
+chanserv_ctcp_check(struct userNode *user, struct chanNode *channel, char *text, UNUSED_ARG(struct userNode *bot))
+{
+    unsigned int eflags, argc;
+    char *argv[4];
+    static char *bad_ctcp_reason = "CTCPs to this channel are forbidden.";
+
+    /* Bail early if channel is inactive or doesn't restrict CTCPs, or sender is a service */
+    if(!channel->channel_info
+       || IsSuspended(channel->channel_info)
+       || IsService(user)
+       || !ircncasecmp(text, "ACTION ", 7))
+        return;
+    /* Figure out the minimum level needed to CTCP the channel */
+    if(check_user_level(channel, user, lvlCTCPUsers, 1, 0))
+        return;
+    /* We need to enforce against them; do so. */
+    eflags = 0;
+    argv[0] = text;
+    argv[1] = user->nick;
+    argc = 2;
+    if(GetUserMode(channel, user))
+        eflags |= ACTION_KICK;
+    switch(channel->channel_info->chOpts[chCTCPReaction]) {
+    default: case 'k': /* just do the kick */ break;
+    case 'b':
+        eflags |= ACTION_BAN;
+        break;
+    case 't':
+        eflags |= ACTION_BAN | ACTION_ADD_BAN | ACTION_ADD_TIMED_BAN;
+        argv[argc++] = (char*)chanserv_conf.ctcp_short_ban_duration;
+        break;
+    case 'T':
+        eflags |= ACTION_BAN | ACTION_ADD_BAN | ACTION_ADD_TIMED_BAN;
+        argv[argc++] = (char*)chanserv_conf.ctcp_long_ban_duration;
+        break;
+    }
+    argv[argc++] = bad_ctcp_reason;
+    eject_user(chanserv, channel, argc, argv, NULL, eflags);
+}
+
+struct note_type *
+chanserv_create_note_type(const char *name)
+{
+    struct note_type *ntype = calloc(1, sizeof(*ntype) + strlen(name));
+    strcpy(ntype->name, name);
+    ntype->refs = 1;
+    dict_insert(note_types, ntype->name, ntype);
+    return ntype;
+}
+
+static void
+chanserv_deref_note_type(void *data)
+{
+    struct note_type *ntype = data;
+
+    if(--ntype->refs > 0)
+       return;
+    free(ntype);
+}
+
+static void
+chanserv_flush_note_type(struct note_type *ntype)
+{
+    struct chanData *cData;
+    for(cData = channelList; cData; cData = cData->next)
+        dict_remove(cData->notes, ntype->name);
+}
+
+static void
+chanserv_truncate_notes(struct note_type *ntype)
+{
+    struct chanData *cData;
+    struct note *note;
+    unsigned int size = sizeof(*note) + ntype->max_length;
+    
+    for(cData = channelList; cData; cData = cData->next) {
+        note = dict_find(cData->notes, ntype->name, NULL);
+        if(!note)
+            continue;
+       if(strlen(note->note) <= ntype->max_length)
+            continue;
+        dict_remove2(cData->notes, ntype->name, 1);
+        note = realloc(note, size);
+        note->note[ntype->max_length] = 0;
+        dict_insert(cData->notes, ntype->name, note);
+    }
+}
+
+static int note_type_visible_to_user(struct chanData *channel, struct note_type *ntype, struct userNode *user);
+
+static struct note *
+chanserv_add_channel_note(struct chanData *channel, struct note_type *type, const char *setter, const char *text)
+{
+    struct note *note;
+    unsigned int len = strlen(text);
+
+    if(len > type->max_length) len = type->max_length;
+    note = calloc(1, sizeof(*note) + len);
+    note->type = type;
+    strncpy(note->setter, setter, sizeof(note->setter)-1);
+    memcpy(note->note, text, len);
+    note->note[len] = 0;
+    dict_insert(channel->notes, type->name, note);
+    type->refs++;
+    return note;
+}
+
+static void
+chanserv_free_note(void *data)
+{
+    struct note *note = data;
+
+    chanserv_deref_note_type(note->type);
+    assert(note->type->refs > 0); /* must use delnote to remove the type */
+    free(note);
+}
+
+static MODCMD_FUNC(cmd_createnote) {
+    struct note_type *ntype;
+    unsigned int arg = 1, existed = 0, max_length;
+
+    if((ntype = dict_find(note_types, argv[1], NULL)))
+        existed = 1;
+    else
+       ntype = chanserv_create_note_type(argv[arg]);
+    if(!irccasecmp(argv[++arg], "privileged"))
+    {
+        arg++;
+        ntype->set_access_type = NOTE_SET_PRIVILEGED;
+        ntype->set_access.min_opserv = strtoul(argv[arg], NULL, 0);
+    }
+    else if(!irccasecmp(argv[arg], "channel"))
+    {
+        unsigned short ulvl = user_level_from_name(argv[++arg], UL_OWNER);
+        if(!ulvl)
+        {
+            reply("CSMSG_INVALID_ACCESS", argv[arg]);
+           goto fail;
+        }
+        ntype->set_access_type = NOTE_SET_CHANNEL_ACCESS;
+        ntype->set_access.min_ulevel = ulvl;
+    }
+    else if(!irccasecmp(argv[arg], "setter"))
+    {
+        ntype->set_access_type = NOTE_SET_CHANNEL_SETTER;
+    }
+    else
+    {
+        reply("CSMSG_BAD_NOTE_ACCESS", argv[arg]);
+       goto fail;
+    }
+
+    if(!irccasecmp(argv[++arg], "privileged"))
+        ntype->visible_type = NOTE_VIS_PRIVILEGED;
+    else if(!irccasecmp(argv[arg], "channel_users"))
+        ntype->visible_type = NOTE_VIS_CHANNEL_USERS;
+    else if(!irccasecmp(argv[arg], "all"))
+        ntype->visible_type = NOTE_VIS_ALL;
+    else {
+        reply("CSMSG_BAD_NOTE_ACCESS", argv[arg]);
+       goto fail;
+    }
+
+    if((arg+1) >= argc) {
+        reply("MSG_MISSING_PARAMS", argv[0]);
+       goto fail;
+    }
+    max_length = strtoul(argv[++arg], NULL, 0);
+    if(max_length < 20 || max_length > 450)
+    {
+        reply("CSMSG_BAD_MAX_LENGTH", argv[arg]);
+       goto fail;
+    }
+    if(existed && (max_length < ntype->max_length))
+    {
+       ntype->max_length = max_length;
+       chanserv_truncate_notes(ntype);
+    }
+    ntype->max_length = max_length;
+
+    if(existed)
+        reply("CSMSG_NOTE_MODIFIED", ntype->name);    
+    else
+        reply("CSMSG_NOTE_CREATED", ntype->name);
+    return 1;
+
+fail:
+    if(!existed)
+       dict_remove(note_types, ntype->name);
+    return 0;
+}
+
+static MODCMD_FUNC(cmd_removenote) {
+    struct note_type *ntype;
+    int force;
+
+    ntype = dict_find(note_types, argv[1], NULL);
+    force = (argc > 2) && !irccasecmp(argv[2], "force");
+    if(!ntype)
+    {
+        reply("CSMSG_BAD_NOTE_TYPE", argv[1]);
+        return 0;
+    }
+    if(ntype->refs > 1)
+    {
+        if(!force)
+        {
+            reply("CSMSG_NOTE_TYPE_USED", ntype->name);
+            return 0;
+        }
+        chanserv_flush_note_type(ntype);
+    }
+    dict_remove(note_types, argv[1]);
+    reply("CSMSG_NOTE_DELETED", argv[1]);
+    return 1;
+}
+
+static int
+mode_lock_violated(const struct mod_chanmode *orig, const struct mod_chanmode *change)
+{
+    if(!orig)
+        return 0;
+    if(orig->modes_set & change->modes_clear)
+        return 1;
+    if(orig->modes_clear & change->modes_set)
+        return 1;
+    if((orig->modes_set & MODE_KEY)
+       && strcmp(orig->new_key, change->new_key))
+        return 1;
+    if((orig->modes_set & MODE_LIMIT)
+       && (orig->new_limit != change->new_limit))
+        return 1;
+    return 0;
+}
+
+static char max_length_text[MAXLEN+1][16];
+
+static struct helpfile_expansion
+chanserv_expand_variable(const char *variable)
+{
+    struct helpfile_expansion exp;
+
+    if(!irccasecmp(variable, "notes"))
+    {
+        dict_iterator_t it;
+        exp.type = HF_TABLE;
+        exp.value.table.length = 1;
+        exp.value.table.width = 3;
+        exp.value.table.flags = 0;
+        exp.value.table.contents = calloc(dict_size(note_types)+1, sizeof(char**));
+        exp.value.table.contents[0] = calloc(exp.value.table.width, sizeof(char*));
+        exp.value.table.contents[0][0] = "Note Type";
+        exp.value.table.contents[0][1] = "Visibility";
+        exp.value.table.contents[0][2] = "Max Length";
+        for(it=dict_first(note_types); it; it=iter_next(it))
+        {
+            struct note_type *ntype = iter_data(it);
+            int row;
+
+            if(!note_type_visible_to_user(NULL, ntype, message_dest)) continue;
+            row = exp.value.table.length++;
+            exp.value.table.contents[row] = calloc(exp.value.table.width, sizeof(char*));
+            exp.value.table.contents[row][0] = ntype->name;
+            exp.value.table.contents[row][1] = (ntype->visible_type == NOTE_VIS_ALL) ? "all" :
+                (ntype->visible_type == NOTE_VIS_CHANNEL_USERS) ? "chan users" :
+                "unknown";
+            if(!max_length_text[ntype->max_length][0])
+                snprintf(max_length_text[ntype->max_length], sizeof(max_length_text[ntype->max_length]), "%u", ntype->max_length);
+            exp.value.table.contents[row][2] = max_length_text[ntype->max_length];
+        }
+        return exp;
+    }
+
+    exp.type = HF_STRING;
+    exp.value.str = NULL;
+    return exp;
+}
+
+static struct chanData*
+register_channel(struct chanNode *cNode, char *registrar)
+{
+    struct chanData *channel;
+    enum levelOption lvlOpt;
+    enum charOption chOpt;
+
+    channel = calloc(1, sizeof(struct chanData));
+
+    channel->notes = dict_new();
+    dict_set_free_data(channel->notes, chanserv_free_note);
+
+    channel->registrar = strdup(registrar);
+    channel->registered = now;
+    channel->visited = now;
+    channel->limitAdjusted = now;
+    channel->flags = CHANNEL_DEFAULT_FLAGS;
+    for(lvlOpt = 0; lvlOpt < NUM_LEVEL_OPTIONS; ++lvlOpt)
+        channel->lvlOpts[lvlOpt] = levelOptions[lvlOpt].default_value;
+    for(chOpt = 0; chOpt < NUM_CHAR_OPTIONS; ++chOpt)
+        channel->chOpts[chOpt] = charOptions[chOpt].default_value;
+
+    channel->prev = NULL;
+    channel->next = channelList;
+
+    if(channelList)
+       channelList->prev = channel;
+    channelList = channel;
+    registered_channels++;
+
+    channel->channel = cNode;
+    LockChannel(cNode);
+    cNode->channel_info = channel;
+
+    return channel;
+}
+
+static struct userData*
+add_channel_user(struct chanData *channel, struct handle_info *handle, unsigned short access, time_t seen, const char *info)
+{
+    struct userData *ud;
+
+    if(access > UL_OWNER)
+       return NULL;
+
+    ud = calloc(1, sizeof(*ud));
+    ud->channel = channel;
+    ud->handle = handle;
+    ud->seen = seen;
+    ud->access = access;
+    ud->info = info ? strdup(info) : NULL;
+
+    ud->prev = NULL;
+    ud->next = channel->users;
+    if(channel->users)
+       channel->users->prev = ud;
+    channel->users = ud;
+
+    channel->userCount++;
+    userCount++;
+
+    ud->u_prev = NULL;
+    ud->u_next = ud->handle->channels;
+    if(ud->u_next)
+        ud->u_next->u_prev = ud;
+    ud->handle->channels = ud;
+
+    return ud;
+}
+
+static void unregister_channel(struct chanData *channel, const char *reason);
+
+void
+del_channel_user(struct userData *user, int do_gc)
+{
+    struct chanData *channel = user->channel;
+
+    channel->userCount--;
+    userCount--;
+
+    if(user->prev)
+        user->prev->next = user->next;
+    else
+        channel->users = user->next;
+    if(user->next)
+        user->next->prev = user->prev;
+
+    if(user->u_prev)
+        user->u_prev->u_next = user->u_next;
+    else
+        user->handle->channels = user->u_next;
+    if(user->u_next)
+        user->u_next->u_prev = user->u_prev;
+
+    free(user->info);
+    free(user);
+    if(do_gc && !channel->users && !IsProtected(channel))
+        unregister_channel(channel, "lost all users.");
+}
+
+static void expire_ban(void *data);
+
+static struct banData*
+add_channel_ban(struct chanData *channel, const char *mask, char *owner, time_t set, time_t triggered, time_t expires, char *reason)
+{
+    struct banData *bd;
+    unsigned int ii, l1, l2;
+
+    if(!mask)
+        return NULL;
+
+    bd = malloc(sizeof(struct banData));
+
+    bd->channel = channel;
+    bd->set = set;
+    bd->triggered = triggered;
+    bd->expires = expires;
+
+    for(ii = 0; ii < chanserv_conf.old_ban_names->used; ++ii)
+    {
+        extern const char *hidden_host_suffix;
+        const char *old_name = chanserv_conf.old_ban_names->list[ii];
+        char *new_mask;
+
+        l1 = strlen(mask);
+        l2 = strlen(old_name);
+        if(l2+2 > l1)
+            continue;
+        if(irccasecmp(mask + l1 - l2, old_name))
+            continue;
+        new_mask = alloca(MAXLEN);
+        sprintf(new_mask, "%.*s%s", l1-l2, mask, hidden_host_suffix);
+        mask = new_mask;
+    }
+    safestrncpy(bd->mask, mask, sizeof(bd->mask));
+    if(owner)
+        safestrncpy(bd->owner, owner, sizeof(bd->owner));
+    bd->reason = reason ? strdup(reason) : NULL;
+
+    if(expires)
+       timeq_add(expires, expire_ban, bd);
+
+    bd->prev = NULL;
+    bd->next = channel->bans;
+    if(channel->bans)
+       channel->bans->prev = bd;
+    channel->bans = bd;
+    channel->banCount++;
+    banCount++;
+
+    return bd;
+}
+
+static void
+del_channel_ban(struct banData *ban)
+{
+    ban->channel->banCount--;
+    banCount--;
+
+    if(ban->prev)
+        ban->prev->next = ban->next;
+    else
+        ban->channel->bans = ban->next;
+
+    if(ban->next)
+        ban->next->prev = ban->prev;
+
+    if(ban->expires)
+       timeq_del(0, expire_ban, ban, TIMEQ_IGNORE_WHEN);
+
+    if(ban->reason)
+        free(ban->reason);
+
+    free(ban);
+}
+
+static void
+expire_ban(void *data)
+{
+    struct banData *bd = data;
+    if(!IsSuspended(bd->channel))
+    {
+        struct banList bans;
+        struct mod_chanmode change;
+        unsigned int ii;
+        bans = bd->channel->channel->banlist;
+        change.modes_set = change.modes_clear = 0;
+        change.argc = 0;
+        for(ii=0; ii<bans.used; ii++)
+        {
+            if(!strcmp(bans.list[ii]->ban, bd->mask))
+           {
+                change.argc = 1;
+                change.args[0].mode = MODE_REMOVE|MODE_BAN;
+                change.args[0].hostmask = bd->mask;
+                mod_chanmode_announce(chanserv, bd->channel->channel, &change);
+                break;
+            }
+        }
+    }
+    bd->expires = 0;
+    del_channel_ban(bd);
+}
+
+static void chanserv_expire_suspension(void *data);
+
+static void
+unregister_channel(struct chanData *channel, const char *reason)
+{
+    char msgbuf[MAXLEN];
+
+    /* After channel unregistration, the following must be cleaned
+       up:
+       - Channel information.
+       - Channel users.
+       - Channel bans.
+       - Channel suspension data.
+       - Timeq entries. (Except timed bans, which are handled elsewhere.)
+    */
+
+    if(!channel)
+       return;
+
+    timeq_del(0, NULL, channel, TIMEQ_IGNORE_FUNC | TIMEQ_IGNORE_WHEN);
+
+    while(channel->users)
+       del_channel_user(channel->users, 0);
+
+    while(channel->bans)
+       del_channel_ban(channel->bans);
+
+    if(channel->topic) free(channel->topic);
+    if(channel->registrar) free(channel->registrar);
+    if(channel->greeting) free(channel->greeting);
+    if(channel->user_greeting) free(channel->user_greeting);
+    if(channel->topic_mask) free(channel->topic_mask);
+
+    if(channel->prev) channel->prev->next = channel->next;
+    else channelList = channel->next;
+
+    if(channel->next) channel->next->prev = channel->prev;
+
+    if(channel->suspended)
+    {
+       struct chanNode *cNode = channel->channel;
+       struct suspended *suspended, *next_suspended;
+
+        for(suspended = channel->suspended; suspended; suspended = next_suspended)
+        {
+            next_suspended = suspended->previous;
+            free(suspended->suspender);
+            free(suspended->reason);
+            if(suspended->expires)
+                timeq_del(suspended->expires, chanserv_expire_suspension, suspended, 0);
+            free(suspended);
+        }
+
+       if(cNode)
+           cNode->channel_info = NULL;
+    }
+    channel->channel->channel_info = NULL;
+
+    if(channel->notes)
+        dict_delete(channel->notes);
+    sprintf(msgbuf, "%s %s", channel->channel->name, reason);
+    if(!IsSuspended(channel))
+        DelChannelUser(chanserv, channel->channel, msgbuf, 0);
+    global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, msgbuf);
+    UnlockChannel(channel->channel);
+    free(channel);
+    registered_channels--;
+}
+
+static void
+expire_channels(UNUSED_ARG(void *data))
+{
+    struct chanData *channel, *next;
+    struct userData *user;
+    char delay[INTERVALLEN], reason[INTERVALLEN + 64];
+
+    intervalString(delay, chanserv_conf.channel_expire_delay);
+    sprintf(reason, "Channel registration automatically expired after %s of disuse.", delay);
+
+    for(channel = channelList; channel; channel = next)
+    {
+       next = channel->next;
+
+        /* See if the channel can be expired. */
+        if(((now - channel->visited) <= chanserv_conf.channel_expire_delay)
+           || IsProtected(channel))
+            continue;
+
+        /* Make sure there are no high-ranking users still in the channel. */
+        for(user=channel->users; user; user=user->next)
+            if(user->present && (user->access >= UL_PRESENT))
+                break;
+        if(user)
+            continue;
+
+        /* Unregister the channel */
+        log_module(CS_LOG, LOG_INFO, "(%s) Channel registration expired.", channel->channel->name);
+        unregister_channel(channel, "registration expired.");
+    }
+
+    if(chanserv_conf.channel_expire_frequency)
+       timeq_add(now + chanserv_conf.channel_expire_frequency, expire_channels, NULL);
+}
+
+static int
+protect_user(const struct userNode *victim, const struct userNode *aggressor, struct chanData *channel)
+{
+    char protect = channel->chOpts[chProtect];
+    struct userData *cs_victim, *cs_aggressor;
+
+    /* Don't protect if no one is to be protected, someone is attacking
+       himself, or if the aggressor is an IRC Operator. */
+    if(protect == 'n' || victim == aggressor || IsOper(aggressor))
+       return 0;
+
+    /* Don't protect if the victim isn't authenticated (because they
+       can't be a channel user), unless we are to protect non-users
+       also. */
+    cs_victim = GetChannelAccess(channel, victim->handle_info);
+    if(protect != 'a' && !cs_victim)
+        return 0;
+
+    /* Protect if the aggressor isn't a user because at this point,
+       the aggressor can only be less than or equal to the victim. */
+    cs_aggressor = GetChannelAccess(channel, aggressor->handle_info);
+    if(!cs_aggressor)
+        return 1;
+
+    /* If the aggressor was a user, then the victim can't be helped. */
+    if(!cs_victim)
+        return 0;
+
+    switch(protect)
+    {
+    case 'l':
+       if(cs_victim->access > cs_aggressor->access)
+            return 1;
+       break;
+    case 'a':
+    case 'e':
+       if(cs_victim->access >= cs_aggressor->access)
+            return 1;
+       break;
+    }
+
+    return 0;
+}
+
+static int
+validate_op(struct userNode *user, struct chanNode *channel, struct userNode *victim)
+{
+    struct chanData *cData = channel->channel_info;
+    struct userData *cs_victim;
+
+    if((!(cs_victim = GetChannelUser(cData, victim->handle_info))
+        || (cs_victim->access < cData->lvlOpts[lvlGiveOps]))
+       && !check_user_level(channel, user, lvlEnfOps, 0, 0))
+    {
+       send_message(user, chanserv, "CSMSG_OPBY_LOCKED");
+       return 0;
+    }
+
+    return 1;
+}
+
+static int
+validate_deop(struct userNode *user, struct chanNode *channel, struct userNode *victim)
+{
+    if(IsService(victim))
+    {
+       send_message(user, chanserv, "MSG_SERVICE_IMMUNE", victim->nick);
+       return 0;
+    }
+
+    if(protect_user(victim, user, channel->channel_info))
+    {
+       send_message(user, chanserv, "CSMSG_USER_PROTECTED", victim->nick);
+       return 0;
+    }
+
+    return 1;
+}
+
+static struct do_not_register *
+chanserv_add_dnr(const char *chan_name, const char *setter, const char *reason)
+{
+    struct do_not_register *dnr = calloc(1, sizeof(*dnr)+strlen(reason));
+    safestrncpy(dnr->chan_name, chan_name, sizeof(dnr->chan_name));
+    safestrncpy(dnr->setter, setter, sizeof(dnr->setter));
+    strcpy(dnr->reason, reason);
+    dnr->set = now;
+    if(dnr->chan_name[0] == '*')
+        dict_insert(handle_dnrs, dnr->chan_name+1, dnr);
+    else if(strpbrk(dnr->chan_name, "*?"))
+        dict_insert(mask_dnrs, dnr->chan_name, dnr);
+    else
+        dict_insert(plain_dnrs, dnr->chan_name, dnr);
+    return dnr;
+}
+
+static struct dnrList
+chanserv_find_dnrs(const char *chan_name, struct handle_info *handle)
+{
+    struct dnrList list;
+    dict_iterator_t it;
+    struct do_not_register *dnr;
+
+    dnrList_init(&list);
+    if(handle && (dnr = dict_find(handle_dnrs, handle->handle, NULL)))
+        dnrList_append(&list, dnr);
+    if(chan_name && (dnr = dict_find(plain_dnrs, chan_name, NULL)))
+        dnrList_append(&list, dnr);
+    if(chan_name)
+        for(it = dict_first(mask_dnrs); it; it = iter_next(it))
+            if(match_ircglob(chan_name, iter_key(it)))
+                dnrList_append(&list, iter_data(it));
+    return list;
+}
+
+static unsigned int
+chanserv_show_dnrs(struct userNode *user, struct svccmd *cmd, const char *chan_name, struct handle_info *handle)
+{
+    struct dnrList list;
+    struct do_not_register *dnr;
+    unsigned int ii;
+    char buf[INTERVALLEN];
+
+    list = chanserv_find_dnrs(chan_name, handle);
+    for(ii = 0; (ii < list.used) && (ii < 10); ++ii)
+    {
+        dnr = list.list[ii];
+        if(dnr->set)
+        {
+            strftime(buf, sizeof(buf), "%Y %b %d", localtime(&dnr->set));
+            reply("CSMSG_DNR_INFO_SET", dnr->chan_name, buf, dnr->setter, dnr->reason);
+        }
+        else
+            reply("CSMSG_DNR_INFO", dnr->chan_name, dnr->setter, dnr->reason);
+    }
+    if(ii < list.used)
+        reply("CSMSG_MORE_DNRS", list.used - ii);
+    free(list.list);
+    return ii;
+}
+
+struct do_not_register *
+chanserv_is_dnr(const char *chan_name, struct handle_info *handle)
+{
+    struct do_not_register *dnr;
+    dict_iterator_t it;
+
+    if(handle && (dnr = dict_find(handle_dnrs, handle->handle, NULL)))
+        return dnr;
+    if(chan_name)
+    {
+        if((dnr = dict_find(plain_dnrs, chan_name, NULL)))
+            return dnr;
+        for(it = dict_first(mask_dnrs); it; it = iter_next(it))
+            if(match_ircglob(chan_name, iter_key(it)))
+                return iter_data(it);
+    }
+    return NULL;
+}
+
+static CHANSERV_FUNC(cmd_noregister)
+{
+    const char *target;
+    struct do_not_register *dnr;
+    char buf[INTERVALLEN];
+    unsigned int matches;
+
+    if(argc < 2)
+    {
+        dict_iterator_t it;
+
+        reply("CSMSG_DNR_SEARCH_RESULTS");
+        matches = 0;
+        for(it = dict_first(handle_dnrs); it; it = iter_next(it))
+        {
+            dnr = iter_data(it);
+            if(dnr->set)
+                reply("CSMSG_DNR_INFO_SET", dnr->chan_name, intervalString(buf, now - dnr->set), dnr->setter, dnr->reason);
+            else
+                reply("CSMSG_DNR_INFO", dnr->chan_name, dnr->setter, dnr->reason);
+            matches++;
+        }
+        for(it = dict_first(plain_dnrs); it; it = iter_next(it))
+        {
+            dnr = iter_data(it);
+            if(dnr->set)
+                reply("CSMSG_DNR_INFO_SET", dnr->chan_name, intervalString(buf, now - dnr->set), dnr->setter, dnr->reason);
+            else
+                reply("CSMSG_DNR_INFO", dnr->chan_name, dnr->setter, dnr->reason);
+            matches++;
+        }
+        for(it = dict_first(mask_dnrs); it; it = iter_next(it))
+        {
+            dnr = iter_data(it);
+            if(dnr->set)
+                reply("CSMSG_DNR_INFO_SET", dnr->chan_name, intervalString(buf, now - dnr->set), dnr->setter, dnr->reason);
+            else
+                reply("CSMSG_DNR_INFO", dnr->chan_name, dnr->setter, dnr->reason);
+            matches++;
+        }
+
+        if(matches)
+            reply("MSG_MATCH_COUNT", matches);
+        else
+            reply("MSG_NO_MATCHES");
+        return 0;
+    }
+
+    target = argv[1];
+
+    if(!IsChannelName(target) && (*target != '*'))
+    {
+        reply("CSMSG_NOT_DNR", target);
+        return 0;
+    }
+
+    if(argc > 2)
+    {
+        const char *reason = unsplit_string(argv + 2, argc - 2, NULL);
+        if((*target == '*') && !get_handle_info(target + 1))
+        {
+            reply("MSG_HANDLE_UNKNOWN", target + 1);
+            return 0;
+        }
+        chanserv_add_dnr(target, user->handle_info->handle, reason);
+        reply("CSMSG_NOREGISTER_CHANNEL", target);
+        return 1;
+    }
+
+    reply("CSMSG_DNR_SEARCH_RESULTS");
+    if(*target == '*')
+        matches = chanserv_show_dnrs(user, cmd, NULL, get_handle_info(target + 1));
+    else
+        matches = chanserv_show_dnrs(user, cmd, target, NULL);
+    if(!matches)
+        reply("MSG_NO_MATCHES");
+    return 0;
+}
+
+static CHANSERV_FUNC(cmd_allowregister)
+{
+    const char *chan_name = argv[1];
+
+    if((chan_name[0] == '*') && dict_find(handle_dnrs, chan_name+1, NULL))
+    {
+        dict_remove(handle_dnrs, chan_name+1);
+        reply("CSMSG_DNR_REMOVED", chan_name);
+    }
+    else if(dict_find(plain_dnrs, chan_name, NULL))
+    {
+        dict_remove(plain_dnrs, chan_name);
+        reply("CSMSG_DNR_REMOVED", chan_name);
+    }
+    else if(dict_find(mask_dnrs, chan_name, NULL))
+    {
+        dict_remove(mask_dnrs, chan_name);
+        reply("CSMSG_DNR_REMOVED", chan_name);
+    }
+    else
+    {
+        reply("CSMSG_NO_SUCH_DNR", chan_name);
+        return 0;
+    }
+    return 1;
+}
+
+unsigned int
+chanserv_get_owned_count(struct handle_info *hi)
+{
+    struct userData *cList;
+    unsigned int owned;
+
+    for(owned=0, cList=hi->channels; cList; cList=cList->u_next)
+        if(cList->access == UL_OWNER)
+            owned++;
+    return owned;
+}
+
+static CHANSERV_FUNC(cmd_register)
+{
+    struct mod_chanmode *change;
+    struct handle_info *handle;
+    struct chanData *cData;
+    struct modeNode *mn;
+    char reason[MAXLEN];
+    char *chan_name;
+    unsigned int new_channel, force=0;
+    struct do_not_register *dnr;
+
+    if(channel)
+    {
+        if(channel->channel_info)
+        {
+            reply("CSMSG_ALREADY_REGGED", channel->name);
+            return 0;
+        }
+
+        if(channel->bad_channel)
+        {
+            reply("CSMSG_ILLEGAL_CHANNEL", channel->name);
+            return 0;
+        }
+
+        if(!IsHelping(user) && (!(mn = GetUserMode(channel, user)) || !(mn->modes & MODE_CHANOP)))
+        {
+            reply("CSMSG_MUST_BE_OPPED", channel->name);
+            return 0;
+        }
+
+        new_channel = 0;
+        chan_name = channel->name;
+    }
+    else
+    {
+        if((argc < 2) || !IsChannelName(argv[1]))
+        {
+            reply("MSG_NOT_CHANNEL_NAME");
+            return 0;
+        }
+
+        if(opserv_bad_channel(argv[1]))
+        {
+            reply("CSMSG_ILLEGAL_CHANNEL", argv[1]);
+            return 0;
+        }
+
+        new_channel = 1;
+        chan_name = argv[1];
+    }
+
+    if(argc >= (new_channel+2))
+    {
+       if(!IsHelping(user))
+       {
+           reply("CSMSG_PROXY_FORBIDDEN");
+           return 0;
+       }
+
+       if(!(handle = modcmd_get_handle_info(user, argv[new_channel+1])))
+            return 0;
+        force = (argc > (new_channel+2)) && !irccasecmp(argv[new_channel+2], "force");
+        dnr = chanserv_is_dnr(chan_name, handle);
+    }
+    else
+    {
+       handle = user->handle_info;
+        dnr = chanserv_is_dnr(chan_name, handle);
+    }
+    if(dnr && !force)
+    {
+        if(!IsHelping(user))
+            reply("CSMSG_DNR_CHANNEL", chan_name);
+        else
+            chanserv_show_dnrs(user, cmd, chan_name, handle);
+        return 0;
+    }
+
+    if((chanserv_get_owned_count(handle) >= chanserv_conf.max_owned) && !force)
+    {
+        reply("CSMSG_OWN_TOO_MANY", handle->handle, chanserv_conf.max_owned);
+        return 0;
+    }
+
+    if(new_channel)
+        channel = AddChannel(argv[1], now, NULL, NULL);
+
+    cData = register_channel(channel, user->handle_info->handle);
+    add_channel_user(cData, handle, UL_OWNER, 0, NULL);
+    scan_handle_presence(channel, handle, NULL);
+    cData->modes = chanserv_conf.default_modes;
+    change = mod_chanmode_dup(&cData->modes, 1);
+    change->args[change->argc].mode = MODE_CHANOP;
+    change->args[change->argc].member = AddChannelUser(chanserv, channel);
+    change->argc++;
+    mod_chanmode_announce(chanserv, channel, change);
+    mod_chanmode_free(change);
+
+    /* Initialize the channel's max user record. */
+    cData->max = channel->members.used;
+
+    if(handle != user->handle_info)
+        reply("CSMSG_PROXY_SUCCESS", handle->handle, channel->name);
+    else
+        reply("CSMSG_REG_SUCCESS", channel->name);
+
+    sprintf(reason, "%s registered to %s by %s.", channel->name, handle->handle, user->handle_info->handle);
+    global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, reason);
+    return 1;
+}
+
+static const char *
+make_confirmation_string(struct userData *uData)
+{
+    static char strbuf[16];
+    char *src;
+    unsigned int accum;
+
+    accum = 0;
+    for(src = uData->handle->handle; *src; )
+        accum = accum * 31 + toupper(*src++);
+    if(uData->channel)
+        for(src = uData->channel->channel->name; *src; )
+            accum = accum * 31 + toupper(*src++);
+    sprintf(strbuf, "%08x", accum);
+    return strbuf;
+}
+
+static CHANSERV_FUNC(cmd_unregister)
+{
+    char *name;
+    char reason[MAXLEN];
+    struct chanData *cData;
+    struct userData *uData;
+
+    cData = channel->channel_info;
+    if(!cData)
+    {
+        reply("CSMSG_NOT_REGISTERED", channel->name);
+        return 0;
+    }
+
+    uData = GetChannelUser(cData, user->handle_info);
+    if(!uData || (uData->access < UL_OWNER))
+    {
+        reply("CSMSG_NO_ACCESS");
+        return 0;
+    }
+
+    if(IsProtected(cData))
+    {
+        reply("CSMSG_UNREG_NODELETE", channel->name);
+        return 0;
+    }
+
+    if(!IsHelping(user))
+    {
+        const char *confirm_string;
+        if(IsSuspended(cData))
+        {
+            reply("CSMSG_CHAN_SUSPENDED", channel->name, cData->suspended->reason);
+            return 0;
+        }
+        confirm_string = make_confirmation_string(uData);
+       if((argc < 2) || strcmp(argv[1], confirm_string))
+       {
+           reply("CSMSG_CONFIRM_UNREG", confirm_string);
+           return 0;
+       }
+    }
+
+    sprintf(reason, "unregistered by %s.", user->handle_info->handle);
+    name = strdup(channel->name);
+    unregister_channel(cData, reason);
+    reply("CSMSG_UNREG_SUCCESS", name);
+    free(name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_move)
+{
+    struct chanNode *target;
+    struct modeNode *mn;
+    struct userData *uData;
+    char reason[MAXLEN];
+    struct do_not_register *dnr;
+
+    REQUIRE_PARAMS(2);
+
+    if(IsProtected(channel->channel_info))
+    {
+       reply("CSMSG_MOVE_NODELETE", channel->name);
+       return 0;
+    }
+
+    if(!IsChannelName(argv[1]))
+    {
+        reply("MSG_NOT_CHANNEL_NAME");
+        return 0;
+    }
+
+    if(opserv_bad_channel(argv[1]))
+    {
+        reply("CSMSG_ILLEGAL_CHANNEL", argv[1]);
+        return 0;
+    }
+
+    if(!IsHelping(user) || (argc < 3) || irccasecmp(argv[2], "force"))
+    {
+        for(uData = channel->channel_info->users; uData; uData = uData->next)
+        {
+            if((uData->access == UL_OWNER) && (dnr = chanserv_is_dnr(argv[1], uData->handle)))
+            {
+                if(!IsHelping(user))
+                    reply("CSMSG_DNR_CHANNEL_MOVE", argv[1]);
+                else
+                    chanserv_show_dnrs(user, cmd, argv[1], uData->handle);
+                return 0;
+            }
+        }
+    }
+
+    if(!(target = GetChannel(argv[1])))
+    {
+        target = AddChannel(argv[1], now, NULL, NULL);
+        LockChannel(target);
+        if(!IsSuspended(channel->channel_info))
+            AddChannelUser(chanserv, target);
+    }
+    else if(target->channel_info)
+    {
+       reply("CSMSG_ALREADY_REGGED", target->name);
+       return 0;
+    }
+    else if((!(mn = GetUserMode(target, user)) || !(mn->modes && MODE_CHANOP))
+            && !IsHelping(user))
+    {
+        reply("CSMSG_MUST_BE_OPPED", target->name);
+        return 0;
+    }
+    else if(!IsSuspended(channel->channel_info))
+    {
+        struct mod_chanmode change;
+        change.modes_set = change.modes_clear = 0;
+        change.argc = 1;
+        change.args[0].mode = MODE_CHANOP;
+        change.args[0].member = AddChannelUser(chanserv, target);
+        mod_chanmode_announce(chanserv, target, &change);
+    }
+
+    /* Move the channel_info to the target channel; it
+       shouldn't be necessary to clear timeq callbacks
+       for the old channel. */
+    target->channel_info = channel->channel_info;
+    target->channel_info->channel = target;
+    channel->channel_info = NULL;
+
+    reply("CSMSG_MOVE_SUCCESS", target->name);
+
+    sprintf(reason, "%s moved to %s by %s.", channel->name, target->name, user->handle_info->handle);
+    if(!IsSuspended(target->channel_info))
+    {
+        char reason2[MAXLEN];
+       sprintf(reason2, "Channel moved to %s by %s.", target->name, user->handle_info->handle);
+       DelChannelUser(chanserv, channel, reason2, 0);
+    }
+    UnlockChannel(channel);
+    global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, reason);
+    return 1;
+}
+
+static void
+merge_users(struct chanData *source, struct chanData *target)
+{
+    struct userData *suData, *tuData, *next;
+    dict_iterator_t it;
+    dict_t merge;
+
+    merge = dict_new();
+
+    /* Insert the source's users into the scratch area. */
+    for(suData = source->users; suData; suData = suData->next)
+       dict_insert(merge, suData->handle->handle, suData);
+
+    /* Iterate through the target's users, looking for
+       users common to both channels. The lower access is
+       removed from either the scratch area or target user
+       list. */
+    for(tuData = target->users; tuData; tuData = next)
+    {
+       struct userData *choice;
+
+       next = tuData->next;
+
+       /* If a source user exists with the same handle as a target
+          channel's user, resolve the conflict by removing one. */
+       suData = dict_find(merge, tuData->handle->handle, NULL);
+       if(!suData)
+           continue;
+
+       /* Pick the data we want to keep. */
+        /* If the access is the same, use the later seen time. */
+       if(suData->access == tuData->access)
+           choice = (suData->seen > tuData->seen) ? suData : tuData;
+       else /* Otherwise, keep the higher access level. */
+           choice = (suData->access > tuData->access) ? suData : tuData;
+
+       /* Remove the user that wasn't picked. */
+       if(choice == tuData)
+       {
+           dict_remove(merge, suData->handle->handle);
+           del_channel_user(suData, 0);
+       }
+       else
+           del_channel_user(tuData, 0);
+    }
+
+    /* Move the remaining users to the target channel. */
+    for(it = dict_first(merge); it; it = iter_next(it))
+    {
+       suData = iter_data(it);
+
+       /* Insert the user into the target channel's linked list. */
+       suData->prev = NULL;
+       suData->next = target->users;
+        suData->channel = target;
+
+       if(target->users)
+           target->users->prev = suData;
+       target->users = suData;
+
+       /* Update the user counts for the target channel; the
+          source counts are left alone. */
+       target->userCount++;
+    }
+
+    /* Possible to assert (source->users == NULL) here. */
+    source->users = NULL;
+    dict_delete(merge);
+}
+
+static void
+merge_bans(struct chanData *source, struct chanData *target)
+{
+    struct banData *sbData, *tbData, *sNext, *tNext, *tFront;
+
+    /* Hold on to the original head of the target ban list
+       to avoid comparing source bans with source bans. */
+    tFront = target->bans;
+
+    /* Perform a totally expensive O(n*m) merge, ick. */
+    for(sbData = source->bans; sbData; sbData = sNext)
+    {
+       /* Flag to track whether the ban's been moved
+          to the destination yet. */
+       int moved = 0;
+
+       /* Possible to assert (sbData->prev == NULL) here. */
+       sNext = sbData->next;
+
+       for(tbData = tFront; tbData; tbData = tNext)
+       {
+           tNext = tbData->next;
+
+           /* Perform two comparisons between each source
+              and target ban, conflicts are resolved by
+              keeping the broader ban and copying the later
+              expiration and triggered time. */
+           if(match_ircglobs(tbData->mask, sbData->mask))
+           {
+               /* There is a broader ban in the target channel that
+                  overrides one in the source channel; remove the 
+                  source ban and break. */
+               if(sbData->expires > tbData->expires)
+                   tbData->expires = sbData->expires;
+               if(sbData->triggered > tbData->triggered)
+                   tbData->triggered = sbData->triggered;
+               del_channel_ban(sbData);
+               break;
+           }
+           else if(match_ircglobs(sbData->mask, tbData->mask))
+           {
+               /* There is a broader ban in the source channel that
+                  overrides one in the target channel; remove the
+                  target ban, fall through and move the source over. */
+               if(tbData->expires > sbData->expires)
+                   sbData->expires = tbData->expires;
+               if(tbData->triggered > sbData->triggered)
+                   sbData->triggered = tbData->triggered;
+               if(tbData == tFront)
+                   tFront = tNext;
+               del_channel_ban(tbData);
+           }
+
+           /* Source bans can override multiple target bans, so
+              we allow a source to run through this loop multiple
+              times, but we can only move it once. */
+           if(moved)
+               continue;
+           moved = 1;
+
+           /* Remove the source ban from the source ban list. */
+           if(sbData->next)
+               sbData->next->prev = sbData->prev;
+
+           /* Modify the source ban's associated channel. */
+           sbData->channel = target;
+
+           /* Insert the ban into the target channel's linked list. */
+           sbData->prev = NULL;
+           sbData->next = target->bans;
+
+           if(target->bans)
+               target->bans->prev = sbData;
+           target->bans = sbData;
+
+           /* Update the user counts for the target channel. */
+           target->banCount++;
+       }
+    }
+
+    /* Possible to assert (source->bans == NULL) here. */
+    source->bans = NULL;
+}
+
+static void
+merge_data(struct chanData *source, struct chanData *target)
+{
+    if(source->visited > target->visited)
+       target->visited = source->visited;
+}
+
+static void
+merge_channel(struct chanData *source, struct chanData *target)
+{
+    merge_users(source, target);
+    merge_bans(source, target);
+    merge_data(source, target);
+}
+
+static CHANSERV_FUNC(cmd_merge)
+{
+    struct userData *target_user;
+    struct chanNode *target;
+    char reason[MAXLEN];
+
+    REQUIRE_PARAMS(2);
+
+    /* Make sure the target channel exists and is registered to the user
+       performing the command. */
+    if(!(target = GetChannel(argv[1])))
+    {
+        reply("MSG_INVALID_CHANNEL");
+        return 0;
+    }
+
+    if(!target->channel_info)
+    {
+        reply("CSMSG_NOT_REGISTERED", target->name);
+        return 0;
+    }
+
+    if(IsProtected(channel->channel_info))
+    {
+        reply("CSMSG_MERGE_NODELETE");
+        return 0;
+    }
+
+    if(IsSuspended(target->channel_info))
+    {
+        reply("CSMSG_MERGE_SUSPENDED");
+        return 0;
+    }
+
+    if(channel == target)
+    {
+        reply("CSMSG_MERGE_SELF");
+        return 0;
+    }
+
+    target_user = GetChannelUser(target->channel_info, user->handle_info);
+    if(!target_user || (target_user->access < UL_OWNER))
+    {
+        reply("CSMSG_MERGE_NOT_OWNER");
+        return 0;
+    }
+
+    /* Merge the channel structures and associated data. */
+    merge_channel(channel->channel_info, target->channel_info);
+    sprintf(reason, "merged into %s by %s.", target->name, user->handle_info->handle);
+    unregister_channel(channel->channel_info, reason);
+    reply("CSMSG_MERGE_SUCCESS", target->name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_opchan)
+{
+    struct mod_chanmode change;
+    if(!IsHelping(user) && !channel->channel_info->may_opchan)
+    {
+        reply("CSMSG_ALREADY_OPCHANNED", channel->name);
+        return 0;
+    }
+    channel->channel_info->may_opchan = 0;
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    change.args[0].mode = MODE_CHANOP;
+    change.args[0].member = GetUserMode(channel, chanserv);
+    mod_chanmode_announce(chanserv, channel, &change);
+    reply("CSMSG_OPCHAN_DONE", channel->name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_adduser)
+{
+    struct userData *actee;
+    struct userData *actor;
+    struct handle_info *handle;
+    unsigned short access;
+
+    REQUIRE_PARAMS(3);
+
+    if(channel->channel_info->userCount >= chanserv_conf.max_chan_users)
+    {
+       reply("CSMSG_MAXIMUM_USERS", chanserv_conf.max_chan_users);
+       return 0;
+    }
+
+    access = user_level_from_name(argv[1], UL_OWNER);
+    if(!access)
+    {
+       reply("CSMSG_INVALID_ACCESS", argv[1]);
+       return 0;
+    }
+
+    actor = GetChannelUser(channel->channel_info, user->handle_info);
+    if(actor->access <= access)
+    {
+       reply("CSMSG_NO_BUMP_ACCESS");
+       return 0;
+    }
+
+    if(!(handle = modcmd_get_handle_info(user, argv[2])))
+        return 0;
+
+    if((actee = GetTrueChannelAccess(channel->channel_info, handle)))
+    {
+       reply("CSMSG_USER_EXISTS", handle->handle, channel->name, actee->access);
+       return 0;
+    }
+
+    actee = add_channel_user(channel->channel_info, handle, access, 0, NULL);
+    scan_handle_presence(channel, handle, NULL);
+    reply("CSMSG_ADDED_USER", handle->handle, channel->name, access);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_clvl)
+{
+    struct handle_info *handle;
+    struct userData *victim;
+    struct userData *actor;
+    unsigned short new_access;
+    int privileged = IsHelping(user) && ((user->handle_info->opserv_level >= chanserv_conf.nodelete_level) || !IsProtected(channel->channel_info));
+
+    REQUIRE_PARAMS(3);
+
+    actor = GetChannelUser(channel->channel_info, user->handle_info);
+
+    if(!(handle = modcmd_get_handle_info(user, argv[1])))
+        return 0;
+
+    if(handle == user->handle_info && !privileged)
+    {
+       reply("CSMSG_NO_SELF_CLVL");
+       return 0;
+    }
+
+    if(!(victim = GetTrueChannelAccess(channel->channel_info, handle)))
+    {
+       reply("CSMSG_NO_CHAN_USER", handle->handle, channel->name);
+       return 0;
+    }
+
+    if(actor->access <= victim->access && !privileged)
+    {
+       reply("MSG_USER_OUTRANKED", handle->handle);
+       return 0;
+    }
+
+    new_access = user_level_from_name(argv[2], UL_OWNER);
+
+    if(!new_access)
+    {
+       reply("CSMSG_INVALID_ACCESS", argv[2]);
+       return 0;
+    }
+
+    if(new_access >= actor->access && !privileged)
+    {
+       reply("CSMSG_NO_BUMP_ACCESS");
+       return 0;
+    }
+
+    victim->access = new_access;
+    reply("CSMSG_CHANGED_ACCESS", handle->handle, new_access, channel->name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_deluser)
+{
+    struct handle_info *handle;
+    struct userData *victim;
+    struct userData *actor;
+    unsigned short access;
+    char *chan_name;
+
+    REQUIRE_PARAMS(2);
+
+    actor = GetChannelUser(channel->channel_info, user->handle_info);
+
+    if(!(handle = modcmd_get_handle_info(user, argv[argc-1])))
+        return 0;
+
+    if(!(victim = GetTrueChannelAccess(channel->channel_info, handle)))
+    {
+       reply("CSMSG_NO_CHAN_USER", handle->handle, channel->name);
+       return 0;
+    }
+
+    if(argc > 2)
+    {
+        access = user_level_from_name(argv[1], UL_OWNER);
+        if(!access)
+        {
+            reply("CSMSG_INVALID_ACCESS", argv[1]);
+            return 0;
+        }
+       if(access != victim->access)
+       {
+           reply("CSMSG_INCORRECT_ACCESS", handle->handle, victim->access, argv[1]);
+           return 0;
+       }
+    }
+    else
+    {
+        access = victim->access;
+    }
+
+    if((actor->access <= victim->access) && !IsHelping(user))
+    {
+       reply("MSG_USER_OUTRANKED", victim->handle->handle);
+       return 0;
+    }
+
+    chan_name = strdup(channel->name);
+    del_channel_user(victim, 1);
+    reply("CSMSG_DELETED_USER", handle->handle, access, chan_name);
+    free(chan_name);
+    return 1;
+}
+
+static int
+cmd_mdel_user(struct userNode *user, struct chanNode *channel, unsigned short min_access, unsigned short max_access, char *mask, struct svccmd *cmd)
+{
+    struct userData *actor, *uData, *next;
+
+    actor = GetChannelUser(channel->channel_info, user->handle_info);
+
+    if(min_access > max_access)
+    {
+        reply("CSMSG_BAD_RANGE", min_access, max_access);
+        return 0;
+    }
+
+    if((actor->access <= max_access) && !IsHelping(user))
+    {
+       reply("CSMSG_NO_ACCESS");
+       return 0;
+    }
+
+    for(uData = channel->channel_info->users; uData; uData = next)
+    {
+       next = uData->next;
+
+       if((uData->access >= min_access)
+           && (uData->access <= max_access)
+           && match_ircglob(uData->handle->handle, mask))
+           del_channel_user(uData, 1);
+    }
+
+    reply("CSMSG_DELETED_USERS", mask, min_access, max_access, channel->name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_mdelowner)
+{
+    return cmd_mdel_user(user, channel, UL_OWNER, UL_OWNER, argv[1], cmd);
+}
+
+static CHANSERV_FUNC(cmd_mdelcoowner)
+{
+    return cmd_mdel_user(user, channel, UL_COOWNER, UL_COOWNER, argv[1], cmd);
+}
+
+static CHANSERV_FUNC(cmd_mdelmaster)
+{
+    return cmd_mdel_user(user, channel, UL_MASTER, UL_MASTER, argv[1], cmd);
+}
+
+static CHANSERV_FUNC(cmd_mdelop)
+{
+    return cmd_mdel_user(user, channel, UL_OP, UL_OP, argv[1], cmd);
+}
+
+static CHANSERV_FUNC(cmd_mdelpeon)
+{
+    return cmd_mdel_user(user, channel, UL_PEON, UL_PEON, argv[1], cmd);
+}
+
+static int
+cmd_trim_bans(struct userNode *user, struct chanNode *channel, unsigned long duration)
+{
+    struct banData *bData, *next;
+    char interval[INTERVALLEN];
+    unsigned int count;
+    time_t limit;
+
+    count = 0;
+    limit = now - duration;
+    for(bData = channel->channel_info->bans; bData; bData = next)
+    {
+       next = bData->next;
+
+        if((bData->triggered && bData->triggered >= limit) || (bData->set && bData->set >= limit))
+            continue;
+
+        del_channel_ban(bData);
+        count++;
+    }
+
+    intervalString(interval, duration);
+    send_message(user, chanserv, "CSMSG_TRIMMED_BANS", count, channel->name, interval);
+    return 1;
+}
+
+static int
+cmd_trim_users(struct userNode *user, struct chanNode *channel, unsigned short min_access, unsigned short max_access, unsigned long duration)
+{
+    struct userData *actor, *uData, *next;
+    char interval[INTERVALLEN];
+    unsigned int count;
+    time_t limit;
+
+    actor = GetChannelUser(channel->channel_info, user->handle_info);
+    if(min_access > max_access)
+    {
+        send_message(user, chanserv, "CSMSG_BAD_RANGE", min_access, max_access);
+        return 0;
+    }
+
+    if((actor->access <= max_access) && !IsHelping(user))
+    {
+       send_message(user, chanserv, "CSMSG_NO_ACCESS");
+       return 0;
+    }
+
+    count = 0;
+    limit = now - duration;
+    for(uData = channel->channel_info->users; uData; uData = next)
+    {
+       next = uData->next;
+
+       if((uData->seen > limit) || uData->present)
+           continue;
+
+       if(((uData->access >= min_access) && (uData->access <= max_access))
+           || (max_access && (uData->access < actor->access)))
+       {
+           del_channel_user(uData, 1);
+           count++;
+       }
+    }
+
+    send_message(user, chanserv, "CSMSG_TRIMMED_USERS", count, min_access, max_access, channel->name, intervalString(interval, duration));
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_trim)
+{
+    unsigned long duration;
+    unsigned short min_level, max_level;
+
+    REQUIRE_PARAMS(3);
+
+    duration = ParseInterval(argv[2]);
+    if(duration < 60)
+    {
+       reply("CSMSG_CANNOT_TRIM");
+       return 0;
+    }
+
+    if(!irccasecmp(argv[1], "bans"))
+    {
+       cmd_trim_bans(user, channel, duration);
+       return 1;
+    }
+    else if(!irccasecmp(argv[1], "users"))
+    {
+       cmd_trim_users(user, channel, 0, 0, duration);
+       return 1;
+    }
+    else if(parse_level_range(&min_level, &max_level, argv[1]))
+    {
+       cmd_trim_users(user, channel, min_level, max_level, duration);
+       return 1;
+    }
+    else if((min_level = user_level_from_name(argv[1], UL_OWNER)))
+    {
+       cmd_trim_users(user, channel, min_level, min_level, duration);
+       return 1;
+    }
+    else
+    {
+        reply("CSMSG_INVALID_TRIM", argv[1]);
+        return 0;
+    }
+}
+
+/* If argc is 0 in cmd_up or cmd_down, no notices will be sent
+   to the user. cmd_all takes advantage of this. */
+static CHANSERV_FUNC(cmd_up)
+{
+    struct mod_chanmode change;
+    struct userData *uData;
+    const char *errmsg;
+
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    change.args[0].member = GetUserMode(channel, user);
+    if(!change.args[0].member)
+    {
+        if(argc)
+            reply("MSG_CHANNEL_ABSENT", channel->name);
+       return 0;
+    }
+
+    uData = GetChannelAccess(channel->channel_info, user->handle_info);
+    if(!uData)
+    {
+        if(argc)
+            reply("CSMSG_GODMODE_UP", argv[0]);
+        return 0;
+    }
+    else if(uData->access >= channel->channel_info->lvlOpts[lvlGiveOps])
+    {
+        change.args[0].mode = MODE_CHANOP;
+        errmsg = "CSMSG_ALREADY_OPPED";
+    }
+    else
+    {
+        change.args[0].mode = MODE_VOICE;
+        errmsg = "CSMSG_ALREADY_VOICED";
+    }
+    change.args[0].mode &= ~change.args[0].member->modes;
+    if(!change.args[0].mode)
+    {
+        if(argc)
+            reply(errmsg, channel->name);
+        return 0;
+    }
+    modcmd_chanmode_announce(&change);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_down)
+{
+    struct mod_chanmode change;
+
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    change.args[0].member = GetUserMode(channel, user);
+    if(!change.args[0].member)
+    {
+        if(argc)
+            reply("MSG_CHANNEL_ABSENT", channel->name);
+       return 0;
+    }
+
+    if(!change.args[0].member->modes)
+    {
+        if(argc)
+            reply("CSMSG_ALREADY_DOWN", channel->name);
+       return 0;
+    }
+
+    change.args[0].mode = MODE_REMOVE | change.args[0].member->modes;
+    modcmd_chanmode_announce(&change);
+    return 1;
+}
+
+static int cmd_all(struct userNode *user, UNUSED_ARG(struct chanNode *channel), UNUSED_ARG(unsigned int argc), UNUSED_ARG(char *argv[]), struct svccmd *cmd, modcmd_func_t mcmd)
+{
+    struct userData *cList;
+
+    for(cList = user->handle_info->channels; cList; cList = cList->u_next)
+    {
+       if(IsSuspended(cList->channel)
+           || IsUserSuspended(cList)
+           || !GetUserMode(cList->channel->channel, user))
+           continue;
+
+       mcmd(user, cList->channel->channel, 0, NULL, cmd);
+    }
+
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_upall)
+{
+    return cmd_all(CSFUNC_ARGS, cmd_up);
+}
+
+static CHANSERV_FUNC(cmd_downall)
+{
+    return cmd_all(CSFUNC_ARGS, cmd_down);
+}
+
+typedef int validate_func_t(struct userNode *user, struct chanNode *channel, struct userNode *victim);
+typedef void process_func_t(unsigned int num, struct userNode **newops, struct chanNode *channel, struct userNode *who, int announce);
+
+static int
+modify_users(struct userNode *user, struct chanNode *channel, unsigned int argc, char *argv[], struct svccmd *cmd, validate_func_t validate, chan_mode_t mode, char *action)
+{
+    unsigned int ii, valid;
+    struct userNode *victim;
+    struct mod_chanmode *change;
+
+    change = mod_chanmode_alloc(argc - 1);
+
+    for(ii=valid=0; ++ii < argc; )
+    {
+       if(!(victim = GetUserH(argv[ii])))
+            continue;
+        change->args[valid].mode = mode;
+        change->args[valid].member = GetUserMode(channel, victim);
+        if(!change->args[valid].member)
+            continue;
+        if(validate && !validate(user, channel, victim))
+           continue;
+        valid++;
+    }
+
+    change->argc = valid;
+    if(valid < (argc-1))
+       reply("CSMSG_PROCESS_FAILED");
+    if(valid)
+    {
+        modcmd_chanmode_announce(change);
+        reply(action, channel->name);
+    }
+    mod_chanmode_free(change);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_op)
+{
+    return modify_users(CSFUNC_ARGS, validate_op, MODE_CHANOP, "CSMSG_OPPED_USERS");
+}
+
+static CHANSERV_FUNC(cmd_deop)
+{
+    return modify_users(CSFUNC_ARGS, validate_deop, MODE_REMOVE|MODE_CHANOP, "CSMSG_DEOPPED_USERS");
+}
+
+static CHANSERV_FUNC(cmd_voice)
+{
+    return modify_users(CSFUNC_ARGS, NULL, MODE_VOICE, "CSMSG_VOICED_USERS");
+}
+
+static CHANSERV_FUNC(cmd_devoice)
+{
+    return modify_users(CSFUNC_ARGS, NULL, MODE_REMOVE|MODE_VOICE, "CSMSG_DEVOICED_USERS");
+}
+
+static int
+bad_channel_ban(struct chanNode *channel, struct userNode *user, const char *ban, int *victimCount, struct modeNode **victims)
+{
+    unsigned int ii;
+
+    if(victimCount)
+        *victimCount = 0;
+    for(ii=0; ii<channel->members.used; ii++)
+    {
+        struct modeNode *mn = channel->members.list[ii];
+
+        if(IsService(mn->user))
+            continue;
+
+        if(!user_matches_glob(mn->user, ban, 1))
+            continue;
+
+        if(protect_user(mn->user, user, channel->channel_info))
+            return 1;
+
+        if(victims)
+            victims[(*victimCount)++] = mn;
+    }
+    return 0;
+}
+
+static int
+eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, char *argv[], struct svccmd *cmd, int action)
+{
+    struct userNode *victim;
+    struct modeNode **victims;
+    unsigned int offset, n, victimCount, duration = 0;
+    char *reason = "Bye.", *ban, *name;
+    char interval[INTERVALLEN];
+
+    offset = (action & ACTION_ADD_TIMED_BAN) ? 3 : 2;
+    REQUIRE_PARAMS(offset);
+    if(argc > offset)
+    {
+       reason = unsplit_string(argv + offset, argc - offset, NULL);
+        if(strlen(reason) > (TOPICLEN - (NICKLEN + 3)))
+        {
+            /* Truncate the reason to a length of TOPICLEN, as
+               the ircd does; however, leave room for an ellipsis
+               and the kicker's nick. */
+            sprintf(reason + (TOPICLEN - (NICKLEN + 6)), "...");
+        }
+    }
+
+    if((victim = GetUserH(argv[1])))
+    {
+        victims = alloca(sizeof(victims[0]));
+        victims[0] = GetUserMode(channel, victim);
+        /* XXX: The comparison with ACTION_KICK is just because all
+         * other actions can work on users outside the channel, and we
+         * want to allow those (e.g.  unbans) in that case.  If we add
+         * some other ejection action for in-channel users, change
+         * this too. */
+       victimCount = victims[0] ? 1 : 0;
+
+       if(IsService(victim))
+       {
+           reply("MSG_SERVICE_IMMUNE", victim->nick);
+           return 0;
+       }
+
+        if((action == ACTION_KICK) && !victimCount)
+        {
+            reply("MSG_CHANNEL_USER_ABSENT", victim->nick, channel->name);
+            return 0;
+        }
+
+       if(protect_user(victim, user, channel->channel_info))
+       {
+           reply("CSMSG_USER_PROTECTED", victim->nick);
+           return 0;
+       }
+
+       ban = generate_hostmask(victim, GENMASK_STRICT_HOST|GENMASK_ANY_IDENT);
+       name = victim->nick;
+    }
+    else
+    {
+       if(!is_ircmask(argv[1]))
+       {
+           reply("MSG_NICK_UNKNOWN", argv[1]);
+           return 0;
+       }
+
+       victims = alloca(sizeof(victims[0]) * channel->members.used);
+
+        if(bad_channel_ban(channel, user, argv[1], &victimCount, victims))
+        {
+           reply("CSMSG_MASK_PROTECTED", argv[1]);
+           return 0;
+       }
+
+        if((victimCount > 4) && ((victimCount * 3) > channel->members.used) && !IsOper(user))
+        {
+            reply("CSMSG_LAME_MASK", argv[1]);
+            return 0;
+        }
+
+        if((action == ACTION_KICK) && (victimCount == 0))
+        {
+            reply("CSMSG_NO_MATCHING_USERS", channel->name, argv[1]);
+            return 0;
+        }
+
+       name = ban = strdup(argv[1]);
+    }
+
+    /* Truncate the ban in place if necessary; we must ensure
+       that 'ban' is a valid ban mask before sanitizing it. */
+    sanitize_ircmask(ban);
+
+    if(action & ACTION_ADD_BAN)
+    {
+       struct banData *bData, *next;
+
+       if(channel->channel_info->banCount >= chanserv_conf.max_chan_bans)
+       {
+           reply("CSMSG_MAXIMUM_BANS", chanserv_conf.max_chan_bans);
+           free(ban);
+           return 0;
+       }
+
+       if(action & ACTION_ADD_TIMED_BAN)
+       {
+           duration = ParseInterval(argv[2]);
+
+           if(duration < 15)
+           {
+               reply("CSMSG_DURATION_TOO_LOW");
+               free(ban);
+               return 0;
+           }
+           else if(duration > (86400 * 365 * 2))
+           {
+               reply("CSMSG_DURATION_TOO_HIGH");
+               free(ban);
+               return 0;
+           }
+       }
+
+       for(bData = channel->channel_info->bans; bData; bData = next)
+       {
+           if(match_ircglobs(bData->mask, ban))
+           {
+               int exact = !irccasecmp(bData->mask, ban);
+
+               /* The ban is redundant; there is already a ban
+                  with the same effect in place. */
+               if(exact)
+               {
+                   if(bData->reason)
+                        free(bData->reason);
+                   bData->reason = strdup(reason);
+                    safestrncpy(bData->owner, (user->handle_info ? user->handle_info->handle : user->nick), sizeof(bData->owner));
+                   reply("CSMSG_REASON_CHANGE", ban);
+                   if(!bData->expires)
+                        goto post_add_ban;
+               }
+               if(exact && bData->expires)
+               {
+                   int reset = 0;
+
+                   /* If the ban matches an existing one exactly,
+                      extend the expiration time if the provided
+                      duration is longer. */
+                   if(duration && ((time_t)(now + duration) > bData->expires))
+                   {
+                       bData->expires = now + duration;
+                       reset = 1;
+                   }
+                   else if(!duration)
+                   {
+                       bData->expires = 0;
+                       reset = 1;
+                   }
+
+                   if(reset)
+                   {
+                       /* Delete the expiration timeq entry and
+                          requeue if necessary. */
+                       timeq_del(0, expire_ban, bData, TIMEQ_IGNORE_WHEN);
+
+                       if(bData->expires)
+                           timeq_add(bData->expires, expire_ban, bData);
+
+                       if(duration)
+                           reply("CSMSG_BAN_EXTENDED", ban, intervalString(interval, duration));
+                       else
+                           reply("CSMSG_BAN_ADDED", name, channel->name);
+
+                       goto post_add_ban;
+                   }
+               }
+               reply("CSMSG_REDUNDANT_BAN", name, channel->name);
+
+               free(ban);
+               return 0;
+           }
+
+           next = bData->next;
+           if(match_ircglobs(ban, bData->mask))
+           {
+               /* The ban we are adding makes previously existing
+                  bans redundant; silently remove them. */
+               del_channel_ban(bData);
+           }
+       }
+
+       bData = add_channel_ban(channel->channel_info, ban, (user->handle_info ? user->handle_info->handle : user->nick), now, (victimCount ? now : 0), (duration ? now + duration : 0), reason);
+        free(ban);
+        name = ban = strdup(bData->mask);
+    }
+    else if(ban)
+    {
+        for(n = 0; n < chanserv_conf.old_ban_names->used; ++n)
+        {
+            extern const char *hidden_host_suffix;
+            const char *old_name = chanserv_conf.old_ban_names->list[n];
+            char *new_mask;
+            unsigned int l1, l2;
+
+            l1 = strlen(ban);
+            l2 = strlen(old_name);
+            if(l2+2 > l1)
+                continue;
+            if(irccasecmp(ban + l1 - l2, old_name))
+                continue;
+            new_mask = malloc(MAXLEN);
+            sprintf(new_mask, "%.*s%s", l1-l2, ban, hidden_host_suffix);
+            free(ban);
+            name = ban = new_mask;
+        }
+    }
+
+  post_add_ban:
+    if(action & ACTION_BAN)
+    {
+       unsigned int exists;
+        struct mod_chanmode *change;
+
+       if(channel->banlist.used >= MAXBANS)
+       {
+           reply("CSMSG_BANLIST_FULL", channel->name);
+           free(ban);
+           return 0;
+       }
+
+        exists = ChannelBanExists(channel, ban);
+        change = mod_chanmode_alloc(victimCount + 1);
+        for(n = 0; n < victimCount; ++n)
+        {
+            change->args[n].mode = MODE_REMOVE|MODE_CHANOP|MODE_VOICE;
+            change->args[n].member = victims[n];
+        }
+        if(!exists)
+        {
+            change->args[n].mode = MODE_BAN;
+            change->args[n++].hostmask = ban;
+        }
+        change->argc = n;
+        if (cmd)
+            modcmd_chanmode_announce(change);
+        else
+            mod_chanmode_announce(chanserv, channel, change);
+        mod_chanmode_free(change);
+
+        if(exists && (action == ACTION_BAN))
+       {
+            reply("CSMSG_REDUNDANT_BAN", name, channel->name);
+            free(ban);
+            return 0;
+        }
+    }
+
+    if(action & ACTION_KICK)
+    {
+        char kick_reason[MAXLEN];
+       sprintf(kick_reason, "%s (%s)", reason, user->nick);
+
+       for(n = 0; n < victimCount; n++)
+           KickChannelUser(victims[n]->user, channel, chanserv, kick_reason);
+    }
+
+    if(!cmd)
+    {
+        /* No response, since it was automated. */
+    }
+    else if(action & ACTION_ADD_BAN)
+    {
+       if(duration)
+           reply("CSMSG_TIMED_BAN_ADDED", name, channel->name, intervalString(interval, duration));
+       else
+           reply("CSMSG_BAN_ADDED", name, channel->name);
+    }
+    else if((action & (ACTION_BAN | ACTION_KICK)) == (ACTION_BAN | ACTION_KICK))
+       reply("CSMSG_KICK_BAN_DONE", name, channel->name);
+    else if(action & ACTION_BAN)
+       reply("CSMSG_BAN_DONE", name, channel->name);
+    else if(action & ACTION_KICK && victimCount)
+       reply("CSMSG_KICK_DONE", name, channel->name);
+
+    free(ban);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_kickban)
+{
+    return eject_user(CSFUNC_ARGS, ACTION_KICK | ACTION_BAN);
+}
+
+static CHANSERV_FUNC(cmd_kick)
+{
+    return eject_user(CSFUNC_ARGS, ACTION_KICK);
+}
+
+static CHANSERV_FUNC(cmd_ban)
+{
+    return eject_user(CSFUNC_ARGS, ACTION_BAN);
+}
+
+static CHANSERV_FUNC(cmd_addban)
+{
+    return eject_user(CSFUNC_ARGS, ACTION_KICK | ACTION_BAN | ACTION_ADD_BAN);
+}
+
+static CHANSERV_FUNC(cmd_addtimedban)
+{
+    return eject_user(CSFUNC_ARGS, ACTION_KICK | ACTION_BAN | ACTION_ADD_BAN | ACTION_ADD_TIMED_BAN);
+}
+
+static struct mod_chanmode *
+find_matching_bans(struct banList *bans, struct userNode *actee, const char *mask)
+{
+    struct mod_chanmode *change;
+    unsigned char *match;
+    unsigned int ii, count;
+
+    match = alloca(bans->used);
+    if(actee)
+    {
+        for(ii = count = 0; ii < bans->used; ++ii)
+        {
+            match[ii] = user_matches_glob(actee, bans->list[ii]->ban, 1);
+            if(match[ii])
+                count++;
+        }
+    }
+    else
+    {
+        for(ii = count = 0; ii < bans->used; ++ii)
+        {
+            match[ii] = match_ircglobs(mask, bans->list[ii]->ban);
+            if(match[ii])
+                count++;
+        }
+    }
+    if(!count)
+        return NULL;
+    change = mod_chanmode_alloc(count);
+    for(ii = count = 0; ii < bans->used; ++ii)
+    {
+        if(!match[ii])
+            continue;
+        change->args[count].mode = MODE_REMOVE | MODE_BAN;
+        change->args[count++].hostmask = bans->list[ii]->ban;
+    }
+    return change;
+}
+
+static int
+unban_user(struct userNode *user, struct chanNode *channel, unsigned int argc, char *argv[], struct svccmd *cmd, int action)
+{
+    struct userNode *actee;
+    char *mask = NULL;
+    int acted = 0;
+
+    REQUIRE_PARAMS(2);
+
+    /* may want to allow a comma delimited list of users... */
+    if(!(actee = GetUserH(argv[1])))
+    {
+       if(!is_ircmask(argv[1]))
+       {
+           reply("MSG_NICK_UNKNOWN", argv[1]);
+           return 0;
+       }
+
+       mask = strdup(argv[1]);
+    }
+
+    /* We don't sanitize the mask here because ircu
+       doesn't do it. */
+    if(action & ACTION_UNBAN)
+    {
+        struct mod_chanmode *change;
+        change = find_matching_bans(&channel->banlist, actee, mask);
+        if(change)
+        {
+            modcmd_chanmode_announce(change);
+            mod_chanmode_free(change);
+            acted = 1;
+        }
+    }
+
+    if(action & ACTION_DEL_BAN)
+    {
+       struct banData *ban, *next;
+
+       ban = channel->channel_info->bans;
+       while(ban)
+       {
+           if(actee)
+               for( ; ban && !user_matches_glob(actee, ban->mask, 1);
+                    ban = ban->next);
+           else
+               for( ; ban && !match_ircglobs(mask, ban->mask);
+                    ban = ban->next);
+           if(!ban)
+                break;
+           next = ban->next;
+           del_channel_ban(ban);
+           ban = next;
+           acted = 1;
+       }
+    }
+
+    if(!acted)
+       reply("CSMSG_BAN_NOT_FOUND", actee ? actee->nick : mask);
+    else
+       reply("CSMSG_BAN_REMOVED", actee ? actee->nick : mask);
+    if(mask)
+        free(mask);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_unban)
+{
+    return unban_user(CSFUNC_ARGS, ACTION_UNBAN);
+}
+
+static CHANSERV_FUNC(cmd_delban)
+{
+    /* it doesn't necessarily have to remove the channel ban - may want
+       to make that an option. */
+    return unban_user(CSFUNC_ARGS, ACTION_UNBAN | ACTION_DEL_BAN);
+}
+
+static CHANSERV_FUNC(cmd_unbanme)
+{
+    struct userData *uData = GetChannelUser(channel->channel_info, user->handle_info);
+    long flags = ACTION_UNBAN;
+
+    /* remove permanent bans if the user has the proper access. */
+    if(uData->access >= UL_MASTER)
+       flags |= ACTION_DEL_BAN;
+
+    argv[1] = user->nick;
+    return unban_user(user, channel, 2, argv, cmd, flags);
+}
+
+static CHANSERV_FUNC(cmd_unbanall)
+{
+    struct mod_chanmode *change;
+    unsigned int ii;
+
+    if(!channel->banlist.used)
+    {
+       reply("CSMSG_NO_BANS", channel->name);
+       return 0;
+    }
+
+    change = mod_chanmode_alloc(channel->banlist.used);
+    for(ii=0; ii<channel->banlist.used; ii++)
+    {
+        change->args[ii].mode = MODE_REMOVE | MODE_BAN;
+        change->args[ii].hostmask = channel->banlist.list[ii]->ban;
+    }
+    modcmd_chanmode_announce(change);
+    mod_chanmode_free(change);
+    reply("CSMSG_BANS_REMOVED", channel->name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_open)
+{
+    struct mod_chanmode *change;
+
+    change = find_matching_bans(&channel->banlist, user, NULL);
+    if(!change)
+        change = mod_chanmode_alloc(0);
+    change->modes_clear |= MODE_INVITEONLY | MODE_LIMIT | MODE_KEY;
+    if(!check_user_level(channel, user, lvlEnfModes, 1, 0)
+       && channel->channel_info->modes.modes_set)
+        change->modes_clear &= ~channel->channel_info->modes.modes_set;
+    modcmd_chanmode_announce(change);
+    reply("CSMSG_CHANNEL_OPENED", channel->name);
+    mod_chanmode_free(change);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_access)
+{
+    struct userNode *target;
+    struct handle_info *target_handle;
+    struct userData *uData;
+    int helping;
+    char prefix[MAXLEN];
+
+    if(!channel)
+    {
+        struct userData *uData;
+        const char *chanName;
+        int hide = 0;
+
+        target_handle = user->handle_info;
+        if(!target_handle)
+        {
+            reply("MSG_AUTHENTICATE");
+            return 0;
+        }
+        if(argc > 1)
+        {
+            if(!IsHelping(user))
+            {
+                reply("CSMSG_ACCESS_SELF_ONLY", argv[0]);
+                return 0;
+            }
+
+            if(!(target_handle = modcmd_get_handle_info(user, argv[1])))
+                return 0;
+            hide = 1;
+        }
+        if(!target_handle->channels)
+        {
+            reply("CSMSG_SQUAT_ACCESS");
+            return 1;
+        }
+        reply("CSMSG_INFOLINE_LIST", target_handle->handle);
+        for(uData = target_handle->channels; uData; uData = uData->u_next)
+        {
+            struct chanData *cData = uData->channel;
+
+            if(uData->access > UL_OWNER)
+                continue;
+            if(IsProtected(cData) && hide && !GetTrueChannelAccess(cData, user->handle_info))
+                continue;
+            chanName = cData->channel->name;
+            if(uData->info)
+                send_message_type(4, user, cmd->parent->bot, "[%s (%d)] %s", chanName, uData->access, uData->info);
+            else
+                send_message_type(4, user, cmd->parent->bot, "[%s (%d)]", chanName, uData->access);
+        }
+        return 1;
+    }
+
+    if(argc < 2)
+    {
+       target = user;
+        target_handle = target->handle_info;
+    }
+    else if((target = GetUserH(argv[1])))
+    {
+        target_handle = target->handle_info;
+    }
+    else if(argv[1][0] == '*')
+    {
+        if(!(target_handle = get_handle_info(argv[1]+1)))
+        {
+            reply("MSG_HANDLE_UNKNOWN", argv[1]+1);
+            return 0;
+        }
+    }
+    else
+    {
+        reply("MSG_NICK_UNKNOWN", argv[1]);
+        return 0;
+    }
+
+    assert(target || target_handle);
+
+    if(target == chanserv)
+    {
+       reply("CSMSG_IS_CHANSERV");
+       return 1;
+    }
+
+    if(!target_handle)
+    {
+        if(IsOper(target))
+        {
+            reply("CSMSG_LAZY_SMURF_TARGET", target->nick, chanserv_conf.irc_operator_epithet);
+            return 0;
+        }
+       if(target != user)
+       {
+           reply("MSG_USER_AUTHENTICATE", target->nick);
+           return 0;
+       }
+        reply("MSG_AUTHENTICATE");
+        return 0;
+    }
+
+    if(target)
+    {
+        const char *epithet = NULL, *type = NULL;
+        if(IsOper(target))
+        {
+            epithet = chanserv_conf.irc_operator_epithet;
+            type = "IRCOp";
+        }
+        else if(IsNetworkHelper(target))
+        {
+            epithet = chanserv_conf.network_helper_epithet;
+            type = "network helper";
+        }
+        else if(IsSupportHelper(target))
+        {
+            epithet = chanserv_conf.support_helper_epithet;
+            type = "support helper";
+        }
+        if(epithet)
+        {
+            if(target_handle->epithet)
+                reply("CSMSG_SMURF_TARGET", target->nick, target_handle->epithet, type);
+            else if(epithet)
+                reply("CSMSG_SMURF_TARGET", target->nick, epithet, type);
+        }
+        sprintf(prefix, "%s (%s)", target->nick, target_handle->handle);
+    }
+    else
+    {
+        sprintf(prefix, "%s", target_handle->handle);
+    }
+
+    if(!channel->channel_info)
+    {
+        reply("CSMSG_NOT_REGISTERED", channel->name);
+        return 1;
+    }
+
+    helping = HANDLE_FLAGGED(target_handle, HELPING)
+        && ((target_handle->opserv_level >= chanserv_conf.nodelete_level) || !IsProtected(channel->channel_info));
+    if((uData = GetTrueChannelAccess(channel->channel_info, target_handle)))
+    {
+        reply((helping ? "CSMSG_HELPER_HAS_ACCESS" : "CSMSG_USER_HAS_ACCESS"), prefix, uData->access, channel->name);
+        /* To prevent possible information leaks, only show infolines
+         * if the requestor is in the channel or it's their own
+         * handle. */
+        if(uData->info && (GetUserMode(channel, user) || (target_handle == user->handle_info)))
+        {
+            send_message_type(4, user, cmd->parent->bot, "[%s] %s", (target ? target->nick : target_handle->handle), uData->info);
+        }
+        /* Likewise, only say it's suspended if the user has active
+         * access in that channel or it's their own entry. */
+        if(IsUserSuspended(uData)
+           && (GetChannelUser(channel->channel_info, user->handle_info)
+               || (user->handle_info == uData->handle)))
+        {
+            reply("CSMSG_USER_SUSPENDED", (target ? target->nick : target_handle->handle), channel->name);
+        }
+    }
+    else
+    {
+        reply((helping ? "CSMSG_HELPER_NO_ACCESS" : "CSMSG_USER_NO_ACCESS"), prefix, channel->name);
+    }
+
+    return 1;
+}
+
+static void
+zoot_list(struct listData *list)
+{
+    struct userData *uData;
+    unsigned int start, curr;
+    struct helpfile_table tmp_table;
+    const char **temp, *msg;
+
+    if(list->table.length == 1)
+    {
+        if(list->search)
+            send_message(list->user, list->bot, "CSMSG_ACCESS_SEARCH_HEADER", list->channel->name, list->lowest, list->highest, list->search);
+        else
+            send_message(list->user, list->bot, "CSMSG_ACCESS_ALL_HEADER", list->channel->name, list->lowest, list->highest);
+        msg = user_find_message(list->user, "MSG_NONE");
+        send_message_type(4, list->user, list->bot, "  %s", msg);
+    }
+    tmp_table.width = list->table.width;
+    tmp_table.flags = list->table.flags;
+    list->table.contents[0][0] = " ";
+    for(start = curr = 1; curr < list->table.length; )
+    {
+        uData = list->users[curr-1];
+        list->table.contents[curr++][0] = " ";
+        if((curr == list->table.length) || (list->users[curr-1]->access != uData->access))
+        {
+            if(list->search)
+                send_message(list->user, list->bot, "CSMSG_ACCESS_SEARCH_HEADER", list->channel->name, list->lowest, list->highest, list->search);
+            else
+                send_message(list->user, list->bot, "CSMSG_ACCESS_ALL_HEADER", list->channel->name, list->lowest, list->highest);
+            temp = list->table.contents[--start];
+            list->table.contents[start] = list->table.contents[0];
+            tmp_table.contents = list->table.contents + start;
+            tmp_table.length = curr - start;
+            table_send(list->bot, list->user->nick, 0, NULL, tmp_table);
+            list->table.contents[start] = temp;
+            start = curr;
+        }
+    }
+}
+
+static void
+def_list(struct listData *list)
+{
+    const char *msg;
+    if(list->search)
+        send_message(list->user, list->bot, "CSMSG_ACCESS_SEARCH_HEADER", list->channel->name, list->lowest, list->highest, list->search);
+    else
+        send_message(list->user, list->bot, "CSMSG_ACCESS_ALL_HEADER", list->channel->name, list->lowest, list->highest);
+    table_send(list->bot, list->user->nick, 0, NULL, list->table);
+    if(list->table.length == 1)
+    {
+        msg = user_find_message(list->user, "MSG_NONE");
+        send_message_type(4, list->user, list->bot, "  %s", msg);
+    }
+}
+
+static int
+userData_access_comp(const void *arg_a, const void *arg_b)
+{
+    const struct userData *a = *(struct userData**)arg_a;
+    const struct userData *b = *(struct userData**)arg_b;
+    int res;
+    if(a->access != b->access)
+        res = b->access - a->access;
+    else
+        res = irccasecmp(a->handle->handle, b->handle->handle);
+    return res;
+}
+
+static int
+cmd_list_users(struct userNode *user, struct chanNode *channel, unsigned int argc, char *argv[], struct svccmd *cmd, unsigned short lowest, unsigned short highest)
+{
+    void (*send_list)(struct listData *);
+    struct userData *uData;
+    struct listData lData;
+    unsigned int matches;
+    const char **ary;
+
+    lData.user = user;
+    lData.bot = cmd->parent->bot;
+    lData.channel = channel;
+    lData.lowest = lowest;
+    lData.highest = highest;
+    lData.search = (argc > 1) ? argv[1] : NULL;
+    send_list = zoot_list;
+
+    if(user->handle_info)
+    {
+       switch(user->handle_info->userlist_style)
+       {
+       case HI_STYLE_DEF: send_list = def_list; break;
+        case HI_STYLE_ZOOT: send_list = zoot_list; break;
+       }
+    }
+
+    lData.users = alloca(channel->channel_info->userCount * sizeof(struct userData *));
+    matches = 0;
+    for(uData = channel->channel_info->users; uData; uData = uData->next)
+    {
+       if((uData->access < lowest)
+           || (uData->access > highest)
+           || (lData.search && !match_ircglob(uData->handle->handle, lData.search)))
+           continue;
+       lData.users[matches++] = uData;
+    }
+    qsort(lData.users, matches, sizeof(lData.users[0]), userData_access_comp);
+
+    lData.table.length = matches+1;
+    lData.table.width = 4;
+    lData.table.flags = TABLE_NO_FREE;
+    lData.table.contents = malloc(lData.table.length*sizeof(*lData.table.contents));
+    ary = malloc(lData.table.width*sizeof(**lData.table.contents));
+    lData.table.contents[0] = ary;
+    ary[0] = "Access";
+    ary[1] = "Account";
+    ary[2] = "Last Seen";
+    ary[3] = "Status";
+    for(matches = 1; matches < lData.table.length; ++matches)
+    {
+        struct userData *uData = lData.users[matches-1];
+        char seen[INTERVALLEN];
+
+        ary = malloc(lData.table.width*sizeof(**lData.table.contents));
+        lData.table.contents[matches] = ary;
+        ary[0] = strtab(uData->access);
+        ary[1] = uData->handle->handle;
+        if(uData->present)
+            ary[2] = "Here";
+        else if(!uData->seen)
+            ary[2] = "Never";
+        else
+            ary[2] = intervalString(seen, now - uData->seen);
+        ary[2] = strdup(ary[2]);
+        if(IsUserSuspended(uData))
+            ary[3] = "Suspended";
+        else if(HANDLE_FLAGGED(uData->handle, FROZEN))
+            ary[3] = "Vacation";
+        else
+            ary[3] = "Normal";
+    }
+    send_list(&lData);
+    for(matches = 1; matches < lData.table.length; ++matches)
+    {
+        free((char*)lData.table.contents[matches][2]);
+        free(lData.table.contents[matches]);
+    }
+    free(lData.table.contents[0]);
+    free(lData.table.contents);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_users)
+{
+    return cmd_list_users(CSFUNC_ARGS, 0, UL_OWNER);
+}
+
+static CHANSERV_FUNC(cmd_wlist)
+{
+    return cmd_list_users(CSFUNC_ARGS, UL_OWNER, UL_OWNER);
+}
+
+static CHANSERV_FUNC(cmd_clist)
+{
+    return cmd_list_users(CSFUNC_ARGS, UL_COOWNER, UL_OWNER-1);
+}
+static CHANSERV_FUNC(cmd_mlist)
+{
+    return cmd_list_users(CSFUNC_ARGS, UL_MASTER, UL_COOWNER-1);
+}
+static CHANSERV_FUNC(cmd_olist)
+{
+    return cmd_list_users(CSFUNC_ARGS, UL_OP, UL_MASTER-1);
+}
+static CHANSERV_FUNC(cmd_plist)
+{
+    return cmd_list_users(CSFUNC_ARGS, 1, UL_OP-1);
+}
+
+static CHANSERV_FUNC(cmd_bans)
+{
+    struct helpfile_table tbl;
+    unsigned int matches = 0, timed = 0, ii;
+    char t_buffer[INTERVALLEN], e_buffer[INTERVALLEN], *search;
+    const char *msg_never, *triggered, *expires;
+    struct banData *ban, **bans;
+
+    if(argc > 1)
+       search = argv[1];
+    else
+        search = NULL;
+
+    bans = alloca(channel->channel_info->banCount * sizeof(struct banData *));
+
+    for(ban = channel->channel_info->bans; ban; ban = ban->next)
+    {
+       if(search && !match_ircglobs(search, ban->mask))
+           continue;
+       bans[matches++] = ban;
+       if(ban->expires)
+            timed = 1;
+    }
+
+    tbl.length = matches + 1;
+    tbl.width = 4 + timed;
+    tbl.flags = 0;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = malloc(tbl.length * sizeof(tbl.contents[0]));
+    tbl.contents[0] = malloc(tbl.width * sizeof(tbl.contents[0][0]));
+    tbl.contents[0][0] = "Mask";
+    tbl.contents[0][1] = "Set By";
+    tbl.contents[0][2] = "Triggered";
+    if(timed)
+    {
+        tbl.contents[0][3] = "Expires";
+        tbl.contents[0][4] = "Reason";
+    }
+    else
+        tbl.contents[0][3] = "Reason";
+    if(!matches)
+    {
+        table_send(cmd->parent->bot, user->nick, 0, NULL, tbl);
+       reply("MSG_NONE");
+       return 0;
+    }
+
+    msg_never = user_find_message(user, "MSG_NEVER");
+    for(ii = 0; ii < matches; )
+    {
+       ban = bans[ii];
+
+       if(!timed)
+           expires = "";
+       else if(ban->expires)
+           expires = intervalString(e_buffer, ban->expires - now);
+       else
+           expires = msg_never;
+
+       if(ban->triggered)
+           triggered = intervalString(t_buffer, now - ban->triggered);
+       else
+           triggered = msg_never;
+
+        tbl.contents[++ii] = malloc(tbl.width * sizeof(tbl.contents[0][0]));
+        tbl.contents[ii][0] = ban->mask;
+        tbl.contents[ii][1] = ban->owner;
+        tbl.contents[ii][2] = strdup(triggered);
+        if(timed)
+        {
+            tbl.contents[ii][3] = strdup(expires);
+            tbl.contents[ii][4] = ban->reason;
+        }
+        else
+            tbl.contents[ii][3] = ban->reason;
+    }
+    table_send(cmd->parent->bot, user->nick, 0, NULL, tbl);
+    reply("MSG_MATCH_COUNT", matches);
+    for(ii = 1; ii < tbl.length; ++ii)
+    {
+        free((char*)tbl.contents[ii][2]);
+        if(timed)
+            free((char*)tbl.contents[ii][3]);
+        free(tbl.contents[ii]);
+    }
+    free(tbl.contents[0]);
+    free(tbl.contents);
+    return 1;
+}
+
+static int
+bad_topic(struct chanNode *channel, struct userNode *user, const char *new_topic)
+{
+    struct chanData *cData = channel->channel_info;
+    if(check_user_level(channel, user, lvlEnfTopic, 1, 0))
+        return 0;
+    if(cData->topic_mask)
+        return !match_ircglob(new_topic, cData->topic_mask);
+    else if(cData->topic)
+        return irccasecmp(new_topic, cData->topic);
+    else
+        return 0;
+}
+
+static CHANSERV_FUNC(cmd_topic)
+{
+    struct chanData *cData;
+    char *topic;
+
+    cData = channel->channel_info;
+    if(argc < 2)
+    {
+       if(cData->topic)
+       {
+           SetChannelTopic(channel, chanserv, cData->topic, 1);
+           reply("CSMSG_TOPIC_SET", cData->topic);
+            return 1;
+       }
+
+        reply("CSMSG_NO_TOPIC", channel->name);
+        return 0;
+    }
+
+    topic = unsplit_string(argv + 1, argc - 1, NULL);
+    /* If they say "!topic *", use an empty topic. */
+    if((topic[0] == '*') && (topic[1] == 0))
+        topic[0] = 0;
+    if(bad_topic(channel, user, topic))
+    {
+        char *topic_mask = cData->topic_mask;
+        if(topic_mask)
+        {
+            char new_topic[TOPICLEN+1], tchar;
+            int pos=0, starpos=-1, dpos=0, len;
+
+            while((tchar = topic_mask[pos++]) && (dpos <= TOPICLEN))
+            {
+                switch(tchar)
+                {
+                case '*':
+                    if(starpos != -1)
+                        goto bad_mask;
+                    len = strlen(topic);
+                    if((dpos + len) > TOPICLEN)
+                        len = TOPICLEN + 1 - dpos;
+                    memcpy(new_topic+dpos, topic, len);
+                    dpos += len;
+                    starpos = pos;
+                    break;
+                case '\\': tchar = topic_mask[pos++]; /* and fall through */
+                default: new_topic[dpos++] = tchar; break;
+                }
+            }
+            if((dpos > TOPICLEN) || tchar)
+            {
+            bad_mask:
+                reply("CSMSG_TOPICMASK_CONFLICT1", channel->name, topic_mask);
+                reply("CSMSG_TOPICMASK_CONFLICT2", TOPICLEN);
+                return 0;
+            }
+            new_topic[dpos] = 0;
+            SetChannelTopic(channel, chanserv, new_topic, 1);
+        } else {
+            reply("CSMSG_TOPIC_LOCKED", channel->name);
+            return 0;
+        }
+    }
+    else
+        SetChannelTopic(channel, chanserv, topic, 1);
+
+    if(cData->flags & CHANNEL_TOPIC_SNARF)
+    {
+        /* Grab the topic and save it as the default topic. */
+        free(cData->topic);
+        cData->topic = strdup(channel->topic);
+    }
+
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_mode)
+{
+    struct mod_chanmode *change;
+    
+    if(argc < 2)
+    {
+        change = &channel->channel_info->modes;
+       if(change->modes_set || change->modes_clear) {
+            modcmd_chanmode_announce(change);
+            reply("CSMSG_DEFAULTED_MODES", channel->name);
+       } else
+           reply("CSMSG_NO_MODES", channel->name);
+       return 1;
+    }
+
+    change = mod_chanmode_parse(channel, argv+1, argc-1, MCP_KEY_FREE);
+    if(!change)
+    {
+       reply("MSG_INVALID_MODES", unsplit_string(argv+1, argc-1, NULL));
+       return 0;
+    }
+
+    if(!check_user_level(channel, user, lvlEnfModes, 1, 0)
+       && mode_lock_violated(&channel->channel_info->modes, change))
+    {
+        char modes[MAXLEN];
+        mod_chanmode_format(&channel->channel_info->modes, modes);
+        reply("CSMSG_MODE_LOCKED", modes, channel->name);
+        return 0;
+    }
+
+    modcmd_chanmode_announce(change);
+    mod_chanmode_free(change);
+    reply("CSMSG_MODES_SET", unsplit_string(argv+1, argc-1, NULL));
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_invite)
+{
+    struct userData *uData;
+    struct userNode *invite;
+
+    uData = GetChannelUser(channel->channel_info, user->handle_info);
+
+    if(argc > 1)
+    {
+        if(!(invite = GetUserH(argv[1])))
+       {
+            reply("MSG_NICK_UNKNOWN", argv[1]);
+            return 0;
+        }
+    }
+    else
+        invite = user;
+
+    if(GetUserMode(channel, invite))
+    {
+       reply("CSMSG_ALREADY_PRESENT", invite->nick, channel->name);
+       return 0;
+    }
+
+    if(user != invite)
+    {
+       char *reason = (argc > 2) ? unsplit_string(argv + 2, argc - 2, NULL) : "";
+       send_message(invite, chanserv, "CSMSG_INVITING_YOU", user->nick, channel->name, (argc > 2) ? ": " : ".", reason);
+    }
+    irc_invite(chanserv, invite, channel);
+    if(argc > 1)
+       reply("CSMSG_INVITED_USER", argv[1], channel->name);
+
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_inviteme)
+{
+    struct userData *uData;
+
+    if(GetUserMode(channel, user))
+    {
+       reply("CSMSG_YOU_ALREADY_PRESENT", channel->name);
+       return 0;
+    }
+    if(channel->channel_info
+       && !(channel->channel_info->flags & CHANNEL_PEON_INVITE)
+       && (uData = GetChannelUser(channel->channel_info, user->handle_info))
+       && (uData->access < channel->channel_info->lvlOpts[lvlGiveOps]))
+    {
+        reply("CSMSG_LOW_CHANNEL_ACCESS", channel->name);
+        return 0;
+    }
+    irc_invite(cmd->parent->bot, user, channel);
+    return 1;
+}
+
+static void
+show_suspension_info(struct svccmd *cmd, struct userNode *user, struct suspended *suspended)
+{
+    unsigned int combo;
+    char buf1[INTERVALLEN], buf2[INTERVALLEN];
+
+    /* We display things based on two dimensions:
+     * - Issue time: present or absent
+     * - Expiration: revoked, expired, expires in future, or indefinite expiration
+     * (in order of precedence, so something both expired and revoked
+     * only counts as revoked)
+     */
+    combo = (suspended->issued ? 4 : 0)
+        + (suspended->revoked ? 3 : suspended->expires ? ((suspended->expires < now) ? 2 : 1) : 0);
+    switch(combo) {
+    case 0: /* no issue time, indefinite expiration */
+        reply("CSMSG_CHANNEL_SUSPENDED_0", suspended->suspender, suspended->reason);
+        break;
+    case 1: /* no issue time, expires in future */
+        intervalString(buf1, suspended->expires-now);
+        reply("CSMSG_CHANNEL_SUSPENDED_1", suspended->suspender, buf1, suspended->reason);
+        break;
+    case 2: /* no issue time, expired */
+        intervalString(buf1, now-suspended->expires);
+        reply("CSMSG_CHANNEL_SUSPENDED_2", suspended->suspender, buf1, suspended->reason);
+        break;
+    case 3: /* no issue time, revoked */
+        intervalString(buf1, now-suspended->revoked);
+        reply("CSMSG_CHANNEL_SUSPENDED_3", suspended->suspender, buf1, suspended->reason);
+        break;
+    case 4: /* issue time set, indefinite expiration */
+        intervalString(buf1, now-suspended->issued);
+        reply("CSMSG_CHANNEL_SUSPENDED_4", buf1, suspended->suspender, suspended->reason);
+        break;
+    case 5: /* issue time set, expires in future */
+        intervalString(buf1, now-suspended->issued);
+        intervalString(buf2, suspended->expires-now);
+        reply("CSMSG_CHANNEL_SUSPENDED_5", buf1, suspended->suspender, buf2, suspended->reason);
+        break;
+    case 6: /* issue time set, expired */
+        intervalString(buf1, now-suspended->issued);
+        intervalString(buf2, now-suspended->expires);
+        reply("CSMSG_CHANNEL_SUSPENDED_6", buf1, suspended->suspender, buf2, suspended->reason);
+        break;
+    case 7: /* issue time set, revoked */
+        intervalString(buf1, now-suspended->issued);
+        intervalString(buf2, now-suspended->revoked);
+        reply("CSMSG_CHANNEL_SUSPENDED_7", buf1, suspended->suspender, buf2, suspended->reason);
+        break;
+    default:
+        log_module(CS_LOG, LOG_ERROR, "Invalid combo value %d in show_suspension_info()", combo);
+        return;
+    }
+}
+
+static CHANSERV_FUNC(cmd_info)
+{
+    char modes[MAXLEN], buffer[INTERVALLEN];
+    struct userData *uData, *owner;
+    struct chanData *cData;
+    struct do_not_register *dnr;
+    struct note *note;
+    dict_iterator_t it;
+    int privileged;
+
+    cData = channel->channel_info;
+    reply("CSMSG_CHANNEL_INFO", channel->name);
+
+    uData = GetChannelUser(cData, user->handle_info);
+    if(uData && (uData->access >= cData->lvlOpts[lvlGiveOps]))
+    {
+        mod_chanmode_format(&cData->modes, modes);
+       reply("CSMSG_CHANNEL_TOPIC", cData->topic);
+       reply("CSMSG_CHANNEL_MODES", modes[0] ? modes : user_find_message(user, "MSG_NONE"));
+    }
+
+    for(it = dict_first(cData->notes); it; it = iter_next(it))
+    {
+        int padding;
+
+        note = iter_data(it);
+        if(!note_type_visible_to_user(cData, note->type, user))
+            continue;
+
+        padding = PADLEN - 1 - strlen(iter_key(it));
+        reply("CSMSG_CHANNEL_NOTE", iter_key(it), padding > 0 ? padding : 1, "", note->note);
+    }
+
+    reply("CSMSG_CHANNEL_MAX", cData->max);
+    for(owner = cData->users; owner; owner = owner->next)
+        if(owner->access == UL_OWNER)
+            reply("CSMSG_CHANNEL_OWNER", owner->handle->handle);
+    reply("CSMSG_CHANNEL_USERS", cData->userCount);
+    reply("CSMSG_CHANNEL_BANS", cData->banCount);
+    reply("CSMSG_CHANNEL_VISITED", intervalString(buffer, now - cData->visited));
+    reply("CSMSG_CHANNEL_REGISTERED", intervalString(buffer, now - cData->registered));
+
+    privileged = IsStaff(user);
+    if(((uData && uData->access >= UL_COOWNER) || privileged) && cData->registrar)
+        reply("CSMSG_CHANNEL_REGISTRAR", cData->registrar);
+
+    if(privileged && (dnr = chanserv_is_dnr(channel->name, NULL)))
+        chanserv_show_dnrs(user, cmd, channel->name, NULL);
+
+    if(cData->suspended && ((uData && (uData->access >= UL_COOWNER)) || IsHelping(user)))
+    {
+        struct suspended *suspended;
+        reply((IsSuspended(cData) ? "CSMSG_CHANNEL_SUSPENDED" : "CSMSG_CHANNEL_HISTORY"), channel->name);
+        for(suspended = cData->suspended; suspended; suspended = suspended->previous)
+            show_suspension_info(cmd, user, suspended);
+    }
+    else if(IsSuspended(cData))
+    {
+        reply("CSMSG_CHANNEL_SUSPENDED", channel->name);
+        show_suspension_info(cmd, user, cData->suspended);
+    }
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_netinfo)
+{
+    extern time_t boot_time;
+    extern unsigned long burst_length;
+    char interval[INTERVALLEN];
+
+    reply("CSMSG_NETWORK_INFO");
+    reply("CSMSG_NETWORK_SERVERS", dict_size(servers));
+    reply("CSMSG_NETWORK_USERS", dict_size(clients));
+    reply("CSMSG_NETWORK_OPERS", curr_opers.used);
+    reply("CSMSG_NETWORK_CHANNELS", registered_channels);
+    reply("CSMSG_NETWORK_BANS", banCount);
+    reply("CSMSG_CHANNEL_USERS", userCount);
+    reply("CSMSG_SERVICES_UPTIME", intervalString(interval, time(NULL) - boot_time));
+    reply("CSMSG_BURST_LENGTH",intervalString(interval, burst_length));
+    return 1;
+}
+
+static void
+send_staff_list(struct userNode *to, struct userList *list, int skip_flags)
+{
+    struct helpfile_table table;
+    unsigned int nn;
+    struct userNode *user;
+    char *nick;
+
+    table.length = 0;
+    table.width = 1;
+    table.flags = TABLE_REPEAT_ROWS | TABLE_NO_FREE | TABLE_NO_HEADERS;
+    table.contents = alloca(list->used*sizeof(*table.contents));
+    for(nn=0; nn<list->used; nn++)
+    {
+        user = list->list[nn];
+        if(user->modes & skip_flags)
+            continue;
+        if(IsBot(user))
+            continue;
+        table.contents[table.length] = alloca(table.width*sizeof(**table.contents));
+        if(IsAway(user))
+        {
+            nick = alloca(strlen(user->nick)+3);
+            sprintf(nick, "(%s)", user->nick);
+        }
+        else
+            nick = user->nick;
+        table.contents[table.length][0] = nick;
+        table.length++;
+    }
+    table_send(chanserv, to->nick, 0, NULL, table);
+}
+
+static CHANSERV_FUNC(cmd_ircops)
+{
+    reply("CSMSG_STAFF_OPERS");
+    send_staff_list(user, &curr_opers, FLAGS_SERVICE);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_helpers)
+{
+    reply("CSMSG_STAFF_HELPERS");
+    send_staff_list(user, &curr_helpers, FLAGS_OPER);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_staff)
+{
+    reply("CSMSG_NETWORK_STAFF");
+    cmd_ircops(CSFUNC_ARGS);
+    cmd_helpers(CSFUNC_ARGS);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_peek)
+{
+    struct modeNode *mn;
+    char modes[MODELEN];
+    unsigned int n;
+    struct helpfile_table table;
+
+    irc_make_chanmode(channel, modes);
+
+    reply("CSMSG_PEEK_INFO", channel->name);
+    reply("CSMSG_PEEK_TOPIC", channel->topic);
+    reply("CSMSG_PEEK_MODES", modes);
+    reply("CSMSG_PEEK_USERS", channel->members.used);
+
+    table.length = 0;
+    table.width = 1;
+    table.flags = TABLE_REPEAT_ROWS | TABLE_NO_FREE | TABLE_NO_HEADERS;
+    table.contents = alloca(channel->members.used*sizeof(*table.contents));
+    for(n = 0; n < channel->members.used; n++)
+    {
+       mn = channel->members.list[n];
+       if(!(mn->modes & MODE_CHANOP) || IsLocal(mn->user))
+            continue;
+        table.contents[table.length] = alloca(sizeof(**table.contents));
+        table.contents[table.length][0] = mn->user->nick;
+        table.length++;
+    }
+    if(table.length)
+    {
+        reply("CSMSG_PEEK_OPS");
+        table_send(chanserv, user->nick, 0, NULL, table);
+    }
+    else
+        reply("CSMSG_PEEK_NO_OPS");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_wipeinfo)
+{
+    struct handle_info *victim;
+    struct userData *ud, *actor;
+
+    REQUIRE_PARAMS(2);
+    actor = GetChannelUser(channel->channel_info, user->handle_info);
+    if(!(victim = modcmd_get_handle_info(user, argv[1])))
+        return 0;
+    if(!(ud = GetTrueChannelAccess(channel->channel_info, victim)))
+    {
+        reply("CSMSG_NO_CHAN_USER", argv[1], channel->name);
+        return 0;
+    }
+    if((ud->access >= actor->access) && (ud != actor))
+    {
+        reply("MSG_USER_OUTRANKED", victim->handle);
+        return 0;
+    }
+    if(ud->info)
+        free(ud->info);
+    ud->info = NULL;
+    reply("CSMSG_WIPED_INFO_LINE", argv[1], channel->name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_resync)
+{
+    struct mod_chanmode *changes;
+    struct chanData *cData = channel->channel_info;
+    unsigned int ii, used;
+
+    changes = mod_chanmode_alloc(channel->members.used * 2);
+    for(ii = used = 0; ii < channel->members.used; ++ii)
+    {
+        struct modeNode *mn = channel->members.list[ii];
+        struct userData *uData;
+        if(IsService(mn->user))
+        {
+            /* must not change modes for this user */
+        }
+        else if(!(uData = GetChannelAccess(cData, mn->user->handle_info)))
+        {
+            if(mn->modes)
+            {
+                changes->args[used].mode = MODE_REMOVE | mn->modes;
+                changes->args[used++].member = mn;
+            }
+        }
+        else if(uData->access < cData->lvlOpts[lvlGiveOps])
+        {
+            if(mn->modes & MODE_CHANOP)
+            {
+                changes->args[used].mode = MODE_REMOVE | (mn->modes & ~MODE_VOICE);
+                changes->args[used++].member = mn;
+            }
+            if(!(mn->modes & MODE_VOICE))
+            {
+                changes->args[used].mode = MODE_VOICE;
+                changes->args[used++].member = mn;
+            }
+        }
+        else
+        {
+            if(!(mn->modes & MODE_CHANOP))
+            {
+                changes->args[used].mode = MODE_CHANOP;
+                changes->args[used++].member = mn;
+            }
+        }
+    }
+    changes->argc = used;
+    modcmd_chanmode_announce(changes);
+    mod_chanmode_free(changes);
+    reply("CSMSG_RESYNCED_USERS", channel->name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_seen)
+{
+    struct userData *uData;
+    struct handle_info *handle;
+    char seen[INTERVALLEN];
+
+    REQUIRE_PARAMS(2);
+
+    if(!irccasecmp(argv[1], chanserv->nick))
+    {
+       reply("CSMSG_IS_CHANSERV");
+       return 1;
+    }
+
+    if(!(handle = get_handle_info(argv[1])))
+    {
+       reply("MSG_HANDLE_UNKNOWN", argv[1]);
+       return 0;
+    }
+
+    if(!(uData = GetTrueChannelAccess(channel->channel_info, handle)))
+    {
+       reply("CSMSG_NO_CHAN_USER", handle->handle, channel->name);
+       return 0;
+    }
+
+    if(uData->present)
+       reply("CSMSG_USER_PRESENT", handle->handle);
+    else if(uData->seen)
+        reply("CSMSG_USER_SEEN", handle->handle, channel->name, intervalString(seen, now - uData->seen));
+    else
+        reply("CSMSG_NEVER_SEEN", handle->handle, channel->name);
+
+    if(!uData->present && HANDLE_FLAGGED(handle, FROZEN))
+        reply("CSMSG_USER_VACATION", handle->handle);
+
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_names)
+{
+    struct userNode *targ;
+    struct userData *targData;
+    unsigned int ii, pos;
+    char buf[400];
+
+    for(ii=pos=0; ii<channel->members.used; ++ii)
+    {
+        targ = channel->members.list[ii]->user;
+        targData = GetTrueChannelAccess(channel->channel_info, targ->handle_info);
+        if(!targData)
+            continue;
+        if(pos + strlen(targ->nick) + strlen(targ->handle_info->handle) + 6 > sizeof(buf))
+        {
+            buf[pos] = 0;
+            reply("CSMSG_CHANNEL_NAMES", channel->name, buf);
+            pos = 0;
+        }
+        buf[pos++] = ' ';
+        if(IsUserSuspended(targData))
+            buf[pos++] = 's';
+        pos += sprintf(buf+pos, "%d:%s(%s)", targData->access, targ->nick, targ->handle_info->handle);
+    }
+    buf[pos] = 0;
+    reply("CSMSG_CHANNEL_NAMES", channel->name, buf);
+    reply("CSMSG_END_NAMES", channel->name);
+    return 1;
+}
+
+static int
+note_type_visible_to_user(struct chanData *channel, struct note_type *ntype, struct userNode *user)
+{
+    switch(ntype->visible_type)
+    {
+    case NOTE_VIS_ALL: return 1;
+    case NOTE_VIS_CHANNEL_USERS: return !channel || !user || (user->handle_info && GetChannelUser(channel, user->handle_info));
+    case NOTE_VIS_PRIVILEGED: default: return user && (IsOper(user) || IsSupportHelper(user) || IsNetworkHelper(user));
+    }
+}
+
+static int
+note_type_settable_by_user(struct chanNode *channel, struct note_type *ntype, struct userNode *user)
+{
+    struct userData *uData;
+
+    switch(ntype->set_access_type)
+    {
+    case NOTE_SET_CHANNEL_ACCESS:
+        if(!user->handle_info)
+            return 0;
+        if(!(uData = GetChannelUser(channel->channel_info, user->handle_info)))
+            return 0;
+        return uData->access >= ntype->set_access.min_ulevel;
+    case NOTE_SET_CHANNEL_SETTER:
+        return check_user_level(channel, user, lvlSetters, 1, 0);
+    case NOTE_SET_PRIVILEGED: default:
+        return IsHelping(user);
+    }
+}
+
+static CHANSERV_FUNC(cmd_note)
+{
+    struct chanData *cData;
+    struct note *note;
+    struct note_type *ntype;
+
+    cData = channel->channel_info;
+    if(!cData)
+    {
+        reply("CSMSG_NOT_REGISTERED", channel->name);
+        return 0;
+    }
+
+    /* If no arguments, show all visible notes for the channel. */
+    if(argc < 2)
+    {
+        dict_iterator_t it;
+        unsigned int count;
+
+        for(count=0, it=dict_first(cData->notes); it; it=iter_next(it))
+        {
+            note = iter_data(it);
+            if(!note_type_visible_to_user(cData, note->type, user))
+                continue;
+            if(!count++)
+                reply("CSMSG_NOTELIST_HEADER", channel->name);
+            reply("CSMSG_NOTE_FORMAT", iter_key(it), note->setter, note->note);
+        }
+        if(count)
+            reply("CSMSG_NOTELIST_END", channel->name);
+        else
+            reply("CSMSG_NOTELIST_EMPTY", channel->name);
+    }
+    /* If one argument, show the named note. */
+    else if(argc == 2)
+    {
+        if((note = dict_find(cData->notes, argv[1], NULL))
+           && note_type_visible_to_user(cData, note->type, user))
+        {
+            reply("CSMSG_NOTE_FORMAT", note->type->name, note->setter, note->note);
+        }
+        else if((ntype = dict_find(note_types, argv[1], NULL))
+                && note_type_visible_to_user(NULL, ntype, user))
+        {
+            reply("CSMSG_NO_SUCH_NOTE", channel->name, ntype->name);
+            return 0;
+        }
+        else
+        {
+            reply("CSMSG_BAD_NOTE_TYPE", argv[1]);
+            return 0;
+        }
+    }
+    /* Assume they're trying to set a note. */
+    else
+    {
+        char *note_text;
+        ntype = dict_find(note_types, argv[1], NULL);
+        if(!ntype)
+        {
+            reply("CSMSG_BAD_NOTE_TYPE", argv[1]);
+            return 0;
+        }
+        else if(note_type_settable_by_user(channel, ntype, user))
+        {
+            note_text = unsplit_string(argv+2, argc-2, NULL);
+            if((note = dict_find(cData->notes, argv[1], NULL)))
+                reply("CSMSG_REPLACED_NOTE", ntype->name, channel->name, note->setter, note->note);
+            chanserv_add_channel_note(cData, ntype, user->handle_info->handle, note_text);
+            reply("CSMSG_NOTE_SET", ntype->name, channel->name);
+
+            if(ntype->visible_type == NOTE_VIS_PRIVILEGED)
+            {
+                /* The note is viewable to staff only, so return 0
+                   to keep the invocation from getting logged (or
+                   regular users can see it in !events). */
+                return 0;
+            }
+        }
+        else
+        {
+            reply("CSMSG_NO_ACCESS");
+            return 0;
+        }
+    }
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_delnote)
+{
+    struct note *note;
+
+    REQUIRE_PARAMS(2);
+    if(!(note = dict_find(channel->channel_info->notes, argv[1], NULL))
+       || !note_type_settable_by_user(channel, note->type, user))
+    {
+        reply("CSMSG_NO_SUCH_NOTE", channel->name, argv[1]);
+        return 0;
+    }
+    dict_remove(channel->channel_info->notes, note->type->name);
+    reply("CSMSG_NOTE_REMOVED", argv[1], channel->name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_events)
+{
+    struct logSearch discrim;
+    struct logReport report;
+    unsigned int matches, limit;
+
+    limit = (argc > 1) ? atoi(argv[1]) : 10;
+    if(limit < 1 || limit > 200) limit = 10;
+
+    memset(&discrim, 0, sizeof(discrim));
+    discrim.masks.bot = chanserv;
+    discrim.masks.channel_name = channel->name;
+    if(argc > 2) discrim.masks.command = argv[2];
+    discrim.limit = limit;
+    discrim.max_time = INT_MAX;
+    discrim.severities = 1 << LOG_COMMAND;
+    report.reporter = chanserv;
+    report.user = user;
+    reply("CSMSG_EVENT_SEARCH_RESULTS");
+    matches = log_entry_search(&discrim, log_report_entry, &report);
+    if(matches)
+       reply("MSG_MATCH_COUNT", matches);
+    else
+       reply("MSG_NO_MATCHES");
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_say)
+{
+    char *msg;
+    if(channel)
+    {
+        REQUIRE_PARAMS(2);
+        msg = unsplit_string(argv + 1, argc - 1, NULL);
+        send_channel_message(channel, cmd->parent->bot, "%s", msg);
+    }
+    else if(GetUserH(argv[1]))
+    {
+        REQUIRE_PARAMS(3);
+        msg = unsplit_string(argv + 2, argc - 2, NULL);
+        send_target_message(1, argv[1], cmd->parent->bot, "%s", msg);
+    }
+    else
+    {
+        reply("You must specify the name of a channel or user.");
+        return 0;
+    }
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_emote)
+{
+    char *msg;
+    assert(argc >= 2);
+    if(channel)
+    {
+        /* CTCP is so annoying. */
+        msg = unsplit_string(argv + 1, argc - 1, NULL);
+        send_channel_message(channel, cmd->parent->bot, "\001ACTION %s\001", msg);
+    }
+    else if(GetUserH(argv[1]))
+    {
+        msg = unsplit_string(argv + 2, argc - 2, NULL);
+        send_target_message(1, argv[1], cmd->parent->bot, "\001ACTION %s\001", msg);
+    }
+    else
+    {
+        reply("You must specify the name of a channel or user.");
+        return 0;
+    }
+    return 1;
+}
+
+struct channelList *
+chanserv_support_channels(void)
+{
+    return &chanserv_conf.support_channels;
+}
+
+static CHANSERV_FUNC(cmd_expire)
+{
+    int channel_count = registered_channels;
+    expire_channels(NULL);
+    reply("CSMSG_CHANNELS_EXPIRED", channel_count - registered_channels);
+    return 1;
+}
+
+static void
+chanserv_expire_suspension(void *data)
+{
+    struct suspended *suspended = data;
+    struct chanNode *channel;
+    struct mod_chanmode change;
+
+    if(!suspended->expires || (now < suspended->expires))
+        suspended->revoked = now;
+    channel = suspended->cData->channel;
+    suspended->cData->channel = channel;
+    suspended->cData->flags &= ~CHANNEL_SUSPENDED;
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    change.args[0].mode = MODE_CHANOP;
+    change.args[0].member = AddChannelUser(chanserv, channel);
+    mod_chanmode_announce(chanserv, channel, &change);
+}
+
+static CHANSERV_FUNC(cmd_csuspend)
+{
+    struct suspended *suspended;
+    char reason[MAXLEN];
+    time_t expiry, duration;
+    struct userData *uData;
+
+    REQUIRE_PARAMS(3);
+
+    if(IsProtected(channel->channel_info))
+    {
+        reply("CSMSG_SUSPEND_NODELETE", channel->name);
+        return 0;
+    }
+
+    if(argv[1][0] == '!')
+        argv[1]++;
+    else if(IsSuspended(channel->channel_info))
+    {
+       reply("CSMSG_ALREADY_SUSPENDED", channel->name);
+        show_suspension_info(cmd, user, channel->channel_info->suspended);
+       return 0;
+    }
+
+    if(!strcmp(argv[1], "0"))
+        expiry = 0;
+    else if((duration = ParseInterval(argv[1])))
+        expiry = now + duration;
+    else
+    {
+        reply("MSG_INVALID_DURATION", argv[1]);
+        return 0;
+    }
+
+    unsplit_string(argv + 2, argc - 2, reason);
+
+    suspended = calloc(1, sizeof(*suspended));
+    suspended->revoked = 0;
+    suspended->issued = now;
+    suspended->suspender = strdup(user->handle_info->handle);
+    suspended->expires = expiry;
+    suspended->reason = strdup(reason);
+    suspended->cData = channel->channel_info;
+    suspended->previous = suspended->cData->suspended;
+    suspended->cData->suspended = suspended;
+
+    if(suspended->expires)
+        timeq_add(suspended->expires, chanserv_expire_suspension, suspended);
+
+    if(IsSuspended(channel->channel_info))
+    {
+        suspended->previous->revoked = now;
+        if(suspended->previous->expires)
+            timeq_del(suspended->previous->expires, chanserv_expire_suspension, suspended->previous, 0);
+        sprintf(reason, "%s suspension modified by %s.", channel->name, suspended->suspender);
+        global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, reason);
+    }
+    else
+    {
+        /* Mark all users in channel as absent. */
+        for(uData = channel->channel_info->users; uData; uData = uData->next)
+        {
+            if(uData->present)
+            {
+                uData->seen = now;
+                uData->present = 0;
+            }
+        }
+
+        /* Mark the channel as suspended, then part. */
+        channel->channel_info->flags |= CHANNEL_SUSPENDED;
+        DelChannelUser(chanserv, channel, suspended->reason, 0);
+        reply("CSMSG_SUSPENDED", channel->name);
+        sprintf(reason, "%s suspended by %s.", channel->name, suspended->suspender);
+        global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, reason);
+    }
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_cunsuspend)
+{
+    struct suspended *suspended;
+    char message[MAXLEN];
+
+    if(!IsSuspended(channel->channel_info))
+    {
+        reply("CSMSG_NOT_SUSPENDED", channel->name);
+        return 0;
+    }
+
+    suspended = channel->channel_info->suspended;
+
+    /* Expire the suspension and join ChanServ to the channel. */
+    timeq_del(suspended->expires, chanserv_expire_suspension, suspended, 0);
+    chanserv_expire_suspension(suspended);
+    reply("CSMSG_UNSUSPENDED", channel->name);
+    sprintf(message, "%s unsuspended by %s.", channel->name, user->handle_info->handle);
+    global_message(MESSAGE_RECIPIENT_OPERS|MESSAGE_RECIPIENT_HELPERS, message);
+    return 1;
+}
+
+typedef struct chanservSearch
+{
+    char *name;
+    char *registrar;
+
+    time_t unvisited;
+    time_t registered;
+
+    unsigned long flags;
+    unsigned int limit;
+} *search_t;
+
+typedef void (*channel_search_func)(struct chanData *channel, void *data);
+
+static search_t
+chanserv_search_create(struct userNode *user, unsigned int argc, char *argv[])
+{
+    search_t search;
+    unsigned int i;
+
+    search = malloc(sizeof(struct chanservSearch));
+    memset(search, 0, sizeof(*search));
+    search->limit = 25;
+
+    for(i = 0; i < argc; i++)
+    {
+       /* Assume all criteria require arguments. */
+       if(i == (argc - 1))
+       {
+           send_message(user, chanserv, "MSG_MISSING_PARAMS", argv[i]);
+            goto fail;
+       }
+
+       if(!irccasecmp(argv[i], "name"))
+           search->name = argv[++i];
+       else if(!irccasecmp(argv[i], "registrar"))
+           search->registrar = argv[++i];
+       else if(!irccasecmp(argv[i], "unvisited"))
+           search->unvisited = ParseInterval(argv[++i]);
+       else if(!irccasecmp(argv[i], "registered"))
+           search->registered = ParseInterval(argv[++i]);
+       else if(!irccasecmp(argv[i], "flags"))
+       {
+           i++;
+           if(!irccasecmp(argv[i], "nodelete"))
+               search->flags |= CHANNEL_NODELETE;
+           else if(!irccasecmp(argv[i], "suspended"))
+               search->flags |= CHANNEL_SUSPENDED;
+           else
+           {
+               send_message(user, chanserv, "CSMSG_INVALID_CFLAG", argv[i]);
+               goto fail;
+           }
+       }
+       else if(!irccasecmp(argv[i], "limit"))
+           search->limit = strtoul(argv[++i], NULL, 10);
+       else
+       {
+           send_message(user, chanserv, "MSG_INVALID_CRITERIA", argv[i]);
+           goto fail;
+       }
+    }
+
+    if(search->name && !strcmp(search->name, "*"))
+       search->name = 0;
+    if(search->registrar && !strcmp(search->registrar, "*"))
+       search->registrar = 0;
+
+    return search;
+  fail:
+    free(search);
+    return NULL;
+}
+
+static int
+chanserv_channel_match(struct chanData *channel, search_t search)
+{
+    const char *name = channel->channel->name;
+    if((search->name && !match_ircglob(name, search->name)) ||
+       (search->registrar && !channel->registrar) ||
+       (search->registrar && !match_ircglob(channel->registrar, search->registrar)) ||
+       (search->unvisited && (now - channel->visited) < search->unvisited) ||
+       (search->registered && (now - channel->registered) > search->registered) ||
+       (search->flags && ((search->flags & channel->flags) != search->flags)))
+       return 0;
+
+    return 1;
+}
+
+static unsigned int
+chanserv_channel_search(search_t search, channel_search_func smf, void *data)
+{
+    struct chanData *channel;
+    unsigned int matches = 0;
+
+    for(channel = channelList; channel && matches < search->limit; channel = channel->next)
+    {
+       if(!chanserv_channel_match(channel, search))
+            continue;
+       matches++;
+       smf(channel, data);
+    }
+
+    return matches;
+}
+
+static void
+search_count(UNUSED_ARG(struct chanData *channel), UNUSED_ARG(void *data))
+{
+}
+
+static void
+search_print(struct chanData *channel, void *data)
+{
+    send_message_type(4, data, chanserv, "%s", channel->channel->name);
+}
+
+static CHANSERV_FUNC(cmd_search)
+{
+    search_t search;
+    unsigned int matches;
+    channel_search_func action;
+
+    REQUIRE_PARAMS(3);
+
+    if(!irccasecmp(argv[1], "count"))
+       action = search_count;
+    else if(!irccasecmp(argv[1], "print"))
+       action = search_print;
+    else
+    {
+       reply("CSMSG_ACTION_INVALID", argv[1]);
+       return 0;
+    }
+
+    search = chanserv_search_create(user, argc - 2, argv + 2);
+    if(!search)
+        return 0;
+
+    if(action == search_count)
+       search->limit = INT_MAX;
+
+    if(action == search_print)
+       reply("CSMSG_CHANNEL_SEARCH_RESULTS");
+
+    matches = chanserv_channel_search(search, action, user);
+
+    if(matches)
+       reply("MSG_MATCH_COUNT", matches);
+    else
+       reply("MSG_NO_MATCHES");
+
+    free(search);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_unvisited)
+{
+    struct chanData *cData;
+    time_t interval = chanserv_conf.channel_expire_delay;
+    char buffer[INTERVALLEN];
+    unsigned int limit = 25, matches = 0;
+
+    if(argc > 1)
+    {
+       interval = ParseInterval(argv[1]);
+       if(argc > 2)
+            limit = atoi(argv[2]);
+    }
+
+    intervalString(buffer, interval);
+    reply("CSMSG_UNVISITED_HEADER", limit, buffer);
+
+    for(cData = channelList; cData && matches < limit; cData = cData->next)
+    {
+       if((now - cData->visited) < interval)
+            continue;
+
+       intervalString(buffer, now - cData->visited);
+       reply("CSMSG_UNVISITED_DATA", cData->channel->name, buffer);
+       matches++;
+    }
+
+    return 1;
+}
+
+static MODCMD_FUNC(chan_opt_defaulttopic)
+{
+    if(argc > 1)
+    {
+        char *topic;
+
+        if(!check_user_level(channel, user, lvlEnfTopic, 1, 0))
+        {
+            reply("CSMSG_TOPIC_LOCKED", channel->name);
+            return 0;
+        }
+
+       topic = unsplit_string(argv+1, argc-1, NULL);
+
+        free(channel->channel_info->topic);
+       if(topic[0] == '*' && topic[1] == 0)
+       {
+            topic = channel->channel_info->topic = NULL;
+       }
+       else
+       {
+           topic = channel->channel_info->topic = strdup(topic);
+            if(channel->channel_info->topic_mask
+               && !match_ircglob(channel->channel_info->topic, channel->channel_info->topic_mask))
+                reply("CSMSG_TOPIC_MISMATCH", channel->name);
+       }
+        SetChannelTopic(channel, chanserv, topic ? topic : "", 1);
+    }
+
+    if(channel->channel_info->topic)
+        reply("CSMSG_SET_DEFAULT_TOPIC", channel->channel_info->topic);
+    else
+        reply("CSMSG_SET_DEFAULT_TOPIC", user_find_message(user, "MSG_NONE"));
+    return 1;
+}
+
+static MODCMD_FUNC(chan_opt_topicmask)
+{
+    if(argc > 1)
+    {
+        struct chanData *cData = channel->channel_info;
+        char *mask;
+
+        if(!check_user_level(channel, user, lvlEnfTopic, 1, 0))
+        {
+            reply("CSMSG_TOPIC_LOCKED", channel->name);
+            return 0;
+        }
+
+       mask = unsplit_string(argv+1, argc-1, NULL);
+
+        if(cData->topic_mask)
+            free(cData->topic_mask);
+       if(mask[0] == '*' && mask[1] == 0)
+       {
+           cData->topic_mask = 0;
+       }
+       else
+       {
+            cData->topic_mask = strdup(mask);
+            if(!cData->topic)
+                reply("CSMSG_MASK_BUT_NO_TOPIC", channel->name);
+            else if(!match_ircglob(cData->topic, cData->topic_mask))
+                reply("CSMSG_TOPIC_MISMATCH", channel->name);
+       }
+    }
+
+    if(channel->channel_info->topic_mask)
+        reply("CSMSG_SET_TOPICMASK", channel->channel_info->topic_mask);
+    else
+        reply("CSMSG_SET_TOPICMASK", user_find_message(user, "MSG_NONE"));
+    return 1;
+}
+
+int opt_greeting_common(struct userNode *user, struct svccmd *cmd, int argc, char *argv[], char *name, char **data)
+{
+    if(argc > 1)
+    {
+        char *greeting = unsplit_string(argv+1, argc-1, NULL);
+        char *previous;
+
+        previous = *data;
+       if(greeting[0] == '*' && greeting[1] == 0)
+           *data = NULL;
+       else
+       {
+           unsigned int length = strlen(greeting);
+           if(length > chanserv_conf.greeting_length)
+           {
+               reply("CSMSG_GREETING_TOO_LONG", length, chanserv_conf.greeting_length);
+               return 0;
+           }
+           *data = strdup(greeting);
+       }
+        if(previous)
+            free(previous);
+    }
+
+    if(*data)
+        reply(name, *data);
+    else
+        reply(name, user_find_message(user, "MSG_NONE"));
+    return 1;
+}
+
+static MODCMD_FUNC(chan_opt_greeting)
+{
+    return opt_greeting_common(user, cmd, argc, argv, "CSMSG_SET_GREETING", &channel->channel_info->greeting);
+}
+
+static MODCMD_FUNC(chan_opt_usergreeting)
+{
+    return opt_greeting_common(user, cmd, argc, argv, "CSMSG_SET_USERGREETING", &channel->channel_info->user_greeting);
+}
+
+static MODCMD_FUNC(chan_opt_modes)
+{
+    struct mod_chanmode *new_modes;
+    char modes[MODELEN];
+
+    if(argc > 1)
+    {
+        if(!check_user_level(channel, user, lvlEnfModes, 1, 0))
+        {
+            reply("CSMSG_NO_ACCESS");
+            return 0;
+        }
+       if(argv[1][0] == '*' && argv[1][1] == 0)
+       {
+            memset(&channel->channel_info->modes, 0, sizeof(channel->channel_info->modes));
+       }
+       else if(!(new_modes = mod_chanmode_parse(channel, argv+1, argc-1, MCP_KEY_FREE)))
+       {
+            reply("CSMSG_INVALID_MODE_LOCK", unsplit_string(argv+1, argc-1, NULL));
+            return 0;
+        }
+        else if(new_modes->argc > 1)
+        {
+            reply("CSMSG_INVALID_MODE_LOCK", unsplit_string(argv+1, argc-1, NULL));
+            mod_chanmode_free(new_modes);
+            return 0;
+        }
+        else
+        {
+            channel->channel_info->modes = *new_modes;
+            modcmd_chanmode_announce(new_modes);
+            mod_chanmode_free(new_modes);
+        }
+    }
+
+    mod_chanmode_format(&channel->channel_info->modes, modes);
+    if(modes[0])
+        reply("CSMSG_SET_MODES", modes);
+    else
+        reply("CSMSG_SET_MODES", user_find_message(user, "MSG_NONE"));
+    return 1;
+}
+
+#define CHANNEL_BINARY_OPTION(MSG, FLAG) return channel_binary_option(MSG, FLAG, CSFUNC_ARGS);
+static int
+channel_binary_option(char *name, unsigned long mask, struct userNode *user, struct chanNode *channel, int argc, char *argv[], struct svccmd *cmd)
+{
+    struct chanData *cData = channel->channel_info;
+    int value;
+
+    if(argc > 1)
+    {
+       /* Set flag according to value. */
+       if(enabled_string(argv[1]))
+       {
+           cData->flags |= mask;
+           value = 1;
+       }
+       else if(disabled_string(argv[1]))
+       {
+           cData->flags &= ~mask;
+           value = 0;
+       }
+       else
+       {
+           reply("MSG_INVALID_BINARY", argv[1]);
+           return 0;
+       }
+    }
+    else
+    {
+       /* Find current option value. */
+       value = (cData->flags & mask) ? 1 : 0;
+    }
+
+    if(value)
+        reply(name, user_find_message(user, "MSG_ON"));
+    else
+        reply(name, user_find_message(user, "MSG_OFF"));
+    return 1;
+}
+
+static MODCMD_FUNC(chan_opt_nodelete)
+{
+    if((argc > 1) && (!IsOper(user) || !user->handle_info || (user->handle_info->opserv_level < chanserv_conf.nodelete_level)))
+    {
+       reply("MSG_SETTING_PRIVILEGED", argv[0]);
+       return 0;
+    }
+
+    CHANNEL_BINARY_OPTION("CSMSG_SET_NODELETE", CHANNEL_NODELETE);
+}
+
+static MODCMD_FUNC(chan_opt_userinfo)
+{
+    CHANNEL_BINARY_OPTION("CSMSG_SET_USERINFO", CHANNEL_INFO_LINES);
+}
+
+static MODCMD_FUNC(chan_opt_voice)
+{
+    CHANNEL_BINARY_OPTION("CSMSG_SET_VOICE", CHANNEL_VOICE_ALL);
+}
+
+static MODCMD_FUNC(chan_opt_dynlimit)
+{
+    CHANNEL_BINARY_OPTION("CSMSG_SET_DYNLIMIT", CHANNEL_DYNAMIC_LIMIT);
+}
+
+static MODCMD_FUNC(chan_opt_topicsnarf)
+{
+    if((argc > 0) && !check_user_level(channel, user, lvlEnfTopic, 1, 0))
+    {
+        reply("CSMSG_TOPIC_LOCKED", channel->name);
+        return 0;
+    }
+    CHANNEL_BINARY_OPTION("CSMSG_SET_TOPICSNARF", CHANNEL_TOPIC_SNARF);
+}
+
+static MODCMD_FUNC(chan_opt_peoninvite)
+{
+    CHANNEL_BINARY_OPTION("CSMSG_SET_PEONINVITE", CHANNEL_PEON_INVITE);
+}
+
+static MODCMD_FUNC(chan_opt_defaults)
+{
+    struct userData *uData;
+    struct chanData *cData;
+    const char *confirm;
+    enum levelOption lvlOpt;
+    enum charOption chOpt;
+
+    cData = channel->channel_info;
+    uData = GetChannelUser(cData, user->handle_info);
+    if(!uData || (uData->access < UL_OWNER))
+    {
+        reply("CSMSG_OWNER_DEFAULTS", channel->name);
+        return 0;
+    }
+    confirm = make_confirmation_string(uData);
+    if((argc < 2) || strcmp(argv[1], confirm))
+    {
+        reply("CSMSG_CONFIRM_DEFAULTS", channel->name, confirm);
+        return 0;
+    }
+    cData->flags = CHANNEL_DEFAULT_FLAGS;
+    cData->modes = chanserv_conf.default_modes;
+    for(lvlOpt = 0; lvlOpt < NUM_LEVEL_OPTIONS; ++lvlOpt)
+        cData->lvlOpts[lvlOpt] = levelOptions[lvlOpt].default_value;
+    for(chOpt = 0; chOpt < NUM_CHAR_OPTIONS; ++chOpt)
+        cData->chOpts[chOpt] = charOptions[chOpt].default_value;
+    reply("CSMSG_SETTINGS_DEFAULTED", channel->name);
+    return 1;
+}
+
+static int
+channel_level_option(enum levelOption option, struct userNode *user, struct chanNode *channel, int argc, char *argv[], struct svccmd *cmd)
+{
+    struct chanData *cData = channel->channel_info;
+    struct userData *uData;
+    unsigned short value;
+
+    if(argc > 1)
+    {
+        if(!check_user_level(channel, user, option, 1, 1))
+        {
+            reply("CSMSG_CANNOT_SET");
+            return 0;
+        }
+        value = user_level_from_name(argv[1], UL_OWNER+1);
+        if(!value && !isdigit(argv[1][0]))
+       {
+           reply("CSMSG_INVALID_ACCESS", index);
+            return 0;
+        }
+        uData = GetChannelUser(cData, user->handle_info);
+        if(!uData || (uData->access < value))
+        {
+            reply("CSMSG_BAD_SETLEVEL");
+            return 0;
+        }
+        cData->lvlOpts[option] = value;
+    }
+    reply(levelOptions[option].format_name, cData->lvlOpts[option]);
+    return argc > 1;
+}
+
+static MODCMD_FUNC(chan_opt_enfops)
+{
+    return channel_level_option(lvlEnfOps, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_giveops)
+{
+    return channel_level_option(lvlGiveOps, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_enfmodes)
+{
+    return channel_level_option(lvlEnfModes, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_enftopic)
+{
+    return channel_level_option(lvlEnfTopic, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_pubcmd)
+{
+    return channel_level_option(lvlPubCmd, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_setters)
+{
+    return channel_level_option(lvlSetters, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_ctcpusers)
+{
+    return channel_level_option(lvlCTCPUsers, CSFUNC_ARGS);
+}
+
+static int
+channel_multiple_option(enum charOption option, struct userNode *user, struct chanNode *channel, int argc, char *argv[], struct svccmd *cmd)
+{
+    struct chanData *cData = channel->channel_info;
+    int count = charOptions[option].count, index;
+
+    if(argc > 1)
+    {
+        index = atoi(argv[1]);
+
+       if(!isdigit(argv[1][0]) || (index < 0) || (index >= count))
+       {
+           reply("CSMSG_INVALID_NUMERIC", index);
+            /* Show possible values. */
+            for(index = 0; index < count; index++)
+                reply(charOptions[option].format_name, index, user_find_message(user, charOptions[option].values[index].format_name));
+           return 0;
+       }
+
+       cData->chOpts[option] = charOptions[option].values[index].value;
+    }
+    else
+    {
+       /* Find current option value. */
+      find_value:
+       for(index = 0;
+            (index < count) && (cData->chOpts[option] != charOptions[option].values[index].value);
+            index++);
+        if(index == count)
+        {
+            /* Somehow, the option value is corrupt; reset it to the default. */
+            cData->chOpts[option] = charOptions[option].default_value;
+            goto find_value;
+        }
+    }
+
+    reply(charOptions[option].format_name, index, user_find_message(user, charOptions[option].values[index].format_name));
+    return 1;
+}
+
+static MODCMD_FUNC(chan_opt_protect)
+{
+    return channel_multiple_option(chProtect, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_toys)
+{
+    return channel_multiple_option(chToys, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_ctcpreaction)
+{
+    return channel_multiple_option(chCTCPReaction, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_topicrefresh)
+{
+    return channel_multiple_option(chTopicRefresh, CSFUNC_ARGS);
+}
+
+static struct svccmd_list set_shows_list;
+
+static void
+handle_svccmd_unbind(struct svccmd *target) {
+    unsigned int ii;
+    for(ii=0; ii<set_shows_list.used; ++ii)
+        if(target == set_shows_list.list[ii])
+            set_shows_list.used = 0;
+}
+
+static CHANSERV_FUNC(cmd_set)
+{
+    struct svccmd *subcmd;
+    char buf[MAXLEN];
+    unsigned int ii;
+
+    /* Check if we need to (re-)initialize set_shows_list. */
+    if(!set_shows_list.used)
+    {
+        if(!set_shows_list.size)
+        {
+            set_shows_list.size = chanserv_conf.set_shows->used;
+            set_shows_list.list = calloc(set_shows_list.size, sizeof(set_shows_list.list[0]));
+        }
+        for(ii = 0; ii < chanserv_conf.set_shows->used; ii++)
+        {
+            const char *name = chanserv_conf.set_shows->list[ii];
+            sprintf(buf, "%s %s", argv[0], name);
+            subcmd = dict_find(cmd->parent->commands, buf, NULL);
+            if(!subcmd)
+            {
+                log_module(CS_LOG, LOG_ERROR, "Unable to find set option \"%s\".", name);
+                continue;
+            }
+            svccmd_list_append(&set_shows_list, subcmd);
+        }
+    }
+
+    if(argc < 2)
+    {
+       reply("CSMSG_CHANNEL_OPTIONS");
+        for(ii = 0; ii < set_shows_list.used; ii++)
+        {
+            subcmd = set_shows_list.list[ii];
+            subcmd->command->func(user, channel, 1, argv+1, subcmd);
+        }
+       return 1;
+    }
+
+    sprintf(buf, "%s %s", argv[0], argv[1]);
+    subcmd = dict_find(cmd->parent->commands, buf, NULL);
+    if(!subcmd)
+    {
+        reply("CSMSG_INVALID_OPTION", argv[1], argv[0]);
+        return 0;
+    }
+    if((argc > 2) && !check_user_level(channel, user, lvlSetters, 1, 0))
+    {
+        reply("CSMSG_NO_ACCESS");
+        return 0;
+    }
+
+    return subcmd->command->func(user, channel, argc - 1, argv + 1, subcmd);
+}
+
+static int
+user_binary_option(char *name, unsigned long mask, struct userNode *user, struct chanNode *channel, int argc, char *argv[], struct svccmd *cmd)
+{
+    struct userData *uData;
+
+    uData = GetChannelAccess(channel->channel_info, user->handle_info);
+    if(!uData)
+    {
+        reply("CSMSG_NOT_USER", channel->name);
+        return 0;
+    }
+
+    if(argc < 2)
+    {
+       /* Just show current option value. */
+    }
+    else if(enabled_string(argv[1]))
+    {
+        uData->flags |= mask;
+    }
+    else if(disabled_string(argv[1]))
+    {
+        uData->flags &= ~mask;
+    }
+    else
+    {
+        reply("MSG_INVALID_BINARY", argv[1]);
+        return 0;
+    }
+
+    reply(name, user_find_message(user, (uData->flags & mask) ? "MSG_ON" : "MSG_OFF"));
+    return 1;
+}
+
+static MODCMD_FUNC(user_opt_noautoop)
+{
+    struct userData *uData;
+
+    uData = GetChannelAccess(channel->channel_info, user->handle_info);
+    if(!uData)
+    {
+        reply("CSMSG_NOT_USER", channel->name);
+        return 0;
+    }
+    if(uData->access < channel->channel_info->lvlOpts[lvlGiveOps])
+        return user_binary_option("CSMSG_USET_NOAUTOVOICE", USER_AUTO_OP, CSFUNC_ARGS);
+    else
+        return user_binary_option("CSMSG_USET_NOAUTOOP", USER_AUTO_OP, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(user_opt_autoinvite)
+{
+    return user_binary_option("CSMSG_USET_AUTOINVITE", USER_AUTO_INVITE, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(user_opt_info)
+{
+    struct userData *uData;
+    char *infoline;
+
+    uData = GetChannelAccess(channel->channel_info, user->handle_info);
+
+    if(!uData)
+    {
+       /* If they got past the command restrictions (which require access)
+         * but fail this test, we have some fool with security override on.
+         */
+       reply("CSMSG_NOT_USER", channel->name);
+       return 0;
+    }
+
+    if(argc > 1)
+    {
+        infoline = unsplit_string(argv + 1, argc - 1, NULL);
+        if(uData->info)
+            free(uData->info);
+        if(infoline[0] == '*' && infoline[1] == 0)
+            uData->info = NULL;
+        else
+            uData->info = strdup(infoline);
+    }
+    if(uData->info)
+        reply("CSMSG_USET_INFO", uData->info);
+    else
+        reply("CSMSG_USET_INFO", user_find_message(user, "MSG_NONE"));
+    return 1;
+}
+
+struct svccmd_list uset_shows_list;
+
+static CHANSERV_FUNC(cmd_uset)
+{
+    struct svccmd *subcmd;
+    char buf[MAXLEN];
+    unsigned int ii;
+
+    /* Check if we need to (re-)initialize uset_shows_list. */
+    if(!uset_shows_list.used)
+    {
+        char *options[] =
+        {
+            "NoAutoOp", "AutoInvite", "Info"
+        };
+
+        if(!uset_shows_list.size)
+        {
+            uset_shows_list.size = ArrayLength(options);
+            uset_shows_list.list = calloc(uset_shows_list.size, sizeof(uset_shows_list.list[0]));
+        }
+        for(ii = 0; ii < ArrayLength(options); ii++)
+        {
+            const char *name = options[ii];
+            sprintf(buf, "%s %s", argv[0], name);
+            subcmd = dict_find(cmd->parent->commands, buf, NULL);
+            if(!subcmd)
+            {
+                log_module(CS_LOG, LOG_ERROR, "Unable to find uset option %s.", name);
+                continue;
+            }
+            svccmd_list_append(&uset_shows_list, subcmd);
+        }
+    }
+
+    if(argc < 2)
+    {
+       /* Do this so options are presented in a consistent order. */
+       reply("CSMSG_USER_OPTIONS");
+        for(ii = 0; ii < uset_shows_list.used; ii++)
+            uset_shows_list.list[ii]->command->func(user, channel, 1, argv+1, uset_shows_list.list[ii]);
+       return 1;
+    }
+
+    sprintf(buf, "%s %s", argv[0], argv[1]);
+    subcmd = dict_find(cmd->parent->commands, buf, NULL);
+    if(!subcmd)
+    {
+        reply("CSMSG_INVALID_OPTION", argv[1], argv[0]);
+        return 0;
+    }
+
+    return subcmd->command->func(user, channel, argc - 1, argv + 1, subcmd);
+}
+
+static CHANSERV_FUNC(cmd_giveownership)
+{
+    struct handle_info *new_owner_hi;
+    struct userData *new_owner, *curr_user;
+    struct chanData *cData = channel->channel_info;
+    struct do_not_register *dnr;
+    unsigned int force;
+    unsigned short co_access;
+    char reason[MAXLEN];
+
+    REQUIRE_PARAMS(2);
+    curr_user = GetChannelAccess(cData, user->handle_info);
+    force = IsHelping(user) && (argc > 2) && !irccasecmp(argv[2], "force");
+    if(!curr_user || (curr_user->access != UL_OWNER))
+    {
+        struct userData *owner = NULL;
+        for(curr_user = channel->channel_info->users;
+            curr_user;
+            curr_user = curr_user->next)
+        {
+            if(curr_user->access != UL_OWNER)
+                continue;
+            if(owner)
+            {
+                reply("CSMSG_MULTIPLE_OWNERS", channel->name);
+                return 0;
+            }
+            owner = curr_user;
+        }
+    }
+    if(!(new_owner_hi = modcmd_get_handle_info(user, argv[1])))
+        return 0;
+    if(new_owner_hi == user->handle_info)
+    {
+        reply("CSMSG_NO_TRANSFER_SELF");
+        return 0;
+    }
+    new_owner = GetChannelAccess(cData, new_owner_hi);
+    if(!new_owner)
+    {
+        reply("CSMSG_NO_CHAN_USER", new_owner_hi->handle, channel->name);
+        return 0;
+    }
+    if((chanserv_get_owned_count(new_owner_hi) >= chanserv_conf.max_owned) && !force)
+    {
+        reply("CSMSG_OWN_TOO_MANY", new_owner_hi->handle, chanserv_conf.max_owned);
+        return 0;
+    }
+    if((dnr = chanserv_is_dnr(NULL, new_owner_hi)) && !force) {
+        if(!IsHelping(user))
+            reply("CSMSG_DNR_ACCOUNT", new_owner_hi->handle);
+        else
+            chanserv_show_dnrs(user, cmd, NULL, new_owner_hi);
+        return 0;
+    }
+    if(new_owner->access >= UL_COOWNER)
+        co_access = new_owner->access;
+    else
+        co_access = UL_COOWNER;
+    new_owner->access = UL_OWNER;
+    if(curr_user)
+        curr_user->access = co_access;
+    reply("CSMSG_OWNERSHIP_GIVEN", channel->name, new_owner_hi->handle);
+    sprintf(reason, "%s ownership transferred to %s by %s.", channel->name, new_owner_hi->handle, user->handle_info->handle);
+    global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, reason);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_suspend)
+{
+    struct handle_info *hi;
+    struct userData *self, *target;
+
+    REQUIRE_PARAMS(2);
+    if(!(hi = modcmd_get_handle_info(user, argv[1]))) return 0;
+    self = GetChannelUser(channel->channel_info, user->handle_info);
+    if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
+    {
+        reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
+        return 0;
+    }
+    if(target->access >= self->access)
+    {
+        reply("MSG_USER_OUTRANKED", hi->handle);
+        return 0;
+    }
+    if(target->flags & USER_SUSPENDED)
+    {
+        reply("CSMSG_ALREADY_SUSPENDED", hi->handle);
+        return 0;
+    }
+    if(target->present)
+    {
+        target->present = 0;
+        target->seen = now;
+    }
+    target->flags |= USER_SUSPENDED;
+    reply("CSMSG_USER_SUSPENDED", hi->handle, channel->name);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_unsuspend)
+{
+    struct handle_info *hi;
+    struct userData *self, *target;
+
+    REQUIRE_PARAMS(2);
+    if(!(hi = modcmd_get_handle_info(user, argv[1]))) return 0;
+    self = GetChannelUser(channel->channel_info, user->handle_info);
+    if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
+    {
+        reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
+        return 0;
+    }
+    if(target->access >= self->access)
+    {
+        reply("MSG_USER_OUTRANKED", hi->handle);
+        return 0;
+    }
+    if(!(target->flags & USER_SUSPENDED))
+    {
+        reply("CSMSG_NOT_SUSPENDED", hi->handle);
+        return 0;
+    }
+    target->flags &= ~USER_SUSPENDED;
+    reply("CSMSG_USER_UNSUSPENDED", hi->handle, channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_deleteme)
+{
+    struct handle_info *hi;
+    struct userData *target;
+    const char *confirm_string;
+    unsigned short access;
+    char *channel_name;
+
+    hi = user->handle_info;
+    if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
+    {
+        reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
+        return 0;
+    }
+    if(target->access == UL_OWNER)
+    {
+        reply("CSMSG_NO_OWNER_DELETEME", channel->name);
+        return 0;
+    }
+    confirm_string = make_confirmation_string(target);
+    if((argc < 2) || strcmp(argv[1], confirm_string))
+    {
+        reply("CSMSG_CONFIRM_DELETEME", confirm_string);
+        return 0;
+    }
+    access = target->access;
+    channel_name = strdup(channel->name);
+    del_channel_user(target, 1);
+    reply("CSMSG_DELETED_YOU", access, channel_name);
+    free(channel_name);
+    return 1;
+}
+
+static void
+chanserv_refresh_topics(UNUSED_ARG(void *data))
+{
+    unsigned int refresh_num = (now - self->link) / chanserv_conf.refresh_period;
+    struct chanData *cData;
+    char opt;
+
+    for(cData = channelList; cData; cData = cData->next)
+    {
+        if(IsSuspended(cData))
+            continue;
+        opt = cData->chOpts[chTopicRefresh];
+        if(opt == 'n')
+            continue;
+        if((refresh_num - cData->last_refresh) < (unsigned int)(1 << (opt - '1')))
+            continue;
+        if(cData->topic)
+            SetChannelTopic(cData->channel, chanserv, cData->topic, 1);
+        cData->last_refresh = refresh_num;
+    }
+    timeq_add(now + chanserv_conf.refresh_period, chanserv_refresh_topics, NULL);
+}
+
+static CHANSERV_FUNC(cmd_unf)
+{
+    if(channel)
+    {
+        char response[MAXLEN];
+        const char *fmt = user_find_message(user, "CSMSG_UNF_RESPONSE");
+        sprintf(response, "\ 2%s\ 2: %s", user->nick, fmt);
+        irc_privmsg(cmd->parent->bot, channel->name, response);
+    }
+    else
+        reply("CSMSG_UNF_RESPONSE");
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_ping)
+{
+    if(channel)
+    {
+        char response[MAXLEN];
+        const char *fmt = user_find_message(user, "CSMSG_PING_RESPONSE");
+        sprintf(response, "\ 2%s\ 2: %s", user->nick, fmt);
+        irc_privmsg(cmd->parent->bot, channel->name, response);
+    }
+    else
+        reply("CSMSG_PING_RESPONSE");
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_wut)
+{
+    if(channel)
+    {
+        char response[MAXLEN];
+        const char *fmt = user_find_message(user, "CSMSG_WUT_RESPONSE");
+        sprintf(response, "\ 2%s\ 2: %s", user->nick, fmt);
+        irc_privmsg(cmd->parent->bot, channel->name, response);
+    }
+    else
+        reply("CSMSG_WUT_RESPONSE");
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_8ball)
+{
+    unsigned int i, j, accum;
+    const char *resp;
+
+    REQUIRE_PARAMS(2);
+    accum = 0;
+    for(i=1; i<argc; i++)
+        for(j=0; argv[i][j]; j++)
+            accum = (accum << 5) - accum + toupper(argv[i][j]);
+    resp = chanserv_conf.eightball->list[accum % chanserv_conf.eightball->used];
+    if(channel)
+    {
+        char response[MAXLEN];
+        sprintf(response, "\ 2%s\ 2: %s", user->nick, resp);
+        irc_privmsg(cmd->parent->bot, channel->name, response);
+    }
+    else
+        send_message_type(4, user, cmd->parent->bot, "%s", resp);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_d)
+{
+    unsigned long sides, count, modifier, ii, total;
+    char response[MAXLEN], *sep;
+    const char *fmt;
+
+    REQUIRE_PARAMS(2);
+    if((count = strtoul(argv[1], &sep, 10)) <= 1)
+        goto no_dice;
+    if(sep[0] == 0)
+    {
+        sides = count;
+        count = 1;
+        modifier = 0;
+    }
+    else if(((sep[0] == 'd') || (sep[0] == 'D')) && isdigit(sep[1])
+            && (sides = strtoul(sep+1, &sep, 10)) > 1)
+    {
+        if(sep[0] == 0)
+            modifier = 0;
+        else if((sep[0] == '-') && isdigit(sep[1]))
+            modifier = strtoul(sep, NULL, 10);
+        else if((sep[0] == '+') && isdigit(sep[1]))
+            modifier = strtoul(sep+1, NULL, 10);
+        else
+            goto no_dice;
+    }
+    else
+    {
+      no_dice:
+        reply("CSMSG_BAD_DIE_FORMAT", argv[1]);
+        return 0;
+    }
+    if(count > 10)
+    {
+        reply("CSMSG_BAD_DICE_COUNT", count, 10);
+        return 0;
+    }
+    for(total = ii = 0; ii < count; ++ii)
+        total += (rand() % sides) + 1;
+    total += modifier;
+
+    if((count > 1) || modifier)
+    {
+        fmt = user_find_message(user, "CSMSG_DICE_ROLL");
+        sprintf(response, fmt, total, count, sides, modifier);
+    }
+    else
+    {
+        fmt = user_find_message(user, "CSMSG_DIE_ROLL");
+        sprintf(response, fmt, total, sides);
+    }
+    if(channel)
+        send_target_message(5, channel->name, cmd->parent->bot, "$b%s$b: %s", user->nick, response);
+    else
+        send_message_type(4, user, cmd->parent->bot, "%s", response);
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_huggle)
+{
+    char response[MAXLEN];
+    const char *fmt;
+    /* CTCP must be via PRIVMSG, never notice */
+    if(channel)
+    {
+        fmt = user_find_message(user, "CSMSG_HUGGLES_HIM");
+        sprintf(response, fmt, user->nick);
+        irc_privmsg(cmd->parent->bot, channel->name, response);
+    }
+    else
+    {
+        fmt = user_find_message(user, "CSMSG_HUGGLES_YOU");
+        irc_privmsg(cmd->parent->bot, user->nick, fmt);
+    }
+    return 1;
+}
+
+static void
+chanserv_adjust_limit(void *data)
+{
+    struct mod_chanmode change;
+    struct chanData *cData = data;
+    struct chanNode *channel = cData->channel;
+    unsigned int limit;
+
+    if(IsSuspended(cData))
+        return;
+
+    cData->limitAdjusted = now;
+    limit = channel->members.used + chanserv_conf.adjust_threshold + 5;
+    if(cData->modes.modes_set & MODE_LIMIT)
+    {
+        if(limit > cData->modes.new_limit)
+            limit = cData->modes.new_limit;
+        else if(limit == cData->modes.new_limit)
+            return;
+    }
+
+    change.modes_set = MODE_LIMIT;
+    change.modes_clear = 0;
+    change.new_limit = limit;
+    change.argc = 0;
+    mod_chanmode_announce(chanserv, channel, &change);
+}
+
+static void
+handle_new_channel(struct chanNode *channel)
+{
+    struct chanData *cData;
+
+    if(!(cData = channel->channel_info))
+        return;
+
+    if(cData->modes.modes_set || cData->modes.modes_clear)
+        mod_chanmode_announce(chanserv, cData->channel, &cData->modes);
+
+    if(self->uplink && !self->uplink->burst && channel->channel_info->topic)
+        SetChannelTopic(channel, chanserv, channel->channel_info->topic, 1);
+}
+
+/* Welcome to my worst nightmare. Warning: Read (or modify)
+   the code below at your own risk. */
+static int
+handle_join(struct modeNode *mNode)
+{
+    struct mod_chanmode change;
+    struct userNode *user = mNode->user;
+    struct chanNode *channel = mNode->channel;
+    struct chanData *cData;
+    struct userData *uData = NULL;
+    struct banData *bData;
+    struct handle_info *handle;
+    unsigned int modes = 0, info = 0;
+    char *greeting;
+
+    if(IsLocal(user) || !channel->channel_info || IsSuspended(channel->channel_info))
+        return 0;
+
+    cData = channel->channel_info;
+    if(channel->members.used > cData->max)
+        cData->max = channel->members.used;
+
+    /* Check for bans.  If they're joining through a ban, one of two
+     * cases applies:
+     *   1: Join during a netburst, by riding the break.  Kick them
+     *      unless they have ops or voice in the channel.
+     *   2: They're allowed to join through the ban (an invite in
+     *   ircu2.10, or a +e on Hybrid, or something).
+     * If they're not joining through a ban, and the banlist is not
+     * full, see if they're on the banlist for the channel.  If so,
+     * kickban them.
+     */
+    if(user->uplink->burst && !mNode->modes)
+    {
+        unsigned int ii;
+        for(ii = 0; ii < channel->banlist.used; ii++)
+        {
+            if(user_matches_glob(user, channel->banlist.list[ii]->ban, 1))
+            {
+                /* Riding a netburst.  Naughty. */
+                KickChannelUser(user, channel, chanserv, "User from far side of netsplit should have been banned - bye.");
+                return 1;
+            }
+        }
+    }
+
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    if(channel->banlist.used < MAXBANS)
+    {
+        /* Not joining through a ban. */
+        for(bData = cData->bans;
+                bData && !user_matches_glob(user, bData->mask, 1);
+                bData = bData->next);
+
+        if(bData)
+        {
+            char kick_reason[MAXLEN];
+            sprintf(kick_reason, "%s (%s)", bData->reason, bData->owner);
+
+            bData->triggered = now;
+            if(bData != cData->bans)
+            {
+                /* Shuffle the ban to the head of the list. */
+                if(bData->next) bData->next->prev = bData->prev;
+                if(bData->prev) bData->prev->next = bData->next;
+
+                bData->prev = NULL;
+                bData->next = cData->bans;
+
+                if(cData->bans)
+                    cData->bans->prev = bData;
+                cData->bans = bData;
+            }
+
+            change.args[0].mode = MODE_BAN;
+            change.args[0].hostmask = bData->mask;
+            mod_chanmode_announce(chanserv, channel, &change);
+            KickChannelUser(user, channel, chanserv, kick_reason);
+            return 1;
+        }
+    }
+
+    /* ChanServ will not modify the limits in join-flooded channels.
+       It will also skip DynLimit processing when the user (or srvx)
+       is bursting in, because there are likely more incoming. */
+    if((cData->flags & CHANNEL_DYNAMIC_LIMIT)
+       && !user->uplink->burst
+       && !channel->join_flooded
+       && (channel->limit - channel->members.used) < chanserv_conf.adjust_threshold)
+    {
+        /* The user count has begun "bumping" into the channel limit,
+           so set a timer to raise the limit a bit. Any previous
+           timers are removed so three incoming users within the delay
+           results in one limit change, not three. */
+
+        timeq_del(0, chanserv_adjust_limit, cData, TIMEQ_IGNORE_WHEN);
+        timeq_add(now + chanserv_conf.adjust_delay, chanserv_adjust_limit, cData);
+    }
+
+    if(cData->lvlOpts[lvlGiveOps] == 0)
+        modes |= MODE_CHANOP;
+    else if((cData->flags & CHANNEL_VOICE_ALL) && !channel->join_flooded)
+        modes |= MODE_VOICE;
+
+    greeting = cData->greeting;
+    if(user->handle_info)
+    {
+        handle = user->handle_info;
+
+        if(IsHelper(user) && !IsHelping(user))
+        {
+            unsigned int ii;
+            for(ii = 0; ii < chanserv_conf.support_channels.used; ++ii)
+            {
+                if(channel == chanserv_conf.support_channels.list[ii])
+                {
+                    HANDLE_SET_FLAG(user->handle_info, HELPING);
+                    break;
+                }
+            }
+        }
+
+        uData = GetTrueChannelAccess(cData, handle);
+        if(uData && !IsUserSuspended(uData))
+        {
+            /* Ops and above were handled by the above case. */
+            if(IsUserAutoOp(uData))
+            {
+                if(uData->access < cData->lvlOpts[lvlGiveOps])
+                    modes |= MODE_VOICE;
+                else
+                    modes |= MODE_CHANOP;
+            }
+            if(uData->access >= UL_PRESENT)
+                cData->visited = now;
+
+            uData->seen = now;
+            uData->present = 1;
+
+            if(cData->user_greeting)
+                greeting = cData->user_greeting;
+            if(uData->info
+               && (cData->flags & CHANNEL_INFO_LINES)
+               && ((now - uData->seen) >= chanserv_conf.info_delay)
+               && !uData->present)
+                info = 1;
+        }
+    }
+    if(!user->uplink->burst)
+    {
+        if(modes)
+        {
+            change.args[0].mode = modes;
+            change.args[0].member = mNode;
+            mod_chanmode_announce(chanserv, channel, &change);
+        }
+        if(greeting && !user->uplink->burst)
+            send_message_type(4, user, chanserv, "(%s) %s", channel->name, greeting);
+        if(uData && info)
+            send_target_message(4, channel->name, chanserv, "[%s] %s", user->nick, uData->info);
+    }
+    return 0;
+}
+
+static void
+handle_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
+{
+    struct mod_chanmode change;
+    struct userData *channel;
+    unsigned int ii, jj;
+
+    if(!user->handle_info)
+       return;
+
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    for(channel = user->handle_info->channels; channel; channel = channel->u_next)
+    {
+        struct chanNode *cn;
+        struct modeNode *mn;
+        if(IsSuspended(channel->channel) || !(cn = channel->channel->channel))
+            continue;
+
+        mn = GetUserMode(cn, user);
+        if(!mn)
+        {
+            if(!IsUserSuspended(channel)
+               && IsUserAutoInvite(channel)
+               && (cn->modes & (MODE_KEY | MODE_INVITEONLY))
+               && !self->burst)
+                irc_invite(chanserv, user, cn);
+            continue;
+        }
+
+       if(channel->access >= UL_PRESENT)
+           channel->channel->visited = now;
+
+        if(IsUserAutoOp(channel))
+        {
+            if(channel->access >= cn->channel_info->lvlOpts[lvlGiveOps])
+                change.args[0].mode = MODE_CHANOP;
+            else
+                change.args[0].mode = MODE_VOICE;
+            change.args[0].member = mn;
+            mod_chanmode_announce(chanserv, cn, &change);
+        }
+
+       channel->seen = now;
+       channel->present = 1;
+    }
+
+    for(ii = 0; ii < user->channels.used; ++ii)
+    {
+        struct chanNode *channel = user->channels.list[ii]->channel;
+        struct banData *ban;
+
+        if((user->channels.list[ii]->modes & (MODE_CHANOP|MODE_VOICE))
+           || !channel->channel_info)
+            continue;
+        for(jj = 0; jj < channel->banlist.used; ++jj)
+            if(user_matches_glob(user, channel->banlist.list[jj]->ban, 1))
+                break;
+        if(jj < channel->banlist.used)
+            continue;
+        for(ban = channel->channel_info->bans; ban; ban = ban->next)
+        {
+            char kick_reason[MAXLEN];
+            if(!user_matches_glob(user, ban->mask, 1))
+                continue;
+            change.args[0].mode = MODE_BAN;
+            change.args[0].hostmask = ban->mask;
+            mod_chanmode_announce(chanserv, channel, &change);
+            sprintf(kick_reason, "(%s) %s", ban->owner, ban->reason);
+            KickChannelUser(user, channel, chanserv, kick_reason);
+            ban->triggered = now;
+            break;
+        }
+    }
+
+    if(IsSupportHelper(user))
+    {
+        for(ii = 0; ii < chanserv_conf.support_channels.used; ++ii)
+        {
+            if(GetUserMode(chanserv_conf.support_channels.list[ii], user))
+            {
+                HANDLE_SET_FLAG(user->handle_info, HELPING);
+                break;
+            }
+        }
+    }
+}
+
+static void
+handle_part(struct userNode *user, struct chanNode *channel, UNUSED_ARG(const char *reason))
+{
+    struct chanData *cData;
+    struct userData *uData;
+    struct handle_info *handle;
+
+    cData = channel->channel_info;
+    if(!cData || IsSuspended(cData) || IsLocal(user)) return;
+
+    if((cData->flags & CHANNEL_DYNAMIC_LIMIT) && !channel->join_flooded)
+    {
+       /* Allow for a bit of padding so that the limit doesn't
+          track the user count exactly, which could get annoying. */
+       if((channel->limit - channel->members.used) > chanserv_conf.adjust_threshold + 5)
+       {
+           timeq_del(0, chanserv_adjust_limit, cData, TIMEQ_IGNORE_WHEN);
+           timeq_add(now + chanserv_conf.adjust_delay, chanserv_adjust_limit, cData);
+       }
+    }
+
+    if((handle = user->handle_info) && (uData = GetTrueChannelAccess(cData, handle)))
+    {
+       uData->seen = now;
+       scan_handle_presence(channel, handle, user);
+    }
+
+    if(IsHelping(user) && IsSupportHelper(user))
+    {
+        unsigned int ii, jj;
+        for(ii = 0; ii < chanserv_conf.support_channels.used; ++ii)
+        {
+            for(jj = 0; jj < user->channels.used; ++jj)
+                if(user->channels.list[jj]->channel == chanserv_conf.support_channels.list[ii])
+                    break;
+            if(jj < user->channels.used)
+                break;
+        }
+        if(ii == chanserv_conf.support_channels.used)
+            HANDLE_CLEAR_FLAG(user->handle_info, HELPING);
+    }
+}
+
+static void
+handle_kick(struct userNode *kicker, struct userNode *victim, struct chanNode *channel)
+{
+    char *reason = "CSMSG_USER_PROTECTED";
+
+    if(!channel->channel_info || !kicker || IsService(kicker)
+       || (kicker == victim) || IsSuspended(channel->channel_info)
+       || (kicker->handle_info && kicker->handle_info == victim->handle_info))
+        return;
+
+    if(protect_user(victim, kicker, channel->channel_info))
+       KickChannelUser(kicker, channel, chanserv, reason);
+}
+
+static int
+handle_topic(struct userNode *user, struct chanNode *channel, const char *old_topic)
+{
+    struct chanData *cData;
+
+    if(!channel->channel_info || !user || IsSuspended(channel->channel_info) || IsService(user)) return 0;
+
+    cData = channel->channel_info;
+    if(bad_topic(channel, user, channel->topic))
+    {
+        send_message(user, chanserv, "CSMSG_TOPIC_LOCKED", channel->name);
+        if(cData->topic_mask && match_ircglob(old_topic, cData->topic_mask))
+            SetChannelTopic(channel, chanserv, old_topic, 1);
+        else if(cData->topic)
+            SetChannelTopic(channel, chanserv, cData->topic, 1);
+        return 1;
+    }
+    /* With topicsnarf, grab the topic and save it as the default topic. */
+    if(cData->flags & CHANNEL_TOPIC_SNARF)
+    {
+        free(cData->topic);
+        cData->topic = strdup(channel->topic);
+    }
+    return 0;
+}
+
+static void
+handle_mode(struct chanNode *channel, struct userNode *user, const struct mod_chanmode *change)
+{
+    struct mod_chanmode *bounce = NULL;
+    unsigned int bnc, ii;
+    char deopped = 0;
+
+    if(!channel->channel_info || IsLocal(user) || IsSuspended(channel->channel_info) || IsService(user))
+        return;
+
+    if(!check_user_level(channel, user, lvlEnfModes, 1, 0)
+       && mode_lock_violated(&channel->channel_info->modes, change))
+    {
+        char correct[MAXLEN];
+        bounce = mod_chanmode_alloc(change->argc + 1);
+        *bounce = channel->channel_info->modes;
+        mod_chanmode_format(&channel->channel_info->modes, correct);
+        send_message(user, chanserv, "CSMSG_MODE_LOCKED", correct, channel->name);
+    }
+    for(ii = bnc = 0; ii < change->argc; ++ii)
+    {
+        if((change->args[ii].mode & (MODE_REMOVE|MODE_CHANOP)) == (MODE_REMOVE|MODE_CHANOP))
+        {
+            const struct userNode *victim = change->args[ii].member->user;
+            if(!protect_user(victim, user, channel->channel_info))
+                continue;
+            if(!bounce)
+                bounce = mod_chanmode_alloc(change->argc + 1 - ii);
+            if(!deopped)
+            {
+                bounce->args[bnc].mode = MODE_REMOVE | MODE_CHANOP;
+                bounce->args[bnc].member = GetUserMode(channel, user);
+                if(bounce->args[bnc].member)
+                    bnc++;
+            }
+            bounce->args[bnc].mode = MODE_CHANOP;
+            bounce->args[bnc].member = change->args[ii].member;
+            bnc++;
+            send_message(user, chanserv, "CSMSG_USER_PROTECTED", victim->nick);
+        }
+        else if(change->args[ii].mode & MODE_CHANOP)
+        {
+            const struct userNode *victim = change->args[ii].member->user;
+            if(IsService(victim) || validate_op(user, channel, (struct userNode*)victim))
+                continue;
+            if(!bounce)
+                bounce = mod_chanmode_alloc(change->argc + 1 - ii);
+            bounce->args[bnc].mode = MODE_REMOVE | MODE_CHANOP;
+            bounce->args[bnc].member = change->args[ii].member;
+            bnc++;
+        }
+        else if(change->args[ii].mode & MODE_BAN)
+        {
+            const char *ban = change->args[ii].hostmask;
+            if(!bad_channel_ban(channel, user, ban, NULL, NULL))
+                continue;
+            if(!bounce)
+                bounce = mod_chanmode_alloc(change->argc + 1 - ii);
+            bounce->args[bnc].mode = MODE_REMOVE | MODE_BAN;
+            bounce->args[bnc].hostmask = ban;
+            bnc++;
+            send_message(user, chanserv, "CSMSG_MASK_PROTECTED", remove);
+        }
+    }
+    if(bounce)
+    {
+        if((bounce->argc = bnc))
+            mod_chanmode_announce(chanserv, channel, bounce);
+        mod_chanmode_free(bounce);
+    }
+}
+
+static void
+handle_nick_change(struct userNode *user, UNUSED_ARG(const char *old_nick))
+{
+    struct chanNode *channel;
+    struct banData *bData;
+    struct mod_chanmode change;
+    unsigned int ii, jj;
+    char kick_reason[MAXLEN];
+
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    change.args[0].mode = MODE_BAN;
+    for(ii = 0; ii < user->channels.used; ++ii)
+    {
+        channel = user->channels.list[ii]->channel;
+        /* Need not check for bans if they're opped or voiced. */
+        if(user->channels.list[ii]->modes & (MODE_CHANOP|MODE_VOICE))
+            continue;
+        /* Need not check for bans unless channel registration is active. */
+        if(!channel->channel_info || IsSuspended(channel->channel_info))
+            continue;
+        /* Look for a matching ban already on the channel. */
+        for(jj = 0; jj < channel->banlist.used; ++jj)
+            if(user_matches_glob(user, channel->banlist.list[jj]->ban, 1))
+                break;
+        /* Need not act if we found one. */
+        if(jj < channel->banlist.used)
+            continue;
+        /* Look for a matching ban in this channel. */
+        for(bData = channel->channel_info->bans; bData; bData = bData->next)
+        {
+            if(!user_matches_glob(user, bData->mask, 1))
+                continue;
+            change.args[0].hostmask = bData->mask;
+            mod_chanmode_announce(chanserv, channel, &change);
+            sprintf(kick_reason, "(%s) %s", bData->owner, bData->reason);
+            KickChannelUser(user, channel, chanserv, kick_reason);
+            bData->triggered = now;
+            break; /* we don't need to check any more bans in the channel */
+        }
+    }
+}
+
+static void handle_rename(struct handle_info *handle, const char *old_handle)
+{
+    struct do_not_register *dnr = dict_find(handle_dnrs, old_handle, NULL);
+
+    if(dnr)
+    {
+        dict_remove2(handle_dnrs, old_handle, 1);
+        safestrncpy(dnr->chan_name + 1, handle->handle, sizeof(dnr->chan_name) - 1);
+        dict_insert(handle_dnrs, dnr->chan_name + 1, dnr);
+    }
+}
+
+static void
+handle_unreg(UNUSED_ARG(struct userNode *user), struct handle_info *handle)
+{
+    struct userNode *h_user;
+
+    if(handle->channels)
+    {
+        for(h_user = handle->users; h_user; h_user = h_user->next_authed)
+            send_message(h_user, chanserv, "CSMSG_HANDLE_UNREGISTERED");
+
+        while(handle->channels)
+            del_channel_user(handle->channels, 1);
+    }
+}
+
+static void
+handle_server_link(UNUSED_ARG(struct server *server))
+{
+    struct chanData *cData;
+
+    for(cData = channelList; cData; cData = cData->next)
+    {
+        if(!IsSuspended(cData))
+            cData->may_opchan = 1;
+        if((cData->flags & CHANNEL_DYNAMIC_LIMIT)
+           && !cData->channel->join_flooded
+           && ((cData->channel->limit - cData->channel->members.used)
+               < chanserv_conf.adjust_threshold))
+        {
+            timeq_del(0, chanserv_adjust_limit, cData, TIMEQ_IGNORE_WHEN);
+            timeq_add(now + chanserv_conf.adjust_delay, chanserv_adjust_limit, cData);
+        }
+    }
+}
+
+static void
+chanserv_conf_read(void)
+{
+    dict_t conf_node;
+    const char *str;
+    char mode_line[MAXLEN], *modes[MAXNUMPARAMS];
+    struct mod_chanmode *change;
+    struct string_list *strlist;
+    struct chanNode *chan;
+    unsigned int ii;
+
+    if(!(conf_node = conf_get_data(CHANSERV_CONF_NAME, RECDB_OBJECT)))
+    {
+       log_module(CS_LOG, LOG_ERROR, "Invalid config node `%s'.", CHANSERV_CONF_NAME);
+       return;
+    }
+    for(ii = 0; ii < chanserv_conf.support_channels.used; ++ii)
+        UnlockChannel(chanserv_conf.support_channels.list[ii]);
+    chanserv_conf.support_channels.used = 0;
+    if((strlist = database_get_data(conf_node, KEY_SUPPORT_CHANNEL, RECDB_STRING_LIST)))
+    {
+        for(ii = 0; ii < strlist->used; ++ii)
+        {
+            const char *str2 = database_get_data(conf_node, KEY_SUPPORT_CHANNEL_MODES, RECDB_QSTRING);
+            if(!str2)
+                str2 = "+nt";
+            chan = AddChannel(strlist->list[ii], now, str2, NULL);
+            LockChannel(chan);
+            channelList_append(&chanserv_conf.support_channels, chan);
+        }
+    }
+    else if((str = database_get_data(conf_node, KEY_SUPPORT_CHANNEL, RECDB_QSTRING)))
+    {
+        const char *str2;
+        str2 = database_get_data(conf_node, KEY_SUPPORT_CHANNEL_MODES, RECDB_QSTRING);
+        if(!str2)
+            str2 = "+nt";
+        chan = AddChannel(str, now, str2, NULL);
+        LockChannel(chan);
+        channelList_append(&chanserv_conf.support_channels, chan);
+    }
+    str = database_get_data(conf_node, KEY_DB_BACKUP_FREQ, RECDB_QSTRING);
+    chanserv_conf.db_backup_frequency = str ? ParseInterval(str) : 7200;
+    str = database_get_data(conf_node, KEY_INFO_DELAY, RECDB_QSTRING);
+    chanserv_conf.info_delay = str ? ParseInterval(str) : 180;
+    str = database_get_data(conf_node, KEY_MAX_GREETLEN, RECDB_QSTRING);
+    chanserv_conf.greeting_length = str ? atoi(str) : 120;
+    str = database_get_data(conf_node, KEY_ADJUST_THRESHOLD, RECDB_QSTRING);
+    chanserv_conf.adjust_threshold = str ? atoi(str) : 15;
+    str = database_get_data(conf_node, KEY_ADJUST_DELAY, RECDB_QSTRING);
+    chanserv_conf.adjust_delay = str ? ParseInterval(str) : 30;
+    str = database_get_data(conf_node, KEY_CHAN_EXPIRE_FREQ, RECDB_QSTRING);
+    chanserv_conf.channel_expire_frequency = str ? ParseInterval(str) : 86400;
+    str = database_get_data(conf_node, KEY_CHAN_EXPIRE_DELAY, RECDB_QSTRING);
+    chanserv_conf.channel_expire_delay = str ? ParseInterval(str) : 86400*30;
+    str = database_get_data(conf_node, KEY_NODELETE_LEVEL, RECDB_QSTRING);
+    chanserv_conf.nodelete_level = str ? atoi(str) : 1;
+    str = database_get_data(conf_node, KEY_MAX_CHAN_USERS, RECDB_QSTRING);
+    chanserv_conf.max_chan_users = str ? atoi(str) : 512;
+    str = database_get_data(conf_node, KEY_MAX_CHAN_BANS, RECDB_QSTRING);
+    chanserv_conf.max_chan_bans = str ? atoi(str) : 512;
+    str = database_get_data(conf_node, KEY_NICK, RECDB_QSTRING);
+    if(str) NickChange(chanserv, str, 0);
+    str = database_get_data(conf_node, KEY_REFRESH_PERIOD, RECDB_QSTRING);
+    chanserv_conf.refresh_period = str ? ParseInterval(str) : 3*60*60;
+    str = database_get_data(conf_node, KEY_CTCP_SHORT_BAN_DURATION, RECDB_QSTRING);
+    chanserv_conf.ctcp_short_ban_duration = str ? str : "3m";
+    str = database_get_data(conf_node, KEY_CTCP_LONG_BAN_DURATION, RECDB_QSTRING);
+    chanserv_conf.ctcp_long_ban_duration = str ? str : "1h";
+    str = database_get_data(conf_node, KEY_MAX_OWNED, RECDB_QSTRING);
+    chanserv_conf.max_owned = str ? atoi(str) : 5;
+    str = database_get_data(conf_node, KEY_IRC_OPERATOR_EPITHET, RECDB_QSTRING);
+    chanserv_conf.irc_operator_epithet = str ? str : "a megalomaniacal power hungry tyrant";
+    str = database_get_data(conf_node, KEY_NETWORK_HELPER_EPITHET, RECDB_QSTRING);
+    chanserv_conf.network_helper_epithet = str ? str : "a wannabe tyrant";
+    str = database_get_data(conf_node, KEY_SUPPORT_HELPER_EPITHET, RECDB_QSTRING);
+    chanserv_conf.support_helper_epithet = str ? str : "a wannabe tyrant";
+    str = database_get_data(conf_node, "default_modes", RECDB_QSTRING);
+    if(!str)
+        str = "+nt";
+    safestrncpy(mode_line, str, sizeof(mode_line));
+    ii = split_line(mode_line, 0, ArrayLength(modes), modes);
+    if((change = mod_chanmode_parse(NULL, modes, ii, MCP_KEY_FREE)) && (change->argc < 2))
+    {
+        chanserv_conf.default_modes = *change;
+        mod_chanmode_free(change);
+    }
+    free_string_list(chanserv_conf.set_shows);
+    strlist = database_get_data(conf_node, "set_shows", RECDB_STRING_LIST);
+    if(strlist)
+        strlist = string_list_copy(strlist);
+    else
+    {
+        static const char *list[] = {
+            /* multiple choice options */
+            "DefaultTopic", "TopicMask", "Greeting", "UserGreeting", "Modes",
+            "PubCmd", "GiveOps", "EnfOps", "EnfModes", "EnfTopic", "Protect",
+            "Toys", "Setters", "TopicRefresh", "CtcpUsers", "CtcpReaction",
+            /* binary options */
+            "Voice", "UserInfo", "DynLimit", "TopicSnarf", "PeonInvite", "NoDelete",
+            /* delimiter */
+            NULL };
+        unsigned int ii;
+        strlist = alloc_string_list(ArrayLength(list)-1);
+        for(ii=0; list[ii]; ii++)
+            string_list_append(strlist, strdup(list[ii]));
+    }
+    chanserv_conf.set_shows = strlist;
+    /* We don't look things up now, in case the list refers to options
+     * defined by modules initialized after this point.  Just mark the
+     * function list as invalid, so it will be initialized.
+     */
+    set_shows_list.used = 0;
+    free_string_list(chanserv_conf.eightball);
+    strlist = database_get_data(conf_node, KEY_8BALL_RESPONSES, RECDB_STRING_LIST);
+    if(strlist)
+    {
+        strlist = string_list_copy(strlist);
+    }
+    else
+    {
+        strlist = alloc_string_list(4);
+        string_list_append(strlist, strdup("Yes."));
+        string_list_append(strlist, strdup("No."));
+        string_list_append(strlist, strdup("Maybe so."));
+    }
+    chanserv_conf.eightball = strlist;
+    free_string_list(chanserv_conf.old_ban_names);
+    strlist = database_get_data(conf_node, KEY_OLD_BAN_NAMES, RECDB_STRING_LIST);
+    if (strlist)
+        strlist = string_list_copy(strlist);
+    else
+        strlist = alloc_string_list(2);
+    chanserv_conf.old_ban_names = strlist;
+}
+
+static void
+chanserv_note_type_read(const char *key, struct record_data *rd)
+{
+    dict_t obj;
+    struct note_type *ntype;
+    const char *str;
+
+    if(!(obj = GET_RECORD_OBJECT(rd)))
+    {
+        log_module(CS_LOG, LOG_ERROR, "Invalid note type %s.", key);
+        return;
+    }
+    if(!(ntype = chanserv_create_note_type(key)))
+    {
+        log_module(CS_LOG, LOG_ERROR, "Memory allocation failed for note %s.", key);
+        return;
+    }
+
+    /* Figure out set access */
+    if((str = database_get_data(obj, KEY_NOTE_OPSERV_ACCESS, RECDB_QSTRING)))
+    {
+        ntype->set_access_type = NOTE_SET_PRIVILEGED;
+        ntype->set_access.min_opserv = strtoul(str, NULL, 0);
+    }
+    else if((str = database_get_data(obj, KEY_NOTE_CHANNEL_ACCESS, RECDB_QSTRING)))
+    {
+        ntype->set_access_type = NOTE_SET_CHANNEL_ACCESS;
+        ntype->set_access.min_ulevel = strtoul(str, NULL, 0);
+    }
+    else if((str = database_get_data(obj, KEY_NOTE_SETTER_ACCESS, RECDB_QSTRING)))
+    {
+        ntype->set_access_type = NOTE_SET_CHANNEL_SETTER;
+    }
+    else
+    {
+        log_module(CS_LOG, LOG_ERROR, "Could not find access type for note %s; defaulting to OpServ access level 0.", key);
+        ntype->set_access_type = NOTE_SET_PRIVILEGED;
+        ntype->set_access.min_opserv = 0;
+    }
+
+    /* Figure out visibility */
+    if(!(str = database_get_data(obj, KEY_NOTE_VISIBILITY, RECDB_QSTRING)))
+        ntype->visible_type = NOTE_VIS_PRIVILEGED;
+    else if(!irccasecmp(str, KEY_NOTE_VIS_PRIVILEGED))
+        ntype->visible_type = NOTE_VIS_PRIVILEGED;
+    else if(!irccasecmp(str, KEY_NOTE_VIS_CHANNEL_USERS))
+        ntype->visible_type = NOTE_VIS_CHANNEL_USERS;
+    else if(!irccasecmp(str, KEY_NOTE_VIS_ALL))
+        ntype->visible_type = NOTE_VIS_ALL;
+    else
+        ntype->visible_type = NOTE_VIS_PRIVILEGED;
+
+    str = database_get_data(obj, KEY_NOTE_MAX_LENGTH, RECDB_QSTRING);
+    ntype->max_length = str ? strtoul(str, NULL, 0) : 400;
+}
+
+static void
+user_read_helper(const char *key, struct record_data *rd, struct chanData *chan)
+{
+    struct handle_info *handle;
+    struct userData *uData;
+    char *seen, *inf, *flags;
+    time_t last_seen;
+    unsigned short access;
+
+    if(rd->type != RECDB_OBJECT || !dict_size(rd->d.object))
+    {
+       log_module(CS_LOG, LOG_ERROR, "Invalid user in %s.", chan->channel->name);
+       return;
+    }
+
+    access = atoi(database_get_data(rd->d.object, KEY_LEVEL, RECDB_QSTRING));
+    if(access > UL_OWNER)
+    {
+       log_module(CS_LOG, LOG_ERROR, "Invalid access level for %s in %s.", key, chan->channel->name);
+       return;
+    }
+
+    inf = database_get_data(rd->d.object, KEY_INFO, RECDB_QSTRING);
+    seen = database_get_data(rd->d.object, KEY_SEEN, RECDB_QSTRING);
+    last_seen = seen ? (signed)strtoul(seen, NULL, 0) : now;
+    flags = database_get_data(rd->d.object, KEY_FLAGS, RECDB_QSTRING);
+    handle = get_handle_info(key);
+    if(!handle)
+    {
+        log_module(CS_LOG, LOG_ERROR, "Nonexistent account %s in %s.", key, chan->channel->name);
+        return;
+    }
+
+    uData = add_channel_user(chan, handle, access, last_seen, inf);
+    uData->flags = flags ? strtoul(flags, NULL, 0) : 0;
+}
+
+static void
+ban_read_helper(const char *key, struct record_data *rd, struct chanData *chan)
+{
+    struct banData *bData;
+    char *set, *triggered, *s_duration, *s_expires, *reason, *owner;
+    time_t set_time, triggered_time, expires_time;
+
+    if(rd->type != RECDB_OBJECT || !dict_size(rd->d.object))
+    {
+       log_module(CS_LOG, LOG_ERROR, "Invalid ban in %s.", chan->channel->name);
+       return;
+    }
+
+    set = database_get_data(rd->d.object, KEY_SET, RECDB_QSTRING);
+    triggered = database_get_data(rd->d.object, KEY_TRIGGERED, RECDB_QSTRING);
+    s_duration = database_get_data(rd->d.object, KEY_DURATION, RECDB_QSTRING);
+    s_expires = database_get_data(rd->d.object, KEY_EXPIRES, RECDB_QSTRING);
+    owner = database_get_data(rd->d.object, KEY_OWNER, RECDB_QSTRING);
+    reason = database_get_data(rd->d.object, KEY_REASON, RECDB_QSTRING);
+
+    set_time = set ? (time_t)strtoul(set, NULL, 0) : now;
+    triggered_time = triggered ? (time_t)strtoul(triggered, NULL, 0) : 0;
+    if(s_expires)
+        expires_time = (time_t)strtoul(s_expires, NULL, 0);
+    else if(s_duration)
+        expires_time = set_time + atoi(s_duration);
+    else
+        expires_time = 0;
+
+    if(expires_time && (expires_time < now))
+        return;
+
+    bData = add_channel_ban(chan, key, owner, set_time, triggered_time, expires_time, reason);
+}
+
+static struct suspended *
+chanserv_read_suspended(dict_t obj)
+{
+    struct suspended *suspended = calloc(1, sizeof(*suspended));
+    char *str;
+    dict_t previous;
+
+    str = database_get_data(obj, KEY_EXPIRES, RECDB_QSTRING);
+    suspended->expires = str ? (time_t)strtoul(str, NULL, 0) : 0;
+    str = database_get_data(obj, KEY_REVOKED, RECDB_QSTRING);
+    suspended->revoked = str ? (time_t)strtoul(str, NULL, 0) : 0;
+    str = database_get_data(obj, KEY_ISSUED, RECDB_QSTRING);
+    suspended->issued = str ? (time_t)strtoul(str, NULL, 0) : 0;
+    suspended->suspender = strdup(database_get_data(obj, KEY_SUSPENDER, RECDB_QSTRING));
+    suspended->reason = strdup(database_get_data(obj, KEY_REASON, RECDB_QSTRING));
+    previous = database_get_data(obj, KEY_PREVIOUS, RECDB_OBJECT);
+    suspended->previous = previous ? chanserv_read_suspended(previous) : NULL;
+    return suspended;
+}
+
+static int
+chanserv_channel_read(const char *key, struct record_data *hir)
+{
+    struct suspended *suspended;
+    struct mod_chanmode *modes;
+    struct chanNode *cNode;
+    struct chanData *cData;
+    struct dict *channel, *obj;
+    char *str, *argv[10];
+    dict_iterator_t it;
+    unsigned int argc;
+
+    channel = hir->d.object;
+
+    str = database_get_data(channel, KEY_REGISTRAR, RECDB_QSTRING);
+    if(!str)
+        str = "<unknown>";
+    cNode = AddChannel(key, now, NULL, NULL);
+    if(!cNode)
+    {
+        log_module(CS_LOG, LOG_ERROR, "Unable to create registered channel %s.", key);
+        return 0;
+    }
+    cData = register_channel(cNode, str);
+    if(!cData)
+    {
+        log_module(CS_LOG, LOG_ERROR, "Unable to register channel %s from database.", key);
+       return 0;
+    }
+
+    if((obj = database_get_data(channel, KEY_OPTIONS, RECDB_OBJECT)))
+    {
+        enum levelOption lvlOpt;
+        enum charOption chOpt;
+        for(lvlOpt = 0; lvlOpt < NUM_LEVEL_OPTIONS; ++lvlOpt)
+        {
+            if(!(str = database_get_data(obj, levelOptions[lvlOpt].db_name, RECDB_QSTRING)))
+                continue;
+            cData->lvlOpts[lvlOpt] = user_level_from_name(str, UL_OWNER+1);
+        }
+        for(chOpt = 0; chOpt < NUM_CHAR_OPTIONS; ++chOpt)
+        {
+            if(!(str = database_get_data(obj, charOptions[chOpt].db_name, RECDB_QSTRING)))
+                continue;
+            cData->chOpts[chOpt] = str[0];
+        }
+        if((str = database_get_data(channel, KEY_FLAGS, RECDB_QSTRING)))
+            cData->flags = atoi(str);
+    }
+    else if((str = database_get_data(channel, KEY_FLAGS, RECDB_QSTRING)))
+    {
+        enum levelOption lvlOpt;
+        enum charOption chOpt;
+        unsigned int count;
+
+       cData->flags = base64toint(str, 5);
+        count = strlen(str += 5);
+        for(lvlOpt = 0; lvlOpt < NUM_LEVEL_OPTIONS; ++lvlOpt)
+        {
+            unsigned short lvl;
+            switch(((count <= levelOptions[lvlOpt].old_idx) ? str : CHANNEL_DEFAULT_OPTIONS)[levelOptions[lvlOpt].old_idx])
+            {
+            case 'c': lvl = UL_COOWNER; break;
+            case 'm': lvl = UL_MASTER; break;
+            case 'n': lvl = UL_OWNER+1; break;
+            case 'o': lvl = UL_OP; break;
+            case 'p': lvl = UL_PEON; break;
+            case 'w': lvl = UL_OWNER; break;
+            default: lvl = 0; break;
+            }
+            cData->lvlOpts[lvlOpt] = lvl;
+        }
+        for(chOpt = 0; chOpt < NUM_CHAR_OPTIONS; ++chOpt)
+            cData->chOpts[chOpt] = ((count <= charOptions[chOpt].old_idx) ? str : CHANNEL_DEFAULT_OPTIONS)[charOptions[chOpt].old_idx];
+    }
+   
+    if((obj = database_get_data(hir->d.object, KEY_SUSPENDED, RECDB_OBJECT)))
+    {
+        suspended = chanserv_read_suspended(obj);
+        cData->suspended = suspended;
+        suspended->cData = cData;
+        /* We could use suspended->expires and suspended->revoked to
+         * set the CHANNEL_SUSPENDED flag, but we don't. */
+    }
+    else if(cData->flags & CHANNEL_SUSPENDED)
+    {
+        suspended = calloc(1, sizeof(*suspended));
+        suspended->issued = 0;
+        suspended->revoked = 0;
+        str = database_get_data(hir->d.object, KEY_SUSPEND_EXPIRES, RECDB_QSTRING);
+        suspended->expires = str ? atoi(str) : 0;
+        suspended->suspender = strdup(database_get_data(hir->d.object, KEY_SUSPENDER, RECDB_QSTRING));
+        str = database_get_data(hir->d.object, KEY_SUSPEND_REASON, RECDB_QSTRING);
+        suspended->reason = strdup(str ? str : "No reason");
+        suspended->previous = NULL;
+        cData->suspended = suspended;
+        suspended->cData = cData;
+    }
+    else
+        suspended = NULL;
+
+    if((cData->flags & CHANNEL_SUSPENDED)
+       && suspended->expires
+       && (suspended->expires <= now))
+    {
+        cData->flags &= ~CHANNEL_SUSPENDED;
+    }
+
+    if((cData->flags & CHANNEL_SUSPENDED) && (suspended->expires > now))
+    {
+        timeq_add(suspended->expires, chanserv_expire_suspension, suspended);
+    }
+    else
+    {
+        struct mod_chanmode change;
+        change.modes_set = change.modes_clear = 0;
+        change.argc = 1;
+        change.args[0].mode = MODE_CHANOP;
+        change.args[0].member = AddChannelUser(chanserv, cNode);
+        mod_chanmode_announce(chanserv, cNode, &change);
+    }
+
+    str = database_get_data(channel, KEY_REGISTERED, RECDB_QSTRING);
+    cData->registered = str ? (time_t)strtoul(str, NULL, 0) : now;
+    str = database_get_data(channel, KEY_VISITED, RECDB_QSTRING);
+    cData->visited = str ? (time_t)strtoul(str, NULL, 0) : now;
+    str = database_get_data(channel, KEY_MAX, RECDB_QSTRING);
+    cData->max = str ? atoi(str) : 0;
+    str = database_get_data(channel, KEY_GREETING, RECDB_QSTRING);
+    cData->greeting = str ? strdup(str) : NULL;
+    str = database_get_data(channel, KEY_USER_GREETING, RECDB_QSTRING);
+    cData->user_greeting = str ? strdup(str) : NULL;
+    str = database_get_data(channel, KEY_TOPIC_MASK, RECDB_QSTRING);
+    cData->topic_mask = str ? strdup(str) : NULL;
+    str = database_get_data(channel, KEY_TOPIC, RECDB_QSTRING);
+    cData->topic = str ? strdup(str) : NULL;
+
+    if((str = database_get_data(channel, KEY_MODES, RECDB_QSTRING))
+       && (argc = split_line(str, 0, ArrayLength(argv), argv))
+       && (modes = mod_chanmode_parse(cNode, argv, argc, MCP_KEY_FREE))) {
+        cData->modes = *modes;
+        if(cData->modes.argc > 1)
+            cData->modes.argc = 1;
+        if(!IsSuspended(cData))
+            mod_chanmode_announce(chanserv, cNode, &cData->modes);
+        mod_chanmode_free(modes);
+    }
+
+    obj = database_get_data(channel, KEY_USERS, RECDB_OBJECT);
+    for(it = dict_first(obj); it; it = iter_next(it))
+       user_read_helper(iter_key(it), iter_data(it), cData);
+
+    if(!cData->users && !IsProtected(cData))
+    {
+        log_module(CS_LOG, LOG_ERROR, "Channel %s had no users in database, unregistering it.", key);
+       unregister_channel(cData, "has empty user list.");
+        return 0;
+    }
+
+    obj = database_get_data(channel, KEY_BANS, RECDB_OBJECT);
+    for(it = dict_first(obj); it; it = iter_next(it))
+        ban_read_helper(iter_key(it), iter_data(it), cData);
+
+    obj = database_get_data(channel, KEY_NOTES, RECDB_OBJECT);
+    for(it = dict_first(obj); it; it = iter_next(it))
+    {
+        struct note_type *ntype = dict_find(note_types, iter_key(it), NULL);
+        struct record_data *rd = iter_data(it);
+        const char *note, *setter;
+
+        if(rd->type != RECDB_OBJECT)
+        {
+            log_module(CS_LOG, LOG_ERROR, "Bad record type for note %s in channel %s.", iter_key(it), key);
+        }
+        else if(!ntype)
+        {
+            log_module(CS_LOG, LOG_ERROR, "Bad note type name %s in channel %s.", iter_key(it), key);
+        }
+        else if(!(note = database_get_data(rd->d.object, KEY_NOTE_NOTE, RECDB_QSTRING)))
+        {
+            log_module(CS_LOG, LOG_ERROR, "Missing note text for note %s in channel %s.", iter_key(it), key);
+        }
+        else
+        {
+            setter = database_get_data(rd->d.object, KEY_NOTE_SETTER, RECDB_QSTRING);
+            if(!setter) setter = "<unknown>";
+            chanserv_add_channel_note(cData, ntype, setter, note);
+        }
+    }
+
+    return 0;
+}
+
+static void
+chanserv_dnr_read(const char *key, struct record_data *hir)
+{
+    const char *setter, *reason, *str;
+    struct do_not_register *dnr;
+
+    setter = database_get_data(hir->d.object, KEY_DNR_SETTER, RECDB_QSTRING);
+    if(!setter)
+    {
+        log_module(CS_LOG, LOG_ERROR, "Missing setter for DNR %s.", key);
+        return;
+    }
+    reason = database_get_data(hir->d.object, KEY_DNR_REASON, RECDB_QSTRING);
+    if(!reason)
+    {
+        log_module(CS_LOG, LOG_ERROR, "Missing reason for DNR %s.", key);
+        return;
+    }
+    dnr = chanserv_add_dnr(key, setter, reason);
+    if(!dnr)
+        return;
+    str = database_get_data(hir->d.object, KEY_DNR_SET, RECDB_QSTRING);
+    if(str)
+        dnr->set = atoi(str);
+    else
+        dnr->set = 0;
+}
+
+static int
+chanserv_saxdb_read(struct dict *database)
+{
+    struct dict *section;
+    dict_iterator_t it;
+
+    if((section = database_get_data(database, KEY_NOTE_TYPES, RECDB_OBJECT)))
+        for(it = dict_first(section); it; it = iter_next(it))
+            chanserv_note_type_read(iter_key(it), iter_data(it));
+
+    if((section = database_get_data(database, KEY_CHANNELS, RECDB_OBJECT)))
+       for(it = dict_first(section); it; it = iter_next(it))
+           chanserv_channel_read(iter_key(it), iter_data(it));
+
+    if((section = database_get_data(database, KEY_DNR, RECDB_OBJECT)))
+        for(it = dict_first(section); it; it = iter_next(it))
+            chanserv_dnr_read(iter_key(it), iter_data(it));
+
+    return 0;
+}
+
+static int
+chanserv_write_users(struct saxdb_context *ctx, struct userData *uData)
+{
+    int high_present = 0;
+    saxdb_start_record(ctx, KEY_USERS, 1);
+    for(; uData; uData = uData->next)
+    {
+        if((uData->access >= UL_PRESENT) && uData->present)
+            high_present = 1;
+        saxdb_start_record(ctx, uData->handle->handle, 0);
+        saxdb_write_int(ctx, KEY_LEVEL, uData->access);
+        saxdb_write_int(ctx, KEY_SEEN, uData->seen);
+        if(uData->flags)
+            saxdb_write_int(ctx, KEY_FLAGS, uData->flags);
+       if(uData->info)
+            saxdb_write_string(ctx, KEY_INFO, uData->info);
+        saxdb_end_record(ctx);
+    }
+    saxdb_end_record(ctx);
+    return high_present;
+}
+
+static void
+chanserv_write_bans(struct saxdb_context *ctx, struct banData *bData)
+{
+    if(!bData)
+        return;
+    saxdb_start_record(ctx, KEY_BANS, 1);
+    for(; bData; bData = bData->next)
+    {
+        saxdb_start_record(ctx, bData->mask, 0);
+        saxdb_write_int(ctx, KEY_SET, bData->set);
+        if(bData->triggered)
+            saxdb_write_int(ctx, KEY_TRIGGERED, bData->triggered);
+        if(bData->expires)
+            saxdb_write_int(ctx, KEY_EXPIRES, bData->expires);
+        if(bData->owner[0])
+            saxdb_write_string(ctx, KEY_OWNER, bData->owner);
+        if(bData->reason)
+            saxdb_write_string(ctx, KEY_REASON, bData->reason);
+        saxdb_end_record(ctx);
+    }
+    saxdb_end_record(ctx);
+}
+
+static void
+chanserv_write_suspended(struct saxdb_context *ctx, const char *name, struct suspended *susp)
+{
+    saxdb_start_record(ctx, name, 0);
+    saxdb_write_string(ctx, KEY_SUSPENDER, susp->suspender);
+    saxdb_write_string(ctx, KEY_REASON, susp->reason);
+    if(susp->issued)
+        saxdb_write_int(ctx, KEY_ISSUED, susp->issued);
+    if(susp->expires)
+        saxdb_write_int(ctx, KEY_EXPIRES, susp->expires);
+    if(susp->revoked)
+        saxdb_write_int(ctx, KEY_REVOKED, susp->revoked);
+    if(susp->previous)
+        chanserv_write_suspended(ctx, KEY_PREVIOUS, susp->previous);
+    saxdb_end_record(ctx);
+}
+
+static void
+chanserv_write_channel(struct saxdb_context *ctx, struct chanData *channel)
+{
+    char buf[MAXLEN];
+    int high_present;
+    enum levelOption lvlOpt;
+    enum charOption chOpt;
+
+    saxdb_start_record(ctx, channel->channel->name, 1);
+
+    saxdb_write_int(ctx, KEY_REGISTERED, channel->registered);
+    saxdb_write_int(ctx, KEY_MAX, channel->max);
+    if(channel->topic)
+        saxdb_write_string(ctx, KEY_TOPIC, channel->topic);
+    if(channel->registrar)
+        saxdb_write_string(ctx, KEY_REGISTRAR, channel->registrar);
+    if(channel->greeting)
+        saxdb_write_string(ctx, KEY_GREETING, channel->greeting);
+    if(channel->user_greeting)
+        saxdb_write_string(ctx, KEY_USER_GREETING, channel->user_greeting);
+    if(channel->topic_mask)
+        saxdb_write_string(ctx, KEY_TOPIC_MASK, channel->topic_mask);
+    if(channel->suspended)
+        chanserv_write_suspended(ctx, "suspended", channel->suspended);
+
+    saxdb_start_record(ctx, KEY_OPTIONS, 0);
+    saxdb_write_int(ctx, KEY_FLAGS, channel->flags);
+    for(lvlOpt = 0; lvlOpt < NUM_LEVEL_OPTIONS; ++lvlOpt)
+        saxdb_write_int(ctx, levelOptions[lvlOpt].db_name, channel->lvlOpts[lvlOpt]);
+    for(chOpt = 0; chOpt < NUM_CHAR_OPTIONS; ++chOpt)
+    {
+        buf[0] = channel->chOpts[chOpt];
+        buf[1] = '\0';
+        saxdb_write_string(ctx, charOptions[chOpt].db_name, buf);
+    }
+    saxdb_end_record(ctx);
+
+    if(channel->modes.modes_set || channel->modes.modes_clear)
+    {
+       mod_chanmode_format(&channel->modes, buf);
+        saxdb_write_string(ctx, KEY_MODES, buf);
+    }
+
+    high_present = chanserv_write_users(ctx, channel->users);
+    chanserv_write_bans(ctx, channel->bans);
+
+    if(dict_size(channel->notes))
+    {
+        dict_iterator_t it;
+
+        saxdb_start_record(ctx, KEY_NOTES, 1);
+        for(it = dict_first(channel->notes); it; it = iter_next(it))
+        {
+           struct note *note = iter_data(it);
+            saxdb_start_record(ctx, iter_key(it), 0);
+            saxdb_write_string(ctx, KEY_NOTE_SETTER, note->setter);
+            saxdb_write_string(ctx, KEY_NOTE_NOTE, note->note);
+            saxdb_end_record(ctx);
+        }
+        saxdb_end_record(ctx);
+    }
+
+    saxdb_write_int(ctx, KEY_VISITED, high_present ? now : channel->visited);
+    saxdb_end_record(ctx);
+}
+
+static void
+chanserv_write_note_type(struct saxdb_context *ctx, struct note_type *ntype)
+{
+    const char *str;
+
+    saxdb_start_record(ctx, ntype->name, 0);
+    switch(ntype->set_access_type)
+    {
+    case NOTE_SET_CHANNEL_ACCESS:
+        saxdb_write_int(ctx, KEY_NOTE_CHANNEL_ACCESS, ntype->set_access.min_ulevel);
+        break;
+    case NOTE_SET_CHANNEL_SETTER:
+        saxdb_write_int(ctx, KEY_NOTE_SETTER_ACCESS, 1);
+        break;
+    case NOTE_SET_PRIVILEGED: default:
+        saxdb_write_int(ctx, KEY_NOTE_OPSERV_ACCESS, ntype->set_access.min_opserv);
+        break;
+    }
+    switch(ntype->visible_type)
+    {
+    case NOTE_VIS_ALL: str = KEY_NOTE_VIS_ALL; break;
+    case NOTE_VIS_CHANNEL_USERS: str = KEY_NOTE_VIS_CHANNEL_USERS; break;
+    case NOTE_VIS_PRIVILEGED: default: str = KEY_NOTE_VIS_PRIVILEGED; break;
+    }
+    saxdb_write_string(ctx, KEY_NOTE_VISIBILITY, str);
+    saxdb_write_int(ctx, KEY_NOTE_MAX_LENGTH, ntype->max_length);
+    saxdb_end_record(ctx);
+}
+
+static void
+write_dnrs_helper(struct saxdb_context *ctx, struct dict *dnrs)
+{
+    struct do_not_register *dnr;
+    dict_iterator_t it;
+
+    for(it = dict_first(dnrs); it; it = iter_next(it))
+    {
+        dnr = iter_data(it);
+        saxdb_start_record(ctx, dnr->chan_name, 0);
+        if(dnr->set)
+            saxdb_write_int(ctx, KEY_DNR_SET, dnr->set);
+        saxdb_write_string(ctx, KEY_DNR_SETTER, dnr->setter);
+        saxdb_write_string(ctx, KEY_DNR_REASON, dnr->reason);
+        saxdb_end_record(ctx);
+    }
+}
+
+static int
+chanserv_saxdb_write(struct saxdb_context *ctx)
+{
+    dict_iterator_t it;
+    struct chanData *channel;
+
+    /* Notes */
+    saxdb_start_record(ctx, KEY_NOTE_TYPES, 1);
+    for(it = dict_first(note_types); it; it = iter_next(it))
+        chanserv_write_note_type(ctx, iter_data(it));
+    saxdb_end_record(ctx);
+
+    /* DNRs */
+    saxdb_start_record(ctx, KEY_DNR, 1);
+    write_dnrs_helper(ctx, handle_dnrs);
+    write_dnrs_helper(ctx, plain_dnrs);
+    write_dnrs_helper(ctx, mask_dnrs);
+    saxdb_end_record(ctx);
+
+    /* Channels */
+    saxdb_start_record(ctx, KEY_CHANNELS, 1);
+    for(channel = channelList; channel; channel = channel->next)
+        chanserv_write_channel(ctx, channel);
+    saxdb_end_record(ctx);
+
+    return 0;
+}
+
+static void
+chanserv_db_cleanup(void) {
+    unsigned int ii;
+    unreg_part_func(handle_part);
+    while(channelList)
+        unregister_channel(channelList, "terminating.");
+    for(ii = 0; ii < chanserv_conf.support_channels.used; ++ii)
+        UnlockChannel(chanserv_conf.support_channels.list[ii]);
+    free(chanserv_conf.support_channels.list);
+    dict_delete(handle_dnrs);
+    dict_delete(plain_dnrs);
+    dict_delete(mask_dnrs);
+    dict_delete(note_types);
+    free_string_list(chanserv_conf.eightball);
+    free_string_list(chanserv_conf.old_ban_names);
+    free_string_list(chanserv_conf.set_shows);
+    free(set_shows_list.list);
+    free(uset_shows_list.list);
+    while(helperList)
+    {
+        struct userData *helper = helperList;
+        helperList = helperList->next;
+        free(helper);
+    }
+}
+
+#define DEFINE_COMMAND(NAME, MIN_ARGC, FLAGS, OPTIONS...) modcmd_register(chanserv_module, #NAME, cmd_##NAME, MIN_ARGC, FLAGS, ## OPTIONS)
+#define DEFINE_CHANNEL_OPTION(NAME) modcmd_register(chanserv_module, "set "#NAME, chan_opt_##NAME, 1, 0, NULL)
+#define DEFINE_USER_OPTION(NAME) modcmd_register(chanserv_module, "uset "#NAME, user_opt_##NAME, 1, MODCMD_REQUIRE_REGCHAN, NULL)
+
+void
+init_chanserv(const char *nick)
+{
+    chanserv = AddService(nick, "Channel Services");
+    CS_LOG = log_register_type("ChanServ", "file:chanserv.log");
+    conf_register_reload(chanserv_conf_read);
+
+    reg_server_link_func(handle_server_link);
+
+    reg_new_channel_func(handle_new_channel);
+    reg_join_func(handle_join);
+    reg_part_func(handle_part);
+    reg_kick_func(handle_kick);
+    reg_topic_func(handle_topic);
+    reg_mode_change_func(handle_mode);
+    reg_nick_change_func(handle_nick_change);
+
+    reg_auth_func(handle_auth);
+    reg_handle_rename_func(handle_rename);
+    reg_unreg_func(handle_unreg);
+
+    handle_dnrs = dict_new();
+    dict_set_free_data(handle_dnrs, free);
+    plain_dnrs = dict_new();
+    dict_set_free_data(plain_dnrs, free);
+    mask_dnrs = dict_new();
+    dict_set_free_data(mask_dnrs, free);
+
+    reg_svccmd_unbind_func(handle_svccmd_unbind);
+    chanserv_module = module_register("ChanServ", CS_LOG, "chanserv.help", chanserv_expand_variable);
+    DEFINE_COMMAND(register, 1, MODCMD_REQUIRE_AUTHED, "flags", "+acceptchan,+helping", NULL);
+    DEFINE_COMMAND(noregister, 1, MODCMD_REQUIRE_AUTHED, "flags", "+helping", NULL);
+    DEFINE_COMMAND(allowregister, 2, 0, "template", "noregister", NULL);
+    DEFINE_COMMAND(move, 1, MODCMD_REQUIRE_AUTHED|MODCMD_REQUIRE_REGCHAN, "template", "register", NULL);
+    DEFINE_COMMAND(csuspend, 2, MODCMD_REQUIRE_AUTHED|MODCMD_REQUIRE_REGCHAN, "flags", "+helping", NULL);
+    DEFINE_COMMAND(cunsuspend, 1, MODCMD_REQUIRE_AUTHED|MODCMD_REQUIRE_REGCHAN, "flags", "+helping", NULL);
+    DEFINE_COMMAND(createnote, 5, 0, "level", "800", NULL);
+    DEFINE_COMMAND(removenote, 2, 0, "level", "800", NULL);
+
+    DEFINE_COMMAND(unregister, 1, MODCMD_REQUIRE_AUTHED|MODCMD_REQUIRE_REGCHAN, "flags", "+loghostmask", NULL);
+    DEFINE_COMMAND(merge, 2, MODCMD_REQUIRE_AUTHED|MODCMD_REQUIRE_REGCHAN, "access", "owner", NULL);
+
+    DEFINE_COMMAND(adduser, 3, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+    DEFINE_COMMAND(deluser, 2, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+    DEFINE_COMMAND(suspend, 2, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+    DEFINE_COMMAND(unsuspend, 2, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+    DEFINE_COMMAND(deleteme, 1, MODCMD_REQUIRE_CHANUSER, NULL);
+
+    DEFINE_COMMAND(mdelowner, 2, MODCMD_REQUIRE_CHANUSER, "flags", "+helping", NULL);
+    DEFINE_COMMAND(mdelcoowner, 2, MODCMD_REQUIRE_CHANUSER, "access", "owner", NULL);
+    DEFINE_COMMAND(mdelmaster, 2, MODCMD_REQUIRE_CHANUSER, "access", "coowner", NULL);
+    DEFINE_COMMAND(mdelop, 2, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+    DEFINE_COMMAND(mdelpeon, 2, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+
+    DEFINE_COMMAND(trim, 3, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+    DEFINE_COMMAND(opchan, 1, MODCMD_REQUIRE_REGCHAN, "access", "peon", NULL);
+    DEFINE_COMMAND(clvl, 3, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+    DEFINE_COMMAND(giveownership, 2, MODCMD_REQUIRE_CHANUSER, "access", "owner", "flags", "+loghostmask", NULL);
+
+    DEFINE_COMMAND(up, 1, MODCMD_REQUIRE_CHANUSER, NULL);
+    DEFINE_COMMAND(down, 1, MODCMD_REQUIRE_REGCHAN, NULL);
+    DEFINE_COMMAND(upall, 1, MODCMD_REQUIRE_AUTHED, NULL);
+    DEFINE_COMMAND(downall, 1, MODCMD_REQUIRE_AUTHED, NULL);
+    DEFINE_COMMAND(op, 2, MODCMD_REQUIRE_CHANNEL, "access", "op", NULL);
+    DEFINE_COMMAND(deop, 2, MODCMD_REQUIRE_CHANNEL, "template", "op", NULL);
+    DEFINE_COMMAND(voice, 2, MODCMD_REQUIRE_CHANNEL, "template", "op", NULL);
+    DEFINE_COMMAND(devoice, 2, MODCMD_REQUIRE_CHANNEL, "template", "op", NULL);
+
+    DEFINE_COMMAND(kickban, 2, MODCMD_REQUIRE_REGCHAN, "template", "op", NULL);
+    DEFINE_COMMAND(kick, 2, MODCMD_REQUIRE_REGCHAN, "template", "op", NULL);
+    DEFINE_COMMAND(ban, 2, MODCMD_REQUIRE_REGCHAN, "template", "op", NULL);
+    DEFINE_COMMAND(unban, 2, 0, "template", "op", NULL);
+    DEFINE_COMMAND(unbanall, 1, 0, "template", "op", NULL);
+    DEFINE_COMMAND(unbanme, 1, MODCMD_REQUIRE_CHANUSER, "template", "op", NULL);
+    DEFINE_COMMAND(open, 1, MODCMD_REQUIRE_CHANUSER, "template", "op", NULL);
+    DEFINE_COMMAND(topic, 1, MODCMD_REQUIRE_REGCHAN, "template", "op", "flags", "+never_csuspend", NULL);
+    DEFINE_COMMAND(mode, 1, MODCMD_REQUIRE_REGCHAN, "template", "op", NULL);
+    DEFINE_COMMAND(inviteme, 1, MODCMD_REQUIRE_CHANNEL, "access", "peon", NULL);
+    DEFINE_COMMAND(invite, 1, MODCMD_REQUIRE_CHANNEL, "access", "master", NULL);
+    DEFINE_COMMAND(set, 1, MODCMD_REQUIRE_CHANUSER, "access", "op", NULL);
+    DEFINE_COMMAND(wipeinfo, 2, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+    DEFINE_COMMAND(resync, 1, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
+
+    DEFINE_COMMAND(events, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog", "access", "coowner", NULL);
+    DEFINE_COMMAND(addban, 2, MODCMD_REQUIRE_REGCHAN, "access", "master", NULL);
+    DEFINE_COMMAND(addtimedban, 3, MODCMD_REQUIRE_REGCHAN, "access", "master", NULL);
+    DEFINE_COMMAND(delban, 2, MODCMD_REQUIRE_REGCHAN, "access", "master", NULL);
+    DEFINE_COMMAND(uset, 1, MODCMD_REQUIRE_CHANUSER, "access", "peon", NULL);
+
+    DEFINE_COMMAND(bans, 1, MODCMD_REQUIRE_REGCHAN, "access", "peon", "flags", "+nolog", NULL);
+    DEFINE_COMMAND(peek, 1, MODCMD_REQUIRE_REGCHAN, "access", "op", "flags", "+nolog", NULL);
+
+    DEFINE_COMMAND(access, 1, 0, "flags", "+nolog,+acceptchan", NULL);
+    DEFINE_COMMAND(users, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
+    DEFINE_COMMAND(wlist, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
+    DEFINE_COMMAND(clist, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
+    DEFINE_COMMAND(mlist, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
+    DEFINE_COMMAND(olist, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
+    DEFINE_COMMAND(plist, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
+    DEFINE_COMMAND(info, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
+    DEFINE_COMMAND(seen, 2, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
+    DEFINE_COMMAND(names, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
+
+    DEFINE_COMMAND(note, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+joinable,+acceptchan", NULL);
+    DEFINE_COMMAND(delnote, 2, MODCMD_REQUIRE_CHANUSER, NULL);
+
+    DEFINE_COMMAND(netinfo, 1, 0, "flags", "+nolog", NULL);
+    DEFINE_COMMAND(ircops, 1, 0, "flags", "+nolog", NULL);
+    DEFINE_COMMAND(helpers, 1, 0, "flags", "+nolog", NULL);
+    DEFINE_COMMAND(staff, 1, 0, "flags", "+nolog", NULL);
+
+    DEFINE_COMMAND(say, 2, 0, "flags", "+oper,+acceptchan", NULL);
+    DEFINE_COMMAND(emote, 2, 0, "flags", "+oper,+acceptchan", NULL);
+    DEFINE_COMMAND(expire, 1, 0, "flags", "+oper", NULL);
+    DEFINE_COMMAND(search, 3, 0, "flags", "+nolog,+helping", NULL);
+    DEFINE_COMMAND(unvisited, 1, 0, "flags", "+nolog,+helping", NULL);
+
+    DEFINE_COMMAND(unf, 1, 0, "flags", "+nolog,+toy,+acceptchan", NULL);
+    DEFINE_COMMAND(ping, 1, 0, "flags", "+nolog,+toy,+acceptchan", NULL);
+    DEFINE_COMMAND(wut, 1, 0, "flags", "+nolog,+toy,+acceptchan", NULL);
+    DEFINE_COMMAND(8ball, 1, 0, "flags", "+nolog,+toy,+acceptchan", NULL);
+    DEFINE_COMMAND(d, 1, 0, "flags", "+nolog,+toy,+acceptchan", NULL);
+    DEFINE_COMMAND(huggle, 1, 0, "flags", "+nolog,+toy,+acceptchan", NULL);
+
+    /* Channel options */
+    DEFINE_CHANNEL_OPTION(defaulttopic);
+    DEFINE_CHANNEL_OPTION(topicmask);
+    DEFINE_CHANNEL_OPTION(greeting);
+    DEFINE_CHANNEL_OPTION(usergreeting);
+    DEFINE_CHANNEL_OPTION(modes);
+    DEFINE_CHANNEL_OPTION(enfops);
+    DEFINE_CHANNEL_OPTION(giveops);
+    DEFINE_CHANNEL_OPTION(protect);
+    DEFINE_CHANNEL_OPTION(enfmodes);
+    DEFINE_CHANNEL_OPTION(enftopic);
+    DEFINE_CHANNEL_OPTION(pubcmd);
+    DEFINE_CHANNEL_OPTION(voice);
+    DEFINE_CHANNEL_OPTION(userinfo);
+    DEFINE_CHANNEL_OPTION(dynlimit);
+    DEFINE_CHANNEL_OPTION(topicsnarf);
+    DEFINE_CHANNEL_OPTION(nodelete);
+    DEFINE_CHANNEL_OPTION(toys);
+    DEFINE_CHANNEL_OPTION(setters);
+    DEFINE_CHANNEL_OPTION(topicrefresh);
+    DEFINE_CHANNEL_OPTION(ctcpusers);
+    DEFINE_CHANNEL_OPTION(ctcpreaction);
+    DEFINE_CHANNEL_OPTION(peoninvite);
+    modcmd_register(chanserv_module, "set defaults", chan_opt_defaults, 1, 0, "access", "owner", NULL);
+
+    /* Alias set topic to set defaulttopic for compatibility. */
+    modcmd_register(chanserv_module, "set topic", chan_opt_defaulttopic, 1, 0, NULL);
+
+    /* User options */
+    DEFINE_USER_OPTION(noautoop);
+    DEFINE_USER_OPTION(autoinvite);
+    DEFINE_USER_OPTION(info);
+
+    /* Alias uset autovoice to uset autoop. */
+    modcmd_register(chanserv_module, "uset noautovoice", user_opt_noautoop, 1, 0, NULL);
+
+    note_types = dict_new();
+    dict_set_free_data(note_types, chanserv_deref_note_type);
+    saxdb_register("ChanServ", chanserv_saxdb_read, chanserv_saxdb_write);
+    reg_chanmsg_func('\001', chanserv, chanserv_ctcp_check);
+
+    if(chanserv_conf.channel_expire_frequency)
+       timeq_add(now + chanserv_conf.channel_expire_frequency, expire_channels, NULL);
+
+    if(chanserv_conf.refresh_period)
+    {
+        time_t next_refresh;
+        next_refresh = (now + chanserv_conf.refresh_period - 1) / chanserv_conf.refresh_period * chanserv_conf.refresh_period;
+        timeq_add(next_refresh, chanserv_refresh_topics, NULL);
+    }
+    
+    reg_exit_func(chanserv_db_cleanup);
+    message_register_table(msgtab);
+}
diff --git a/src/chanserv.h b/src/chanserv.h
new file mode 100644 (file)
index 0000000..4f6aaa6
--- /dev/null
@@ -0,0 +1,168 @@
+/* chanserv.h - Channel service bot
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef _chanserv_h
+#define _chanserv_h
+
+#include "nickserv.h"
+
+enum UL_ALIASES {
+    UL_PEON = 100,
+    UL_OP = 200,
+    UL_MASTER = 300,
+    UL_PRESENT = UL_MASTER,
+    UL_COOWNER = 400,
+    UL_OWNER = 500,
+    UL_HELPER = 600,
+};
+
+enum levelOption {
+    lvlGiveOps,
+    lvlEnfOps,
+    lvlEnfModes,
+    lvlEnfTopic,
+    lvlPubCmd,
+    lvlSetters,
+    lvlCTCPUsers,
+    NUM_LEVEL_OPTIONS
+};
+
+enum charOption {
+    chProtect,
+    chToys,
+    chTopicRefresh,
+    chCTCPReaction,
+    NUM_CHAR_OPTIONS
+};
+
+#define CHANNEL_NODELETE       0x00000001 /* (1 << 0) */
+#define CHANNEL_SUSPENDED      0x00000002 /* (1 << 1) */
+#define CHANNEL_INFO_LINES     0x00000004 /* (1 << 2) */
+#define CHANNEL_VOICE_ALL      0x00000008 /* (1 << 3) */
+/* No longer used. */                      /* (1 << 4) */
+#define CHANNEL_DYNAMIC_LIMIT  0x00000020 /* (1 << 5) */
+#define CHANNEL_TOPIC_SNARF    0x00000040 /* (1 << 6) */
+#define CHANNEL_PEON_INVITE     0x00000080 /* (1 << 7) */
+/* Flags with values over 0x20000000 or (1 << 29) will not work
+ * because chanData.flags is a 30-bit field.
+ */
+
+#define IsProtected(x)         ((x)->flags & CHANNEL_NODELETE)
+#define IsSuspended(x)         ((x)->flags & CHANNEL_SUSPENDED)
+
+struct chanData
+{
+    struct chanNode    *channel;
+    struct mod_chanmode modes;
+
+    time_t             registered;
+    time_t             visited;
+    time_t             limitAdjusted;
+
+    char               *topic;
+    char               *greeting;
+    char               *user_greeting;
+    char               *registrar;
+    char                *topic_mask;
+
+    unsigned int       flags : 30;
+    unsigned int        may_opchan : 1;
+    unsigned int        max;
+    unsigned int        last_refresh;
+    unsigned short      banCount;
+    unsigned short      userCount;
+    unsigned short      lvlOpts[NUM_LEVEL_OPTIONS];
+    unsigned char       chOpts[NUM_CHAR_OPTIONS];
+
+    struct userData    *users;
+    struct banData     *bans;
+    struct dict         *notes;
+    struct suspended   *suspended;
+    struct chanData    *prev;
+    struct chanData    *next;
+};
+
+#define USER_AUTO_OP            0x00000001
+#define USER_SUSPENDED          0x00000002
+#define USER_AUTO_INVITE        0x00000004
+#define USER_FLAGS_SIZE         7
+
+#define IsUserAutoOp(USER)      (!((USER)->flags & USER_AUTO_OP))
+#define IsUserSuspended(USER)   ((USER)->flags & USER_SUSPENDED)
+#define IsUserAutoInvite(USER)  ((USER)->flags & USER_AUTO_INVITE)
+
+struct userData
+{
+    struct handle_info *handle;
+    struct chanData    *channel;
+
+    char               *info;
+    time_t             seen;
+    unsigned short      access;
+    unsigned int       present : 1;
+    unsigned int        flags : USER_FLAGS_SIZE;
+
+    /* linked list of userDatas for a chanData */
+    struct userData    *prev;
+    struct userData    *next;
+    /* linked list of userDatas for a handle_info */
+    struct userData     *u_prev;
+    struct userData     *u_next;
+};
+
+struct banData
+{
+    char               mask[NICKLEN + USERLEN + HOSTLEN + 3];
+    char               owner[NICKLEN+1];
+    struct chanData     *channel;
+
+    time_t             set;
+    time_t             triggered;
+    time_t              expires;
+
+    char               *reason;
+
+    struct banData     *prev;
+    struct banData     *next;
+};
+
+struct suspended
+{
+    struct chanData    *cData;
+    char               *suspender;
+    char                *reason;
+    time_t              issued, expires, revoked;
+    struct suspended    *previous;
+};
+
+struct do_not_register
+{
+    char   chan_name[CHANNELLEN+1];
+    char   setter[NICKSERV_HANDLE_LEN+1];
+    time_t set; 
+    char   reason[1];
+};
+
+void init_chanserv(const char *nick);
+void del_channel_user(struct userData *user, int do_gc);
+struct channelList *chanserv_support_channels(void);
+unsigned short user_level_from_name(const char *name, unsigned short clamp_level);
+struct do_not_register *chanserv_is_dnr(const char *chan_name, struct handle_info *handle);
+int check_user_level(struct chanNode *channel, struct userNode *user, enum levelOption opt, int allow_override, int exempt_owner);
+
+#endif
diff --git a/src/chanserv.help b/src/chanserv.help
new file mode 100644 (file)
index 0000000..82bca01
--- /dev/null
@@ -0,0 +1,512 @@
+"<INDEX>" ("$b$C Help$b",
+        "$b$C$b is a channel service bot, intended primarily to prevent and defend against channel takeovers. It also includes convenience features aimed at making it easier to maintain control over all aspects of your channel.",
+        "$b$C$b command categories:",
+        "  USER            User management.",
+        "  CHANNEL         Channel management.",
+        "  BAN MANAGEMENT  Ban management.",
+        "  INFORMATION     Informative commands.",
+        "  OPER            Helper/Operator commands."
+);
+"USER" ("$bUser commands:$b",
+        "  ACCESS      Check your own or another person's access to a channel.",
+        "  ADDCOOWNER  Give another person coowner status in a channel.",
+        "  ADDMASTER                       master status in a channel.",
+        "  ADDOP                           op status in a channel.",
+        "  ADDPEON                         peon status in a channel.",
+        "  GIVEOWNERSHIP    Give ownership to another user in the channel.",
+        "  CLVL        Change a person's access level in a channel.",
+        "  ADDUSER     Give another person access in a channel.",
+        "  DELUSER     Remove a person's access from a channel.",
+        "  DELETEME    Remove your own access from a channel.",
+        "  MDELCOOWNER Remove coowners with accounts matching a mask.",
+        "  MDELMASTER         masters with accounts matching a mask.",
+        "  MDELOP             ops with accounts matching a mask.",
+        "  MDELPEON           peons with accounts matching a mask.",
+       "  TRIM        Remove users inactive for a certain period.",
+        "  UP          Obtain ops in a channel you have access to.",
+        "  DOWN        Remove ops",
+        "  UPALL       Obtain ops in all channels you have access to.",
+        "  DOWNALL     Remove ops in all",
+        "  OP          Give ops to the specified user.",
+        "  DEOP        Remove ops from the specified user.",
+        "  VOICE       Give voice to the specified user.",
+        "  DEVOICE     Remove voice from the specified user.",
+        "  USET        Set channel user options.",
+        "  SUSPEND     Suspend a user's access to a channel.",
+        "  UNSUSPEND   Restore a user's access to a channel.",
+        "  WIPEINFO    Remove a lower-ranked user's infoline."
+);
+"BAN MANAGEMENT" ("$bBan Management Commands:$b",
+        "  KICK        Kick a user from a channel.",
+        "  BAN         Ban a user from a channel.",
+        "  KICKBAN     Kick and ban a user from a channel.",
+        "  BANS        List lasting bans in a channel",
+        "  ADDBAN      Add a permanent ban for a user.",
+        "  ADDTIMEDBAN Add a ban that expires in the specified time.",
+        "  UNBAN       Remove the specified ban from the channel.",
+        "  DELBAN      Remove the specified permanent ban from memory.",
+       "  TRIM        Remove bans inactive for a certain period.",
+        "  UNBANME     Remove a ban matching your hostmask from specified channel.",
+        "  UNBANALL    Remove all bans from a channel."
+);
+"CHANNEL" ("$bChannel Management Commands:$b",
+        "  OPEN        Remove +ilk channel modes and any bans on you from a channel.",
+        "  USERS       List all users of a channel.",
+        "  CLIST                coowners of a channel.",
+        "  MLIST                masters of a channel.",
+        "  OLIST                ops of a channel.",
+        "  PLIST                peons of a channel.",    
+        "  BANS        List all the bans for a channel.",
+        "  TOPIC       Set the current topic, or reset it to the default topic.",
+        "  MODE        Change a channel mode.",
+        "  INVITE      Invite new users to your channel.",
+        "  INFO        Show numerical information about the users in a channel.",
+        "  SET         Change various channel settings.",
+        "  EVENTS      View a list of events relevant to a channel.",
+        "  NOTE        Set a note on a channel.",
+        "  DELNOTE     Remove a note from a channel.",
+        "  RESYNC      Synchronize ops and voice with the channel userlist."
+);
+"INFORMATION" ("$bInformative Commands:$b",
+        "  VERSION     Check the current running version of $C",
+        "  NETINFO     Check current network-wide information.",
+        "  STAFF       Get a list of all the current staff.",
+        "  IRCOPS                                    IRC operators.",
+        "  HELPERS                                   support helpers.",
+        "  PEEK        Reveal information on a channel's modes, topic and ops.",
+        "  SEEN        Find out the last time a user was in a channel.",
+        "  COMMAND     Display some information about a command."
+);
+"OPER" ("$bHelper/IRC Operator commands:$b",
+        "  SAY         Have $C say a message in a channel.",
+        "  EMOTE       Equivalent to $C doing a /me in a channel.",
+        "  GOD         Turn security override on/off.",
+        "  EXPIRE      Automatically unregister old channels.",
+        "  CSUSPEND    Remove $C from a channel (preserving user data).",
+        "  CUNSUSPEND  Restore $C to a channel that was suspended.",
+        "  UNVISITED   List all channels that have not been visited in specified duration.",
+       "  MERGE       Merges a source and target channels' registration, users, bans, and other data into the target channel.",
+        "  MOVE        Transition one channel's registration to a new channel name.",
+        "  OPCHAN      Force $C to op itself in a channel.",
+        "  REGISTER    Register a new channel with $C.",
+        "  NOREGISTER  Add a channel to the do-not-register list.",
+        "  ALLOWREGISTER Remove a channel from the do-not-register list.",
+        "  UNREGISTER  Remove $C from a registered channel.",
+        "  SEARCH      Find registered channel matching criteria.",
+        "  ADDOWNER    Add a new owner to a channel.",
+        "  DELOWNER    Remove a current owner from a channel.",
+        "  MDELOWNER   Remove multiple owners by account mask from a channel.",
+        "  CREATENOTE  Create a new note type.",
+        "  REMOVENOTE  Remove an existing note type."
+);
+"ACCESS" ("/msg $C ACCESS <#channel> <nick|*account>",
+        "Reports various pieces of information about a channel user, including channel and network access level, and the user's info line. If no nick or account is provided, $C returns your own information.",
+        "If no channel is provided, $C lists your infolines in all registered channels.",
+        "$uSee Also:$u users");
+"ADDBAN" ("/msg $C ADDBAN <#channel> <mask|nick> [Reason]",
+        "Adds a ban to the channels permanent ban list, remaining in effect until removed with the DELBAN command. If it exactly matches an existing ban already in the list, the reason will be updated. If the existing ban was a timed ban, it will be extended into a permanent ban.",
+        "$uSee Also:$u bans, delban, mdelban");
+"ADDUSER" ("/msg $C ADDUSER <#channel> <level> <nick|*account>",
+        "This command adds someone to the channel user list with the specified access level.  (You may only add users to levels less than your own.)",
+        "The level may be one of $bpeon$b, $bop$b, $bmaster$b, $bcoowner$b or $bowner$b (only network staff may add owners).",
+        "$uSee Also:$u deluser, users");
+"ADDTIMEDBAN" ("/msg $C ADDTIMEDBAN <#channel> <mask|nick> <Duration> [Reason]",
+        "Adds an automatically expiring ban to the channel ban list. This command behaves in the exact same fashion as ADDBAN with the exception that the bans are automatically removed after the user-supplied duration. If it exactly matches an existing ban already in the list, the reason will be updated. If the existing ban was a timed ban, it will be extended. Timed bans can be removed with the DELBAN command, as with permanent bans.",
+        "$uSee Also:$u addban, bans, delban, durations");
+"ALLOWREGISTER" ("/msg $C ALLOWREGISTER <#channel|*Account>",
+        "Removes the named channel (or channel mask) from the do-not-register list.",
+        "$uSee Also:$u register, noregister, unregister");
+"BAN" ("/msg $C BAN <#channel> <mask|nick>",
+        "This command will temporarily add a ban for the user specified as the parameter. Masks are to be supplied in the format <Nick>!<Ident>@<Host> and usually contain wildcards. If a nick is specified, a mask is automatically generated (though not completely foolproof). This ban is removed either by removing it from the channel ban list using any irc client, or sending the UNBAN or UNBANALL commands. If you are banned with this method, the UNBANME command can be used.",
+        "$uFor assistance, please join to #support$u",
+        "Example: *!*serv@*.gamesnet.net would ban anyone with ident 'serv' and a gamesnet.net hostname from joining the channel.",
+        "$uSee Also:$u unban, unbanall, unbanme");
+"BANS" ("/msg $C BANS <#channel>",
+        "This command lists all permanent and timed bans in the channel.",
+        "$uSee Also:$u addban, delban, mdelban");
+"CLIST" ("/msg $C CLIST <#channel> [mask]",
+        "This command lists all users of level $bCoowner$b on a channel's userlist. If a mask is supplied, only coowners matching the mask will be shown.",
+        "$uSee Also:$u addcoowner, delcoowner, mdelcoowner, users");
+"CLVL" ("/msg $C CLVL <#channel> <nick|*account> <level>",
+        "Modifies a channel user's access level. You cannot give users access greater than or equal to your own.",
+       "You may use *Account instead of Nick as the name argument; the * makes $C use the name of a account directly (useful if the user is not online).",
+        "$uSee Also:$u access, users, giveownership");
+"CREATENOTE" ("/msg $S CREATENOTE <typename> <set-access> [access-arg] <view-access> <max-length>",
+        "Defines a new note type.  $btypename$b is the name of the note type.  $bset-access$b is one of:",
+        "  $bPRIVILEGED$b with required $baccess-arg$b being the minimum OpServ level to set",
+        "  $bCHANNEL   $b with required $baccess-arg$b being the channel access level to set",
+        "  $bSETTER    $b (with no access-arg) to allow anyone who can !set to set",
+        "$bview-access$b is one of $bPRIVILEGED$b, $bCHANNEL_USERS$b, or $bALL$b, that determines who can see the note (if it is set).",
+        "$bmax-length$b is the maximum length for this kind of note.",
+        "If the note type already exists, it is modified with the new values you specify.",
+        "$uSee Also:$u removenote");
+"GIVEOWNERSHIP" ("/msg $C GIVEOWNERSHIP <#channel> <nick|*account>",
+        "Transfer ownership of the channel from you to another user on the channel's userlist.  You are demoted to co-owner, and they are promoted to owner.",
+       "You may use *Account instead of Nick as the name argument; the * makes $C use the name of a account directly (useful if the user is not online).",
+        "$uSee Also:$u clvl, access, users");
+"CSUSPEND" ("/msg $C CSUSPEND <#channel> [!]<duration> <reason>",
+        "This command will temporarily remove $b$C$b from a channel and suspend its registration.",
+        "The duration may be \"0\" to make it never expire; otherwise, $C will automatically unsuspend the channel after $uduration$u.",
+        "If you wish to modify a currently existing suspension, add a ! before the duration.",
+        "$uSee Also:$u unregister, cunsuspend, durations");
+"CUNSUSPEND" ("/msg $C CUNSUSPEND <#channel>",
+        "Restores a channel's $b$C$b registration.",
+        "$bSee Also:$b csuspend, unregister");
+"DELBAN" ("/msg $C DELBAN <#channel> <mask|nick>",
+        "Deletes a ban from the channel ban list. This command works for both permanent and timed bans alike.",
+        "$uSee Also:$u addban, addtimedban, bans");
+"DELNOTE" ("/msg $C DELNOTE <#channel> <note-name>",
+        "Deletes a note from the channel.  To do this, you must be able to set the note.",
+        "$uSee Also:$u note, note types");
+"DELUSER" ("/msg $C DELUSER <#channel> <nick|*account>",
+        "Deletes a user from the channel user list.",
+        "You may use *Account instead of Nick as the name argument; the * makes $C use the name of a account directly (useful if the user is not online).",
+        "$uSee Also:$u adduser, deleteme, users");
+"DELETEME" ("/msg $C DELETEME <#channel> [<secret>]",
+        "If you have less than owner access in a channel, $bdeleteme$b removes your access from the channel.  $bIf you do this by mistake, you must find a higher-ranked user to re-add you.$b",
+        "The secret value changes for each channel and each user.  If you do not include the secret value, $C will tell you what it should be.",
+        "$uSee Also:$u adduser, deluser, giveownership, users");
+"DEOP" ("/msg $C DEOP <#channel> <nick> [nick]...",
+        "Deops the specified user[s].",
+        "$uSee Also:$u down, op");
+"DEVOICE" ("/msg $C DEVOICE <#channel> <nick> [nick]...",
+        "This command will make $C devoice the selected user[s].",
+        "$uSee Also:$u addpeon, delpeon, deop, voice");
+"DOWN" ("/msg $C DOWN <#channel>",
+        "This command will devoice/deop you in the selected channel.",
+        "$uSee Also:$u downall, up, upall");
+"DOWNALL" ("/msg $C DOWNALL",
+        "Executes the $bdown$b command for each channel you have access to.",
+        "$uSee Also:$u down, up, upall");
+"DURATIONS" ("[<n>y][<n>M][<n>w][<n>d][<n>h][<n>m][<n>[s]]",
+        "There is a standard syntax for durations that let you easily specify longer periods of time.  A duration consists of one or more \"duration parts\", which are sequences of digits, separated by duration letters.",
+        "The valid duration letters are $by$b (for year, or 365 days), $bM$b (for month, or 31 days), $bw$b (for week, or 7 days), $bd$b (for day, or 24 hours), $bh$b (for hour, or 60 minutes), $bm$b (for minute, or 60 seconds), and $bs$b (for second).  We know not all years are 365 days and not all months are 31 days, but we pretend they are for parsing durations.",
+        "If the last duration part does not have any letter to indicate the units, seconds are assumed.",
+        "$uExamples$u: 1y1M is 365 days plus 31 days; 1y1m is 365 days plus 1 minute; 1h30m45 is ninety minutes and forty-five seconds; and so forth.");
+"EMOTE" ("/msg $C EMOTE <#channel> <text>",
+        "Makes $b$C$b send a CTCP ACTION message to the specified channel.",
+        "$uSee Also:$u say");
+"EVENTS" ("/msg $C EVENTS <#channel> [limit [pattern]]",
+        "Allows channel coowners to view a list of events related to their channel. If a pattern is provided, only events with a matching description will be displayed.",
+       "$bNote:$b You must specify a limit if you want to use a pattern to match against.",
+        "$uSee Also:$u last");
+"EXPIRE" ("/msg $C EXPIRE",
+        "Expires any channels that have not been visited within the configured duration. Channel registrations are automatically expired, so you only need to use this command if the duration has been changed.",
+        "$uSee Also:$u unvisited");
+"HELPERS" ("/msg $C HELPERS",
+        "Lists all the helpers currently online. Nicknames enclosed in parentheses are away, and likely unavailable.",
+        "$uSee Also:$u staff");
+"INFO" ("/msg $C INFO <#channel>",
+        "This command responds with various pieces of information about a channel's users, status, and registration.");
+"INVITE" ("/msg $C INVITE <#channel> [nick [reason]]",
+        "Invites a user into the channel, sending them a reason if one is provided. However, unless you have at least $bmaster$b access, you can only invite yourself with this command. If no nick is provided, the user issuing the command will be invited.");
+"IRCOPS" ("/msg $C IRCOPS",
+        "Lists all the IRC operators currently online. Nicknames enclosed in parentheses are away, and likely unavailable.",
+        "$uSee Also:$u staff");
+"KICK" ("/msg $C KICK <#channel> <mask|nick> [reason]",
+        "Kicks the users matching the given nick or mask with the specified reason. If no reason is provided, a default will be used.",
+        "$bSee Also:$b kickban");
+"KICKBAN" ("/msg $C KICKBAN <#channel> <mask|nick> [reason]",
+        "Kicks and bans with the specified reason any users with a matching nick or hostmask. If no reason is provided, a default one will be used.",
+        "$bSee Also:$b addban, kick");
+"MDELCOOWNER" ("/msg $C MDELCOOWNER <#channel> <pattern>",
+        "Deletes all coowners with accounts matching the given pattern from the channel user list.",
+        "$bSee Also:$b clist, delcoowner");
+"MDELMASTER" ("/msg $C MDELMASTER <#channel> <pattern>",
+        "Deletes all masters with accounts matching the given pattern from the channel user list.",
+        "$bSee Also:$b mdelban, mdelcoowner, mdelop, mdelowner, mdelpeon");
+"MDELOP" ("/msg $C MDELOP <#channel> <pattern>",
+        "Deletes all ops with accounts matching the given pattern from the channel user list.");
+"MDELOWNER" ("/msg $C MDELOWNER <#channel> <pattern>",
+        "Deletes all owners with accounts matching the given pattern from the channel user list.",
+        "$uSee Also:$u addowner, mdelcoowner, mdelmaster, mdelop, mdelpeon");
+"MDELPEON" ("/msg $C MDELPEON <#channel> <pattern>",
+        "Deletes all peons with accounts matching the given pattern from the channel user list.",
+        "$uSee Also:$u addpeon, mdelcoowner, mdelmaster, mdelop");
+"MERGE" ("/msg $C MERGE <#channel> <destination>",
+        "Merges the source channel's registration, users, bans, and other data into the target channel. Users with access to both the source and target channels will retain the higher access level; if the access levels are the same, the more recent seen time is kept. Bans are also merged, with bans expiring later taking precedence.",
+        "$uSee Also:$u register, move, unregister");
+"MLIST" ("/msg $C MLIST <#channel> [mask]",
+        "This command lists all users of level $bMaster$b on a channel's userlist. If a mask is supplied, only masters matching the mask will be shown.",
+        "$uSee Also:$u addmaster, delmaster, mdelmaster, users");
+"MODE" ("/msg $C MODE <#channel>",
+        "Resets the modes in the channel to their default.",
+        "$uSee Also:$u open, set");
+"MOVE" ("/msg $C MOVE <#channel> <destination>",
+        "Transfers a channel's registration to a different channel along with the settings and user/ban lists. All the restrictions that apply for the $bregister$b command apply to $bmove$b.",
+        "$uSee Also:$u register, merge, unregister");
+"NETINFO" ("/msg $C NETINFO",
+        "Displays some assorted pieces of information about network-wide $b$C$b statistics.",
+        "$uSee Also:$u info, staff");
+"NOREGISTER" ("/msg $C NOREGISTER <#channel|*Account> <reason>",
+        "With no arguments, lists the current do-not-register channels.",
+        "With arguments, adds a do-not-register channel (or account) with the specified reason.  In this case, the channel name may include * or ? wildcards.",
+        "$uSee Also:$u allowregister, register, unregister");
+"NOTE" ("/msg $C NOTE <#channel> [<type> [new-note-text]]",
+        "With no arguments, lists all visible notes on the specified channel.",
+        "With one argument, lists the note type you name (if it is visible to you).",
+        "With two arguments, sets the note type (if you can set it).",
+        "$uSee Also:$u delnote, note types");
+"NOTE TYPES" ("$bNOTE TYPES$b",
+        "${notes}",
+        "$uSee Also:$u note, delnote");
+"OLIST" ("/msg $C OLIST <#channel> [mask]",
+        "This command lists all users of level $bOp$b on a channel's userlist.  If a mask is supplied, only ops matching the mask will be shown.",
+        "$uSee Also:$u addop, delop, mdelop, users");
+"OP" ("/msg $C OP <#channel> <nick> [nick]...",
+        "This command makes $C op the specified user.",
+        "$uSee Also:$u addop, delop, deop");
+"OPCHAN" ("/msg $C OPCHAN <#channel>",
+        "This command makes $C op itself in the specified channel.");
+"OPEN" ("/msg $C OPEN <#channel>",
+        "This command will make $C remove all modes preventing a user from the specified channel, and remove all bans matching the users hostmask.",
+        "$uSee Also:$u unbanme");
+"PEEK" ("/msg $C PEEK <#channel>",
+        "Displays the current topic, modes, and ops of the specified channel. Unlike $binfo$b, $bpeek$b displays channel information unrelated to $b$C$b.",
+        "$uSee Also:$u info");
+"PLIST" ("/msg $C PLIST <#channel>",
+        "This command lists all users of level $bPeon$b on a channel's userlist. If a mask is supplied, only peons matching the mask will be shown.",
+        "$uSee Also:$u addpeon, delpeon, mdelpeon, users");
+"REGISTER" ("/msg $C REGISTER <#channel> [user|*account] [force]",
+        "Registers a channel with $b$C$b, automatically granting owner access to the specified user. If no user is provided, $b$C$b gives owner access to the user executing the command.",
+        "If the registrar is on the network staff and provides the third argument, $bforce$b, it will allow a do-not-register channel to be registered anyway.",
+        "In addition, $bregister$b will only allow one user to own a certain number of channels without the $bforce$b argument.",
+        "$uSee Also:$u addowner, noregister, unregister");
+"REMOVENOTE" ("/msg $S REMOVENOTE <typename> [FORCE]",
+        "Permanently deletes a note type.  Without the argument $bFORCE$b, it will only delete an unused note type.  With the argument $bFORCE$b, it will delete the note from all channels and then delete the note type.",
+        "$uSee Also:$u createnote");
+"RESYNC" ("/msg $S RESYNC <#channel>",
+        "Synchronizes users in the channel with the userlist.  This means that if the user can normally get ops, $S makes sure the user has ops.  Otherwise, if the user normally gets voice, $S makes sure the user has voice but not ops.  Otherwise, $S makes sure the user has neither voice nor ops.");
+"SAY" ("/msg $C SAY <#channel> <text>",
+        "Makes $b$C$b send a message to the specified channel.",
+        "$uSee Also:$u emote");
+"SEARCH CRITERIA" ("$bSEARCH CRITERIA$b",
+        "The following criteria may be used:",
+        "  NAME        Channels whose names match the given mask",
+        "  REGISTRAR   Channels whose registrar's account matches the given mask",
+        "  UNVISITED   Channels that have not been visited in at least the given duration",
+        "  REGISTERED  Channels that have been registered for less than the given duration",
+        "  FLAGS       Matches channels with the specified flag set",
+        "  LIMIT       Limit the number of channels returned by the search",
+        "Flags that can be matched against are: nodelete and suspended.",
+        "$uSee Also:$u search, search actions");
+"SEARCH ACTIONS" ("$bSEARCH ACTIONS$b",
+       "The following are valid $bsearch$b actions:",
+       "  PRINT       Prints matching channels",
+       "  COUNT       Prints the number of matching channels",
+       "$uSee Also:$u search, search criteria");
+"SEARCH" ("/msg $C SEARCH <action> <criteria> <value> [<criteria> <value>]...",
+        "Searches for channels which match the specified criteria. For a list of search actions, see $bsearch actions$b. For a list of the criteria, see $bsearch criteria$b.",
+        "$uSee Also:$u search actions, search criteria");
+"SEEN" ("/msg $C SEEN <#channel> <account>",
+        "This command will tell you if the selected user is in the channel, or when was the last time the user was seen in the channel.",
+        "$uSee Also:$u access, users");
+"SET" ("/msg $C SET <#channel> [<parameter> [setting]]",
+        "This command will set various channel options.  With no arguments, it will show the current values of all channel options.",
+        "DEFAULTTOPIC: The channel's default topic.",
+        "TOPICMASK:    A pattern that topics must match.",
+        "GREETING:     A greeting message for visitors to the channel.",
+        "USERGREETING: A greeting message for users on the channel's userlist.",
+        "MODES:        The channel's default modes.",
+       "PUBCMD:       Restrictions to use commands in public.",
+        "STRICTOP:     Restrictions for opping users.",
+        "AUTOOP:       The users that $b$C$b will autoop.",
+        "ENFMODES:     Restrictions to change the default modes.",
+        "ENFTOPIC:     Restrictions on changing the topic.",
+        "PROTECT:      The protection level $b$C$b provides.",
+       "TOYS:         Toggles how $b$C$b will respond to toy commands (!8ball etc).",
+        "SETTERS:      Who may change channel settings (using $bSET$b).",
+        "TOPICREFRESH: Controls if (and how often) $C will reset the topic.",
+        "CTCPUSERS:    Who is allowed to send CTCPs to the channel.",
+        "CTCPREACTION: What happens when a disallowed CTCP is sent to the channel.",
+        "VOICE:        Toggles whether $b$C$b will autovoice people on join.",
+        "USERINFO:     Toggles whether or not infolines are displayed.",
+        "DYNLIMIT:     Adjusts user limit (+l channel mode) to prevent join floods.",
+        "TOPICSNARF:   Topics set manually (by /TOPIC #channel ...) change default $C topic",
+        "PEONINVITE:   Indicates whether peons may use the $bINVITEME$b command.",
+        "$bIRCOP ONLY$b:",
+        "NODELETE:  Prevents channel deletion.",
+        "If you wish to reset your channel to the default settings, you can use the $bSET DEFAULTS$b command.",
+        "$uSee Also:$u set pubcmd, set strictop, set autoop, set enfmodes, set enftopic, set protect, set toys, set setters, set topicrefresh, set ctcpusers, set ctcpreaction, set defaults");
+"SET DEFAULTTOPIC" ("/msg $C SET <#channel> DEFAULTTOPIC <New default topic>",
+        "This changes the default topic for the channel.  $C will set the IRC topic to this value when the $btopic$b command is used with no arguments, when the topic refresh happens (if you have $bset topicrefresh$b), or when an unauthorized user changes the topic to something else.",
+        "$uSee Also:$u set, set topicrefresh, set enftopic, set topicmask");
+"SET TOPICMASK" ("/msg $C SET <#channel> TOPICMASK <Topic mask with * and ?>",
+        "This sets a pattern that $C allows for the topic.  A $b*$b will match any number of characters (including 0); a $b?$b will match any single character -- the same as with IRC hostmasks.  A user may set a topic if either $btopicmask$b or $benftopic$b allow it.",
+        "For example, $b!set topicmask Hello *$b allows anyone to set the topic to be $bHello world$b, but they must be able to override the topic (according to the $bEnfTopic$b setting) to set the topic to $bGoodbye world$b.",
+        "You may \"escape\" those characters by putting a \\ before them in the topic mask; for example, $b!set topicmask Whassup\\?$b would only allow the topic to be $bWhassup?$b, while leaving out the \\ would allow the topic to be $bWhassup!$b (among other things).",
+        "For ease of use, if the TopicMask has only one * and no ?, then using $b!topic something$b will (if $bsomething$b does not match the TopicMask and you cannot override the topic lock) replace the * with the text $bsomething$b.",
+        "$uSee Also:$u set, set topic, set enftopic");
+"SET TOYS" ("/msg $C SET <#channel> TOYS <value>",
+       "This setting changes how $C will respond to commands like !8ball, or whether it responds at all.  Valid settings are:",
+       "$b0$b  Toys are completely disabled.",
+       "$b1$b  Toys will only reply privately.",
+       "$b2$b  Toys will reply publicly.",
+       "$uSee Also:$u set");
+"SET PUBCMD" ("/msg $C SET <#channel> PUBCMD <value>",
+        "This setting restricts the access necessary to use in-channel commands.  Valid settings are:",
+        "$b0$b  Anyone may use public commands",
+        "$b1$b  Peons and above may use public commands",
+        "$b2$b  Ops and above may use public commands",
+        "$b3$b  Masters and above may use public commands",
+        "$b4$b  Coowners and above may use public commands",
+        "$b5$b  Only owners may use public commands",
+        "$b6$b  No one may use public commands",
+        "$uSee Also:$u set");
+"SET STRICTOP" ("/msg $C SET <#channel> STRICTOP <value>",
+        "This setting restricts who may op users who are not at least ops on the userlist.  If $C sees someone with access below the specified access op someone not on the userlist (or who is a peon), it will deop the second user.  Valid settings are:",
+        "$b0$b  Anyone may op unknown users",
+        "$b1$b  Ops and above may op unknown users",
+        "$b2$b  Masters and above may op unknown users",
+        "$b3$b  Coowners and above may op unknown users",
+        "$b4$b  Only owners may op unknown users",
+        "$b5$b  No one may op unknown users",
+        "$uSee Also:$u set");
+"SET AUTOOP" ("/msg $C SET <#channel> AUTOOP <value>",
+        "This setting restricts the minimum access someone must be to be automatically op'ed by $C.  Valid settings are:",
+        "$b0$b  Everyone will be automatically op'ed",
+        "$b1$b  Ops and above will be automatically op'ed",
+        "$b2$b  Masters and above will be automatically op'ed",
+        "$b3$b  Coowners and above will be automatically op'ed",
+        "$b4$b  Only owners will be automatically op'ed",
+        "$b5$b  No one will be automatically op'ed",
+        "$uSee Also:$u set");
+"SET ENFMODES" ("/msg $C SET <#channel> ENFMODES <value>",
+        "This setting restricts the minimum access someone must have to change the channel modes from what is specified in the channel settings.  Valid settings are:",
+        "$b0$b  Anyone may override the mode lock",
+        "$b1$b  Ops and above may override the mode lock",
+        "$b2$b  Masters and above may override the mode lock",
+        "$b3$b  Coowners and above may override the mode lock",
+        "$b4$b  Only owners may override the mode lock",
+        "$b5$b  No one may override the mode lock",
+        "$uSee Also:$u set");
+"SET ENFTOPIC" ("/msg $C SET <#channel> ENFTOPIC <value>",
+        "This setting restricts the minimum access someone must have to change the channel topic.  Valid settings are:",
+        "$b0$b  Anyone may change the topic",
+        "$b1$b  Ops and above may change the topic",
+        "$b2$b  Masters and above may change the topic",
+        "$b3$b  Coowners and above may change the topic",
+        "$b4$b  Only owners may change the topic",
+        "$b5$b  No one may change the topic",
+        "If a topic mask is set, then a person may change the topic as long as it matches that mask $bor$b they have the above access.",
+        "If no topic mask is set, then a person must have the above access to change the topic from the default.",
+        "$uSee Also:$u set, set topic, set topicmask");
+"SET PROTECT" ("/msg $C SET <#channel> PROTECT <value>",
+        "This setting restricts the protection that $C enforces.  Valid settings are:",
+        "$b0$b  Non-users and users will be protected from those of equal or lower access",
+        "$b1$b  Users will be protected from those of equal or lower access.",
+        "$b2$b  Users will be protected from those of lower access.",
+        "$b3$b  No users will be protected.",
+        "$uSee Also:$u set");
+"SET SETTERS" ("/msg $C SET <#channel> SETTERS <value>",
+        "This setting restricts the protection that $C enforces.  Valid settings are:",
+        "$b0$b  Masters and above may change settings.",
+        "$b1$b  Co-owners and above may change settings.",
+        "$b2$b  Only owners may change settings.",
+        "$uSee Also:$u set");
+"SET TOPICREFRESH" ("/msg $C SET <#channel> TOPICREFRESH <value>",
+        "This setting controls if (and how often) $C refreshes the topic to the default value.  Valid settings are:",
+        "$b0$b  Never refresh topic.",
+        "$b1$b  Refresh every 3 hours.",
+        "$b2$b  Refresh every 6 hours.",
+        "$b3$b  Refresh every 12 hours.",
+        "$b4$b  Refresh every 24 hours.",
+        "$uSee Also:$u set, set topic");
+"SET CTCPUSERS" ("/msg $C SET <#channel> CTCPUSERS <value>",
+        "This setting controls who is allowed to send CTCPs to the channel.  CTCP ACTION, the way that /me is implemented, are always allowed:",
+        "$b0$b  Anyone may send CTCPs to the channel.",
+        "$b1$b  Peons and above may send CTCPs to the channel.",
+        "$b2$b  Ops and above may send CTCPs to the channel.",
+        "$b3$b  Masters and above may send CTCPs to the channel.",
+        "$b4$b  Coowners and above may send CTCPs to the channel.",
+        "$b5$b  Owners may send CTCPs to the channel.",
+        "$b6$b  No one may send CTCPs to the channel.",
+        "If a user below the specified level sends a CTCP (besides ACTION) to the channel, the enforcement is specified by the $bCTCPReaction$b setting.",
+        "$uSee Also:$u set, set ctcpreaction");
+"SET CTCPREACTION" ("/msg $C SET <#channel> CTCPREACTION <value>",
+        "This setting controls what happens to those who send disallowed CTCPs to the channel:",
+        "$b0$b  Kick on disallowed CTCPs.",
+        "$b1$b  Kickban on disallowed CTCPs.",
+        "$b2$b  Short timed ban (defaults to 3 minutes) on disallowed CTCPs.",
+        "$b3$b  Long timed ban (defaults to 1 hour) on disallowed CTCPs.",
+        "$uSee Also:$u set, set ctcpusers");
+"SET DEFAULTS" ("/msg $C SET <#channel> DEFAULTS [<confirmation>]",
+        "With the proper confirmation string, resets all the options for the channel to their default values.",
+        "With no confirmation string, displays the appropriate confirmation string.",
+        "$uSee Also:$u set");
+"STAFF" ("/msg $C STAFF",
+        "Lists all the IRC operators and helpers currently online. Nicknames enclosed in parentheses are away, and likely unavailable.",
+        "$uSee Also:$u helpers, ircops, netinfo");
+"SUSPEND" ("/msg $C SUSPEND <#channel> <nick|*account>",
+        "This disables the target's access to the channel.  That access can be restored using the unsuspend command.",
+        "$uSee Also:$u unsuspend, deluser");
+"TOPIC" ("/msg $C TOPIC <#channel> [topic]",
+        "Sets the current topic for the specified channel.  If no topic is specified, then set the current topic to the default topic.");
+"TRIM" ("/msg $C TRIM <#channel> <target> <duration>",
+       "The trim command removes target objects inactive for more than a certain duration from a channel. The target must be a channel access level, \"users\", or \"bans\". The duration argument specifies the amount of time the target has been inactive for to be removed.",
+        "$uSee Also:$u durations");
+"UNBAN" ("/msg $C UNBAN <#channel> <mask|nick>",
+        "Unbans the specified nick or hostmask. If a nick is given, $b$C$b determines what hostmask(s) to unban.",
+        "$bSee Also:$b ban, kick, kickban");
+"UNBANALL" ("/msg $C UNBANALL <#channel>",
+        "Clears the specified channel's banlist. If the channel is omitted, then $bunbanall$b will be done in the channel where the command was given.",
+        "$bSee Also:$b ban, unban, unbanme");
+"UNBANME" ("/msg $C UNBANME <#channel>",
+        "Unbans your hostmask from the specified channel.",
+        "$bSee Also:$b ban, unban");
+"UNREGISTER" ("/msg $C UNREGISTER <#channel>",
+        "Unregisters a channel that is registered with $b$C$b. $bIMPORTANT$b: Once the channel is unregistered, the userlist $bcannot$b be recovered.",
+        "If you are not network staff, you must add $bCONFIRM$b to the end of your line to confirm channel unregistration.",
+        "$bSee Also:$b register");
+"UNSUSPEND" ("/msg $C UNSUSPEND <#channel> <nick|*account>",
+        "This restores the target's access to the channel (after it has been suspended).",
+        "$uSee Also:$u suspend, deluser");
+"UNVISITED" ("/msg $C UNVISITED [duration] [limit]",
+        "Displays up to a certain limit, all channels registered with $b$C$b that have not been visited within a certain duration. If a duration is not provided, a default will be used.",
+        "$bSee Also:$b expire, search, durations");
+"UP" ("/msg $C UP <#channel>",
+        "For a peon, this will cause $b$C$b to voice the user executing the command. For anyone of level op on higher on the userlist, this will cause $b$C$b to op the user executing the command.");
+"UPALL" ("/msg $C UPALL",
+        "Executes the $bup$b command for each channel you have access in.",
+        "$uSee Also:$u up, down, downall");
+"USET"  ("/msg $C USET <#channel> [<option> [<setting>]]",
+         "The $buset$b command allows you to toggle various channel user settings. With no arguments, it will print the current values of all channel user options.",
+         "$bOptions:$b",
+         "INFO:       Sets the infoline that $C sends when you join the channel.",
+         "NOAUTOOP:   Enable or disable $C automatically opping you upon joining or authenticating.",
+         "AUTOINVITE: $C will invite you to +i/+k channels which you have access to and are not in when you authenticate if this setting is on.",
+         "NOTE: The NoAutoOp setting is equivalent to the !togop command in previous versions of srvx.",
+         "$uSee Also:$u set");
+"USET INFO" ("/msg $C USET <#channel> INFO <info>",
+        "This command will set a user defined information message to be displayed when you join the channel. By setting the message to '*', you will clear your message.",
+        "$uSee Also:$u access");
+"USERS" ("/msg $C USERS <#channel> [mask]",
+        "Displays the userlist for the specified channel. If a mask is supplied, only users matching the mask will be shown.",
+        "$uSee Also:$u clist, mlist, olist, plist, wlist");
+"VERSION" ("/msg $C VERSION",
+        "Sends you the srvx version and some additional version information that is specific to $b$C$b.");
+"VOICE" ("/msg $C VOICE <#channel> <nick> [nick]...",
+        "Voices the specified nick in the specified channel. If the channel is omitted, then $bvoice$b will be done in the channel where the command was given.",
+        "$uSee Also:$u devoice");
+"WIPEINFO" ("/msg $C WIPEINFO <#channel> <nick|*account>",
+        "Removes the named user's infoline in the channel.");
+"WLIST" ("/msg $C WLIST <#channel> [mask]",
+        "This command lists all users of level $bOwner$b on a channel's userlist. If a mask is supplied, only owners matching the mask will be shown.",
+        "$uSee Also:$u addcoowner, delcoowner, mdelcoowner, users");
+
+"I'VE FALLEN AND I CAN'T GET UP" ("$bHelp, I've fallen and I can't get up$b!",
+        "If you drank your milk, you might not feel so vulnerable right now.",
+        "So next time you're sitting in front of a tall frosty glass of milk, just think how much it will help build up your bone structure.");
+"ME"   "Help you? You're beyond help.";
+"Zoot" "Crash it again, please?";
+"Entrope" "C Code.  C Code Run.  Run, Code, Run!!    (please?)";
+"emacs" ("A novice of the temple once approached the Chief Priest with a question.",
+        "The novice asked, \"Master, does Emacs have the Buddha nature?\"",
+        "The Chief Priest had been in the temple for many years and could be relied upon to know these things.  He thought for several minutes before replying.",
+        "\"I don't see why not.  It's got bloody well everything else.\"",
+        "With that, the Chief Priest went to lunch.  The novice suddenly achieved enlightenment, several years later.");
+"thanks" ("The srvx developers would like to thank the following people for their help in making srvx as polished as it is today:",
+        "$bGamesNET IRC Network$b - All the users and staff there bear with our shortcomings and bugs and let us know what needs to be fixed.",
+        "$bIC5 Networks$b (and JohnM in particular) - Never afraid to critique things, even if GamesNET is the 800 pound gorilla.",
+        "$bMeeko, eraser, hock(ey), KilledInAction, MadEwokHerd, Milon and Shoat$b - Hardcore beta testing and bug finding on the testnet.",
+        "$bCrips$b - Reading through all the boring messages and finding ways to make them clearer.");
diff --git a/src/checkdb.c b/src/checkdb.c
new file mode 100644 (file)
index 0000000..45ef136
--- /dev/null
@@ -0,0 +1,118 @@
+#include "conf.h"
+#include "modcmd.h"
+#include "saxdb.h"
+#include "timeq.h"
+
+int bad;
+const char *hidden_host_suffix;
+
+/* because recdb likes to log stuff.. */
+void log_module(UNUSED_ARG(struct log_type *lt), UNUSED_ARG(enum log_severity ls), const char *format, ...)
+{
+    va_list va;
+    va_start(va, format);
+    vfprintf(stderr, format, va);
+    va_end(va);
+    bad = 1;
+}
+
+/* and because saxdb is tied in to lots of stuff.. */
+
+time_t now;
+
+void *conf_get_data(UNUSED_ARG(const char *full_path), UNUSED_ARG(enum recdb_type type)) {
+    return NULL;
+}
+
+void conf_register_reload(UNUSED_ARG(conf_reload_func crf)) {
+}
+
+void reg_exit_func(UNUSED_ARG(exit_func_t handler)) {
+}
+
+void timeq_add(UNUSED_ARG(time_t when), UNUSED_ARG(timeq_func func), UNUSED_ARG(void *data)) {
+}
+
+void timeq_del(UNUSED_ARG(time_t when), UNUSED_ARG(timeq_func func), UNUSED_ARG(void *data), UNUSED_ARG(int mask)) {
+}
+
+int send_message(UNUSED_ARG(struct userNode *dest), UNUSED_ARG(struct userNode *src), UNUSED_ARG(const char *message), ...) {
+    return 0;
+}
+
+struct module *module_register(UNUSED_ARG(const char *name), UNUSED_ARG(enum log_type clog), UNUSED_ARG(const char *helpfile_name), UNUSED_ARG(expand_func_t expand_help)) {
+    return NULL;
+}
+
+struct modcmd *modcmd_register(UNUSED_ARG(struct module *module), UNUSED_ARG(const char *name), UNUSED_ARG(modcmd_func_t func), UNUSED_ARG(unsigned int min_argc), UNUSED_ARG(unsigned int flags), ...) {
+    return NULL;
+}
+
+void table_send(UNUSED_ARG(struct userNode *from), UNUSED_ARG(const char *to), UNUSED_ARG(unsigned int size), UNUSED_ARG(irc_send_func irc_send), UNUSED_ARG(struct helpfile_table table)) {
+}
+
+/* back to our regularly scheduled code: */
+
+int check_record(const char *key, void *data, UNUSED_ARG(void *extra))
+{
+    struct record_data *rd = data;
+    switch (rd->type) {
+    case RECDB_INVALID:
+       fprintf(stdout, "Invalid database record type for key %s\n", key);
+       return 1;
+    case RECDB_QSTRING:
+    case RECDB_STRING_LIST:
+       return 0;
+    case RECDB_OBJECT:
+       return dict_foreach(rd->d.object, check_record, NULL) ? 1 : 0;
+    }
+    return 0;
+}
+
+int main(int argc, char *argv[])
+{
+    dict_t db;
+    char *infile;
+
+    if (argc < 2 || argc > 3) {
+        fprintf(stderr, "%s usage: %s <dbfile> [outputfile]\n\n", argv[0], argv[0]);
+        fprintf(stderr, "If [outputfile] is specified, dbfile is rewritten into outputfile after being\nparsed.\n\n");
+        fprintf(stderr, "<dbfile> and/or [outputfile] may be given as '-' to use stdin and stdout,\nrespectively.\n");
+        return 1;
+    }
+
+    tools_init();
+    if (!strcmp(argv[1], "-")) {
+        infile = "/dev/stdin";
+    } else {
+        infile = argv[1];
+    }
+    if (!(db = parse_database(infile))) return 2;
+    fprintf(stdout, "Database read okay.\n");
+    fflush(stdout);
+    if (dict_foreach(db, check_record, 0)) return 3;
+    if (!bad) {
+        fprintf(stdout, "Database checked okay.\n");
+        fflush(stdout);
+    }
+
+    if (argc == 3) {
+        FILE *f;
+
+        if (!strcmp(argv[2], "-")) {
+            f = stdout;
+        } else {
+            if (!(f = fopen(argv[2], "w+"))) {
+                fprintf(stderr, "fopen: %s\n", strerror(errno));
+                return 4;
+            }
+        }
+
+        write_database(f, db);
+        fclose(f);
+        fprintf(stdout, "Database written okay.\n");
+        fflush(stdout);
+    }
+
+    return 0;
+}
diff --git a/src/common.h b/src/common.h
new file mode 100644 (file)
index 0000000..ad5af73
--- /dev/null
@@ -0,0 +1,192 @@
+/* common.h - Common functions/includes
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef COMMON_H
+#define COMMON_H
+
+#include "compat.h"
+#include "proto.h"
+
+#if !defined(HAVE_LOCALTIME_R) && !defined(__CYGWIN__)
+extern struct tm *localtime_r(const time_t *clock, struct tm *res);
+#elif defined(__CYGWIN__)
+# define localtime_r(clock, res) memcpy(res, localtime(clock), sizeof(struct tm));
+#endif
+
+#ifndef true
+#define true 1
+#endif
+
+#ifndef false
+#define false 0
+#endif
+
+#ifndef INADDR_NONE
+#define INADDR_NONE 0xffffffffL
+#endif
+#ifndef INADDR_LOOPBACK
+#define INADDR_LOOPBACK 0x7f000001L
+#endif
+
+#define ArrayLength(x)         (sizeof(x)/sizeof(x[0]))
+#define safestrncpy(dest, src, len) do { char *d = (dest); const char *s = (src); size_t l = strlen(s)+1;  if ((len) < l) l = (len); memmove(d, s, l); d[l-1] = 0; } while (0)
+
+#ifdef __GNUC__
+#define PRINTF_LIKE(M,N) __attribute__((format (printf, M, N)))
+#else
+#define PRINTF_LIKE(M,N)
+#endif
+
+#if __GNUC__ >= 2
+#define UNUSED_ARG(ARG) ARG __attribute__((unused))
+#elif defined(S_SPLINT_S)
+#define UNUSED_ARG(ARG) /*@unused@*/ ARG
+#define const /*@observer@*/ /*@temp@*/
+#else
+#define UNUSED_ARG(ARG) ARG
+#endif
+
+#if defined(WITH_MALLOC_DMALLOC)
+#define DMALLOC_FUNC_CHECK 1
+#include <string.h>
+#include <dmalloc.h>
+#elif defined(WITH_MALLOC_MPATROL)
+#include <string.h>
+#include <mpatrol.h>
+#elif defined(WITH_MALLOC_BOEHM_GC)
+#if !defined(NDEBUG)
+#define GC_DEBUG 1
+#endif
+#include <stdlib.h>
+#include <string.h>
+#include <gc/gc.h>
+#define malloc(n) GC_MALLOC(n)
+#define calloc(m,n) GC_MALLOC((m)*(n))
+#define realloc(p,n) GC_REALLOC((p),(n))
+#define free(p) GC_FREE(p)
+#undef  HAVE_STRDUP
+#undef strdup
+#endif
+
+extern time_t now;
+extern int quit_services;
+extern struct log_type *MAIN_LOG;
+
+int create_socket_client(struct uplinkNode *target);
+void close_socket(void);
+
+typedef void (*exit_func_t)(void);
+void reg_exit_func(exit_func_t handler);
+void call_exit_funcs(void);
+
+const char *inttobase64(char *buf, unsigned int v, unsigned int count);
+unsigned long base64toint(const char *s, int count);
+int split_line(char *line, int irc_colon, int argv_size, char *argv[]);
+
+/* match_ircglobs(oldglob, newglob) returns non-zero if oldglob is a superset of newglob */
+#define match_ircglobs !mmatch
+int mmatch(const char *glob, const char *newglob);
+int match_ircglob(const char *text, const char *glob);
+int user_matches_glob(struct userNode *user, const char *glob, int include_nick);
+
+int is_ircmask(const char *text);
+int is_gline(const char *text);
+
+char *sanitize_ircmask(char *text);
+
+unsigned long ParseInterval(const char *interval);
+unsigned long ParseVolume(const char *volume);
+int parse_ipmask(const char *str, struct in_addr *addr, unsigned long *mask);
+#define MATCH_IPMASK(test, addr, mask) (((ntohl(test.s_addr) & mask) ^ (ntohl(addr.s_addr) & mask)) == 0)
+
+#define MD5_CRYPT_LENGTH 42
+/* buffer[] must be at least MD5_CRYPT_LENGTH bytes long */
+const char *cryptpass(const char *pass, char buffer[]);
+int checkpass(const char *pass, const char *crypt);
+
+int split_ircmask(char *text, char **nick, char **ident, char **host);
+char *unsplit_string(char *set[], unsigned int max, char *dest);
+
+#define DECLARE_LIST(STRUCTNAME,ITEMTYPE) struct STRUCTNAME {\
+  unsigned int used, size;\
+  ITEMTYPE *list;\
+};\
+void STRUCTNAME##_init(struct STRUCTNAME *list);\
+void STRUCTNAME##_append(struct STRUCTNAME *list, ITEMTYPE new_item);\
+int STRUCTNAME##_remove(struct STRUCTNAME *list, ITEMTYPE new_item);\
+void STRUCTNAME##_clean(struct STRUCTNAME *list)
+
+#define DEFINE_LIST(STRUCTNAME,ITEMTYPE) \
+void STRUCTNAME##_init(struct STRUCTNAME *list) {\
+  list->used = 0;\
+  list->size = 8;\
+  list->list = malloc(list->size*sizeof(list->list[0]));\
+}\
+void STRUCTNAME##_append(struct STRUCTNAME *list, ITEMTYPE new_item) {\
+  if (list->used == list->size) {\
+    list->size = list->size ? (list->size << 1) : 4;\
+    list->list = realloc(list->list, list->size*sizeof(list->list[0]));\
+  }\
+  list->list[list->used++] = new_item;\
+}\
+int STRUCTNAME##_remove(struct STRUCTNAME *list, ITEMTYPE new_item) {\
+    unsigned int n, found;\
+    for (found=n=0; n<list->used; n++) {\
+       if (list->list[n] == new_item) {\
+           memmove(list->list+n, list->list+n+1, (list->used-n-1)*sizeof(list->list[n]));\
+           found = 1;\
+           list->used--;\
+       }\
+    }\
+    return found;\
+}\
+void STRUCTNAME##_clean(struct STRUCTNAME *list) {\
+  list->used = list->size = 0;\
+  free(list->list);\
+}
+
+/* The longest string that's likely to be produced is "10 minutes, and 10
+   seconds." (27 characters) */
+#define INTERVALLEN    32
+
+char *intervalString2(char *output, time_t interval, int brief);
+#define intervalString(OUTPUT, INTERVAL) intervalString2((OUTPUT), (INTERVAL), 0)
+int getipbyname(const char *name, unsigned long *ip);
+int set_policer_param(const char *param, void *data, void *extra);
+const char *strtab(unsigned int ii);
+
+void tools_init(void);
+void tools_cleanup(void);
+
+int irccasecmp(const char *stra, const char *strb);
+int ircncasecmp(const char *stra, const char *strb, unsigned int len);
+const char *irccasestr(const char *haystack, const char *needle);
+
+DECLARE_LIST(string_buffer, char);
+void string_buffer_append_string(struct string_buffer *buf, const char *tail);
+void string_buffer_append_substring(struct string_buffer *buf, const char *tail, unsigned int len);
+void string_buffer_append_vprintf(struct string_buffer *buf, const char *fmt, va_list args);
+void string_buffer_append_printf(struct string_buffer *buf, const char *fmt, ...);
+void string_buffer_replace(struct string_buffer *buf, unsigned int from, unsigned int len, const char *repl);
+
+#define enabled_string(string)  (!irccasecmp((string), "on") || !strcmp((string), "1") || !irccasecmp((string), "enabled"))
+#define disabled_string(string) (!irccasecmp((string), "off") || !strcmp((string), "0") || !irccasecmp((string), "disabled"))
+#define true_string(string)     (!irccasecmp((string), "true") || !strcmp((string), "1") || !irccasecmp((string), "yes"))
+#define false_string(string)    (!irccasecmp((string), "false") || !strcmp((string), "0") || !irccasecmp((string), "no"))
+
+#endif /* ifdef COMMON_H */
diff --git a/src/compat.c b/src/compat.c
new file mode 100644 (file)
index 0000000..820771e
--- /dev/null
@@ -0,0 +1,355 @@
+#undef gettimeofday
+#undef memcpy
+#undef memset
+#undef strerror
+
+#include "common.h"
+
+#ifdef HAVE_SYS_TIMEB_H
+# include <sys/timeb.h>
+#endif
+#ifdef HAVE_MEMORY_H
+# include <memory.h>
+#endif
+
+#if !defined(HAVE_GETTIMEOFDAY) && defined(HAVE_FTIME)
+extern gettimeofday(struct timeval * tv, struct timezone * tz);
+{
+    if (!tv)
+    {
+        errno = EFAULT;
+        return -1;
+    }
+
+    struct timeb tb;
+
+    ftime(&tb); /* FIXME: some versions are void return others int */
+
+    tv->tv_sec  = tb.time;
+    tv->tv_usec = ((long)tb.millitm)*1000;
+    if (tz)
+    {
+        tz->tz_minuteswest = 0;
+        tz->tz_dsttime     = 0;
+    }
+
+    return 0;
+}
+#endif
+
+#ifndef HAVE_MEMCPY
+extern void * memcpy(void * dest, void const * src, unsigned long n)
+{
+#ifdef HAVE_BCOPY
+    bcopy(src,dest,n);
+    return dest;
+#else
+/* very slow, your fault for not having memcpy()*/
+    unsigned char * td=dest;
+    unsigned char * ts=src;
+    unsigned long   i;
+
+    if (!td || !ts)
+        return NULL;
+
+    for (i=0; i<n; i++)
+        td[i] = ts[i];
+    return dest;
+#endif
+}
+#endif
+
+#ifndef HAVE_MEMSET
+/* very slow, deal with it */
+extern void * memset(void * dest, int c, unsigned long n)
+{
+    unsigned char * temp=dest;
+    unsigned long   i;
+
+    if (!temp)
+        return NULL;
+
+    for (i=0; i<n; i++)
+        temp[i] = (unsigned char)c;
+    return dest;
+}
+#endif
+
+#ifndef HAVE_STRDUP
+extern char * strdup(char const * str)
+{
+    char * out;
+
+    if (!str)
+        return NULL;
+    if (!(out = malloc(strlen(str)+1)))
+        return NULL;
+    strcpy(out,str);
+    return out;
+}
+#endif
+
+#ifndef HAVE_STRERROR
+extern char const * strerror(int errornum)
+{
+    if (errornum==0)
+       return "No error";
+#ifdef EPERM
+    if (errornum==EPERM)
+       return "Operation not permitted";
+#endif
+#ifdef ENOENT
+    if (errornum==ENOENT)
+       return "No such file or directory";
+#endif
+#ifdef ESRCH
+    if (errornum==ESRCH)
+       return "No such process";
+#endif
+#ifdef EINTR
+    if (errornum==EINTR)
+       return "Interrupted system call";
+#endif
+#ifdef EIO
+    if (errornum==EIO)
+       return "I/O error";
+#endif
+#ifdef ENXIO
+    if (errornum==EIO)
+       return "No such device or address";
+#endif
+#ifdef EBADF
+    if (errornum==EBADF)
+       return "Bad file number";
+#endif
+#ifdef EAGAIN
+    if (errornum==EAGAIN)
+       return "Try again";
+#endif
+#ifdef ENOMEM
+    if (errornum==ENOMEM)
+       return "Out of memory";
+#endif
+#ifdef EACCES
+    if (errornum==EACCES)
+       return "Permission denied";
+#endif
+#ifdef EFAULT
+    if (errornum==EFAULT)
+       return "Bad address";
+#endif
+#ifdef EBUSY
+    if (errornum==EBUSY)
+       return "Device or resource busy";
+#endif
+#ifdef EEXIST
+    if (errornum==EEXIST)
+       return "File exists";
+#endif
+#ifdef EXDEV
+    if (errornum==EXDEV)
+       return "Cross-device link";
+#endif
+#ifdef EDEADLK
+    if (errornum==EXDEV)
+       return "Resource deadlock would occur";
+#endif
+#ifdef EDEADLOCK
+    if (errornum==EDEADLOCK)
+       return "Resource deadlock would occur";
+#endif
+#ifdef ENODEV
+    if (errornum==ENODEV)
+       return "No such device";
+#endif
+#ifdef ENOTDIR
+    if (errornum==ENOTDIR)
+       return "Not a directory";
+#endif
+#ifdef EISDIR
+    if (errornum==EISDIR)
+       return "Is a directory";
+#endif
+#ifdef EINVAL
+    if (errornum==EINVAL)
+       return "Invalid argument";
+#endif
+#ifdef ENFILE
+    if (errornum==ENFILE)
+       return "Too many open files in system";
+#endif
+#ifdef EMFILE
+    if (errornum==EMFILE)
+       return "Too many open files";
+#endif
+#ifdef ENOTTY
+    if (errornum==ENOTTY)
+       return "Not a typewriter";
+#endif
+#ifdef ETXTBSY
+    if (errornum==ETXTBSY)
+       return "Text file busy";
+#endif
+#ifdef EFBIG
+    if (errornum==EFBIG)
+       return "File too large";
+#endif
+#ifdef ENOSPC
+    if (errornum==ENOSPC)
+       return "No space left on device";
+#endif
+#ifdef ESPIPE
+    if (errornum==ESPIPE)
+       return "Illegal seek";
+#endif
+#ifdef EROFS
+    if (errornum==EROFS)
+       return "Read-only file system";
+#endif
+#ifdef EMLINK
+    if (errornum==EMLINK)
+       return "Too many links";
+#endif
+#ifdef EPIPE
+    if (errornum==EPIPE)
+       return "Broken pipe";
+#endif
+#ifdef EDOM
+    if (errornum==EDOM)
+       return "Math argument out of domain of func";
+#endif
+#ifdef ERANGE
+    if (errornum==ERANGE)
+       return "Math result not representable";
+#endif
+#ifdef ENAMETOOLONG
+    if (errornum==ENAMETOOLONG)
+       return "File name too long";
+#endif
+#ifdef ENOLCK
+    if (errornum==ENOLCK)
+       return "No record locks avaliable";
+#endif
+#ifdef ENOSYS
+    if (errornum==ENOSYS)
+       return "Function not implemented";
+#endif
+#ifdef ENOTEMPTY
+    if (errornum==ENOTEMPTY)
+       return "Directory not empty";
+#endif
+#ifdef ELOOP
+    if (errornum==ELOOP)
+       return "Too many symbolic links encountered";
+#endif
+#ifdef EHOSTDOWN
+    if (errornum==EHOSTDOWN)
+       return "Host is down";
+#endif
+#ifdef EHOSTUNREACH
+    if (errornum==EHOSTUNREACH)
+       return "No route to host";
+#endif
+#ifdef EALREADY
+    if (errornum==EALREADY)
+       return "Operation already in progress";
+#endif
+#ifdef EINPROGRESS
+    if (errornum==EINPROGRESS)
+       return "Operation now in progress";
+#endif
+#ifdef ESTALE
+    if (errornum==ESTALE)
+       return "Stale NFS filehandle";
+#endif
+#ifdef EDQUOT
+    if (errornum==EDQUOT)
+       return "Quota exceeded";
+#endif
+#ifdef EWOULDBLOCK
+    if (errornum==EWOULDBLOCK)
+       return "Operation would block";
+#endif
+#ifdef ECOMM
+    if (errornum==ECOMM)
+       return "Communication error on send";
+#endif
+#ifdef EPROTO
+    if (errornum==EPROTO)
+       return "Protocol error";
+#endif
+#ifdef EPROTONOSUPPORT
+    if (errornum==EPROTONOSUPPORT)
+       return "Protocol not supported";
+#endif
+#ifdef ESOCKTNOSUPPORT
+    if (errornum==ESOCKTNOSUPPORT)
+       return "Socket type not supported";
+#endif
+#ifdef ESOCKTNOSUPPORT
+    if (errornum==EOPNOTSUPP)
+       return "Operation not supported";
+#endif
+#ifdef EPFNOSUPPORT
+    if (errornum==EPFNOSUPPORT)
+       return "Protocol family not supported";
+#endif
+#ifdef EAFNOSUPPORT
+    if (errornum==EAFNOSUPPORT)
+       return "Address family not supported by protocol family";
+#endif
+#ifdef EADDRINUSE
+    if (errornum==EADDRINUSE)
+       return "Address already in use";
+#endif
+#ifdef EADDRNOTAVAIL
+    if (errornum==EADDRNOTAVAIL)
+       return "Cannot assign requested address";
+#endif
+#ifdef ENETDOWN
+    if (errornum==ENETDOWN)
+       return "Network is down";
+#endif
+#ifdef ENETUNREACH
+    if (errornum==ENETUNREACH)
+       return "Network is unreachable";
+#endif
+#ifdef ENETRESET
+    if (errornum==ENETRESET)
+       return "Network dropped connection on reset";
+#endif
+#ifdef ECONNABORTED
+    if (errornum==ECONNABORTED)
+       return "Software caused connection abort";
+#endif
+#ifdef ECONNRESET
+    if (errornum==ECONNRESET)
+       return " Connection reset by peer";
+#endif
+#ifdef ENOBUFS
+    if (errornum==ENOBUFS)
+       return "No buffer space available";
+#endif
+#ifdef EISCONN
+    if (errornum==EISCONN)
+       return "Socket is already connected";
+#endif
+#ifdef ENOTCONN
+    if (errornum==ENOTCONN)
+       return "Socket is not connected";
+#endif
+#ifdef ESHUTDOWN
+    if (errornum==ESHUTDOWN)
+       return " Cannot send after socket shutdown";
+#endif
+#ifdef ETIMEDOUT
+    if (errornum==ETIMEDOUT)
+       return "Connection timed out";
+#endif
+#ifdef ECONNREFUSED
+    if (errornum==ECONNREFUSED)
+       return "Connection refused";
+#endif
+    return "Unknown error";
+}
+#endif
diff --git a/src/compat.h b/src/compat.h
new file mode 100644 (file)
index 0000000..8571259
--- /dev/null
@@ -0,0 +1,90 @@
+#ifndef COMPAT_H
+#define COMPAT_H
+
+#include "config.h"
+
+/* AIX's compiler requires this to be the first thing in the compiled
+ * files.  Yay for braindead compilers. */
+#if defined(__GNUC__) && !defined(HAVE_ALLOCA_H)
+# define alloca __builtin_alloca
+#else
+# if defined(HAVE_ALLOCA_H)
+#  include <alloca.h>
+# else
+#  ifdef _AIX
+#   pragma alloca
+#  else
+#   ifndef alloca
+char *alloca();
+#   endif
+#  endif
+# endif
+#endif
+
+/* These are ANSI C89 headers, so everybody should have them.  If
+ * they're missing, we probably don't care much about the platform.
+ * If we do, we can add an autoconf test and try to patch around;
+ * this is the right file for that, after all :)
+ */
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef TIME_WITH_SYS_TIME
+# include <sys/time.h>
+# include <time.h>
+#else
+# ifdef HAVE_SYS_TIME_H
+#  include <sys/time.h>
+# else
+#  include <time.h>
+# endif
+#endif
+
+#ifdef HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
+#ifdef HAVE_VA_COPY
+#define VA_COPY(DEST, SRC) va_copy(DEST, SRC)
+#elif HAVE___VA_COPY
+#define VA_COPY(DEST, SRC) __va_copy(DEST, SRC)
+#else
+#define VA_COPY(DEST, SRC) memcpy(&(DEST), &(SRC), sizeof(DEST))
+#endif
+
+#ifndef HAVE_GETTIMEOFDAY
+extern int gettimeofday(struct timeval * tv, struct timezone * tz);
+#endif
+
+#ifndef HAVE_MEMCPY
+/* this should use size_t, but some systems don't define it */
+extern void * memcpy(void * dest, void const * src, unsigned long n);
+#endif
+
+#ifndef HAVE_MEMSET
+/* this should use size_t, but some systems don't define it */
+extern void * memset(void * dest, int c, unsigned long n);
+#endif
+
+#ifndef HAVE_STRDUP
+extern char * strdup(char const * str);
+#endif
+
+#ifndef HAVE_STRERROR
+extern char const * strerror(int errornum);
+#endif
+
+#endif /* COMPAT_H */
diff --git a/src/conf.c b/src/conf.c
new file mode 100644 (file)
index 0000000..62aa146
--- /dev/null
@@ -0,0 +1,96 @@
+/* conf.c - Config file reader
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "conf.h"
+#include "log.h"
+
+static dict_t conf_db;
+static conf_reload_func *reload_funcs;
+static int num_rfs, size_rfs;
+
+void
+conf_register_reload(conf_reload_func crf)
+{
+    if (num_rfs >= size_rfs) {
+        if (reload_funcs) {
+            size_rfs <<= 1;
+            reload_funcs = realloc(reload_funcs, size_rfs*sizeof(conf_reload_func));
+        } else {
+            size_rfs = 8;
+            reload_funcs = calloc(size_rfs, sizeof(conf_reload_func));
+        }
+    }
+    reload_funcs[num_rfs++] = crf;
+    if (conf_db) {
+        crf();
+    }
+}
+
+void
+conf_call_reload_funcs(void)
+{
+    int i;
+    for (i=0; i<num_rfs; i++) reload_funcs[i]();
+}
+
+int
+conf_read(const char *conf_file_name)
+{
+    dict_t old_conf = conf_db;
+    if (!(conf_db = parse_database(conf_file_name))) {
+        goto fail;
+    }
+    if (reload_funcs) {
+        conf_call_reload_funcs();
+    }
+    if (old_conf && old_conf != conf_db) {
+        free_database(old_conf);
+    }
+    return 1;
+
+fail:
+    log_module(MAIN_LOG, LOG_ERROR, "Reverting to previous configuration.");
+    free_database(conf_db);
+    conf_db = old_conf;
+    return 0;
+}
+
+void
+conf_close(void)
+{
+    free_database(conf_db);
+    free(reload_funcs);
+}
+
+struct record_data *
+conf_get_node(const char *full_path)
+{
+    return database_get_path(conf_db, full_path);
+}
+
+void *
+conf_get_data(const char *full_path, enum recdb_type type)
+{
+    return database_get_data(conf_db, full_path, type);
+}
+
+const char*
+conf_enum_root(dict_iterator_f it, void *extra)
+{
+    return dict_foreach(conf_db, it, extra);
+}
diff --git a/src/conf.h b/src/conf.h
new file mode 100644 (file)
index 0000000..28b07ab
--- /dev/null
@@ -0,0 +1,35 @@
+/* conf.h - Config file reader
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef CONF_H
+#define CONF_H
+
+#include "recdb.h"
+
+int conf_read(const char *conf_file_name);
+void conf_close(void);
+
+typedef void (*conf_reload_func)(void);
+void conf_register_reload(conf_reload_func crf);
+void conf_call_reload_funcs(void);
+
+void *conf_get_data(const char *full_path, enum recdb_type type);
+struct record_data *conf_get_node(const char *full_path);
+const char *conf_enum_root(dict_iterator_f it, void *extra);
+
+#endif
diff --git a/src/dict-splay.c b/src/dict-splay.c
new file mode 100644 (file)
index 0000000..3a6cafd
--- /dev/null
@@ -0,0 +1,322 @@
+/* dict-splay.c - Abstract dictionary type
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "common.h"
+#include "dict.h"
+
+/*
+ *    Create new dictionary.
+ */
+dict_t
+dict_new(void)
+{
+    dict_t dict = calloc(1, sizeof(*dict));
+    return dict;
+}
+
+/*
+ *    Return number of entries in the dictionary.
+ */
+unsigned int
+dict_size(dict_t dict)
+{
+    return dict->count;
+}
+
+/*
+ *    Set the function to be called when freeing a key structure.
+ *    If the function is NULL, just forget about the pointer.
+ */
+void
+dict_set_free_keys(dict_t dict, free_f free_keys)
+{
+    dict->free_keys = free_keys;
+}
+
+/*
+ *    Set the function to free data.
+ * If the function is NULL, just forget about the pointer.
+ */
+void
+dict_set_free_data(dict_t dict, free_f free_data)
+{
+    dict->free_data = free_data;
+}
+
+const char *
+dict_foreach(dict_t dict, dict_iterator_f it_f, void *extra)
+{
+    dict_iterator_t it;
+
+    for (it=dict_first(dict); it; it=iter_next(it)) {
+        if (it_f(iter_key(it), iter_data(it), extra)) return iter_key(it);
+    }
+    return NULL;
+}
+
+/*
+ *   This function finds a node and pulls it to the top of the tree.
+ *   This helps balance the tree and auto-cache things you search for.
+ */
+static struct dict_node*
+dict_splay(struct dict_node *node, const char *key)
+{
+    struct dict_node N, *l, *r, *y;
+    if (!node) return NULL;
+    N.l = N.r = NULL;
+    l = r = &N;
+
+    while (1) {
+       int res = irccasecmp(key, node->key);
+       if (!res) break;
+       if (res < 0) {
+           if (!node->l) break;
+           res = irccasecmp(key, node->l->key);
+           if (res < 0) {
+               y = node->l;
+               node->l = y->r;
+               y->r = node;
+               node = y;
+               if (!node->l) break;
+           }
+           r->l = node;
+           r = node;
+           node = node->l;
+       } else { /* res > 0 */
+           if (!node->r) break;
+           res = irccasecmp(key, node->r->key);
+           if (res > 0) {
+               y = node->r;
+               node->r = y->l;
+               y->l = node;
+               node = y;
+               if (!node->r) break;
+           }
+           l->r = node;
+           l = node;
+           node = node->r;
+       }
+    }
+    l->r = node->l;
+    r->l = node->r;
+    node->l = N.r;
+    node->r = N.l;
+    return node;
+}
+
+/*
+ *    Free node.  Free data/key using free_f functions.
+ */
+static void
+dict_dispose_node(struct dict_node *node, free_f free_keys, free_f free_data)
+{
+    if (free_keys && node->key)
+        free_keys((void*)node->key);
+    if (free_data && node->data)
+        free_data(node->data);
+    free(node);
+}
+
+/*
+ *    Insert an entry into the dictionary.
+ *    Key ordering (and uniqueness) is determined by case-insensitive
+ *    string comparison.
+ */
+void
+dict_insert(dict_t dict, const char *key, void *data)
+{
+    struct dict_node *new_node;
+    if (!key)
+        return;
+    new_node = malloc(sizeof(struct dict_node));
+    new_node->key = key;
+    new_node->data = data;
+    if (dict->root) {
+       int res;
+       dict->root = dict_splay(dict->root, key);
+       res = irccasecmp(key, dict->root->key);
+       if (res < 0) {
+            /* insert just "before" current root */
+           new_node->l = dict->root->l;
+           new_node->r = dict->root;
+           dict->root->l = NULL;
+            if (dict->root->prev) {
+                dict->root->prev->next = new_node;
+            } else {
+                dict->first = new_node;
+            }
+            new_node->prev = dict->root->prev;
+            new_node->next = dict->root;
+            dict->root->prev = new_node;
+           dict->root = new_node;
+       } else if (res > 0) {
+            /* insert just "after" current root */
+           new_node->r = dict->root->r;
+           new_node->l = dict->root;
+           dict->root->r = NULL;
+            if (dict->root->next) {
+                dict->root->next->prev = new_node;
+            } else {
+                dict->last = new_node;
+            }
+            new_node->next = dict->root->next;
+            new_node->prev = dict->root;
+            dict->root->next = new_node;
+           dict->root = new_node;
+       } else {
+           /* maybe we don't want to overwrite it .. oh well */
+           if (dict->free_data) dict->free_data(dict->root->data);
+            if (dict->free_keys) dict->free_keys((void*)dict->root->key);
+            free(new_node);
+            dict->root->key = key;
+           dict->root->data = data;
+           /* decrement the count since we dropped the node */
+           dict->count--;
+       }
+    } else {
+       new_node->l = new_node->r = NULL;
+        new_node->next = new_node->prev = NULL;
+       dict->root = dict->first = dict->last = new_node;
+    }
+    dict->count++;
+}
+
+/*
+ *    Remove an entry from the dictionary.
+ *    Return non-zero if it was found, or zero if the key was not in the
+ *    dictionary.
+ */
+int
+dict_remove2(dict_t dict, const char *key, int no_dispose)
+{
+    struct dict_node *new_root;
+
+    if (!dict->root)
+        return 0;
+    dict->root = dict_splay(dict->root, key);
+    if (irccasecmp(key, dict->root->key))
+        return 0;
+
+    if (!dict->root->l) {
+        new_root = dict->root->r;
+    } else {
+        new_root = dict_splay(dict->root->l, key);
+        new_root->r = dict->root->r;
+    }
+    if (dict->root->prev) dict->root->prev->next = dict->root->next;
+    if (dict->first == dict->root) dict->first = dict->first->next;
+    if (dict->root->next) dict->root->next->prev = dict->root->prev;
+    if (dict->last == dict->root) dict->last = dict->last->prev;
+    if (no_dispose) {
+        free(dict->root);
+    } else {
+        dict_dispose_node(dict->root, dict->free_keys, dict->free_data);
+    }
+    dict->root = new_root;
+    dict->count--;
+    return 1;
+}
+
+/*
+ *    Find an entry in the dictionary.
+ *    If "found" is non-NULL, set it to non-zero if the key was found.
+ *    Return the data associated with the key (or NULL if the key was
+ *    not found).
+ */
+void*
+dict_find(dict_t dict, const char *key, int *found)
+{
+    int was_found;
+    if (!dict || !dict->root || !key) {
+       if (found)
+            *found = 0;
+       return NULL;
+    }
+    dict->root = dict_splay(dict->root, key);
+    was_found = !irccasecmp(key, dict->root->key);
+    if (found)
+        *found = was_found;
+    return was_found ? dict->root->data : NULL;
+}
+
+/*
+ *    Delete an entire dictionary.
+ */
+void
+dict_delete(dict_t dict)
+{
+    dict_iterator_t it, next;
+    if (!dict)
+        return;
+    for (it=dict_first(dict); it; it=next) {
+        next = iter_next(it);
+        dict_dispose_node(it, dict->free_keys, dict->free_data);
+    }
+    free(dict);
+}
+
+struct dict_sanity_struct {
+    unsigned int node_count;
+    struct dict_node *bad_node;
+    char error[128];
+};
+
+static int
+dict_sanity_check_node(struct dict_node *node, struct dict_sanity_struct *dss)
+{
+    if (!node->key) {
+        snprintf(dss->error, sizeof(dss->error), "Node %p had null key", node);
+        return 1;
+    }
+    if (node->l) {
+        if (dict_sanity_check_node(node->l, dss)) return 1;
+        if (irccasecmp(node->l->key, node->key) >= 0) {
+            snprintf(dss->error, sizeof(dss->error), "Node %p's left child's key '%s' >= its key '%s'", node, node->l->key, node->key);
+            return 1;
+        }
+    }
+    if (node->r) {
+        if (dict_sanity_check_node(node->r, dss)) return 1;
+        if (irccasecmp(node->key, node->r->key) >= 0) {
+            snprintf(dss->error, sizeof(dss->error), "Node %p's right child's key '%s' <= its key '%s'", node, node->r->key, node->key);
+            return 1;
+        }
+    }
+    dss->node_count++;
+    return 0;
+}
+
+/*
+ *    Perform sanity checks on the dict's internal structure.
+ */
+char *
+dict_sanity_check(dict_t dict)
+{
+    struct dict_sanity_struct dss;
+    dss.node_count = 0;
+    dss.bad_node = 0;
+    dss.error[0] = 0;
+    if (dict->root && dict_sanity_check_node(dict->root, &dss)) {
+        return strdup(dss.error);
+    } else if (dss.node_count != dict->count) {
+        snprintf(dss.error, sizeof(dss.error), "Counted %d nodes but expected %d.", dss.node_count, dict->count);
+        return strdup(dss.error);
+    } else {
+        return 0;
+    }
+}
diff --git a/src/dict.h b/src/dict.h
new file mode 100644 (file)
index 0000000..3adae6f
--- /dev/null
@@ -0,0 +1,64 @@
+/* dict.h - Abstract dictionary type
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#if !defined(DICT_H)
+#define DICT_H
+
+/* helper types */
+typedef void (*free_f)(void*);
+typedef int (*dict_iterator_f)(const char *key, void *data, void *extra);
+
+/* exposed ONLY for the iteration macros; if you use these, DIE */
+struct dict_node {
+    const char *key;
+    void *data;
+    struct dict_node *l, *r, *prev, *next;
+};
+
+struct dict {
+    free_f free_keys, free_data;
+    struct dict_node *root, *first, *last;
+    unsigned int count;
+};
+
+/* "published" API */
+typedef struct dict *dict_t;
+typedef struct dict_node *dict_iterator_t;
+
+#define dict_first(DICT) ((DICT) ? (DICT)->first : NULL)
+#define iter_key(ITER) ((ITER)->key)
+#define iter_data(ITER) ((ITER)->data)
+#define iter_next(ITER) ((ITER)->next)
+
+dict_t dict_new(void);
+/* dict_foreach returns key of node causing halt (non-zero return from
+ * iterator function) */
+const char* dict_foreach(dict_t dict, dict_iterator_f it, void *extra);
+void dict_insert(dict_t dict, const char *key, void *data);
+void dict_set_free_keys(dict_t dict, free_f free_keys);
+void dict_set_free_data(dict_t dict, free_f free_data);
+unsigned int dict_size(dict_t dict);
+/* if present!=NULL, then *present=1 iff node was found (if node is
+ * not found, return value is NULL, which may be a valid datum) */
+void* dict_find(dict_t dict, const char *key, int *present);
+int dict_remove2(dict_t dict, const char *key, int no_dispose);
+#define dict_remove(DICT, KEY) dict_remove2(DICT, KEY, 0)
+char *dict_sanity_check(dict_t dict);
+void dict_delete(dict_t dict);
+
+#endif /* !defined(DICT_H) */
diff --git a/src/expnhelp.c b/src/expnhelp.c
new file mode 100644 (file)
index 0000000..8e01716
--- /dev/null
@@ -0,0 +1,84 @@
+#include "log.h"
+#include "recdb.h"
+
+/* because recdb likes to log stuff.. */
+struct log_type *MAIN_LOG;
+void log_module(UNUSED_ARG(struct log_type *lt), UNUSED_ARG(enum log_severity ls), const char *format, ...)
+{
+    va_list va;
+    va_start(va, format);
+    vfprintf(stderr, format, va);
+    va_end(va);
+    fflush(stderr);
+}
+
+struct string_list *new_argv;
+const char *hidden_host_suffix;
+
+struct cfg_scan {
+    struct cfg_scan *parent;
+    char *path;
+};
+
+int
+scan_db(const char *key, void *data, void *extra)
+{
+    struct record_data *rd = data;
+    struct cfg_scan child, *self = extra;
+
+    child.parent = extra;
+
+    switch (rd->type) {
+    case RECDB_QSTRING:
+        if ((irccasestr(key, "enable")
+             || (irccasestr(key, "disable"))
+             || (irccasestr(key, "require")))
+            && enabled_string(rd->d.qstring)) {
+            char *new_arg;
+            new_arg = malloc(strlen(self->path)+strlen(key)+4);
+            sprintf(new_arg, "-D%s/%s", self->path, key);
+            string_list_append(new_argv, new_arg);
+        }
+        break;
+    case RECDB_OBJECT:
+        child.path = malloc(strlen(self->path) + strlen(key) + 2);
+        sprintf(child.path, "%s/%s", self->path, key);
+        dict_foreach(rd->d.object, scan_db, &child);
+        free(child.path);
+        break;
+    default: /* ignore */ break;
+    }
+    return 0;
+}
+
+int
+main(int argc, char *argv[])
+{
+    const char *cfg_file;
+    struct cfg_scan scanner;
+    dict_t cfg_db;
+
+    tools_init();
+    new_argv = alloc_string_list(4);
+    string_list_append(new_argv, strdup("m4"));
+
+    if (argc > 1) {
+        cfg_file = argv[1];
+    } else {
+        cfg_file = "srvx.conf";
+    }
+
+    if (!(cfg_db = parse_database(cfg_file))) {
+        fprintf(stderr, "Unable to parse config file %s; you will get a 'default' expansion.\n", cfg_file);
+    } else {
+        scanner.parent = NULL;
+        scanner.path = "";
+        dict_foreach(cfg_db, scan_db, &scanner);
+    }
+
+    string_list_append(new_argv, NULL);
+    execvp("m4", new_argv->list);
+    fprintf(stderr, "Error in exec: %s (%d)\n", strerror(errno), errno);
+    fprintf(stderr, "Maybe you do not have the 'm4' program installed?\n");
+    return 1;
+}
diff --git a/src/getopt.c b/src/getopt.c
new file mode 100644 (file)
index 0000000..b75deb8
--- /dev/null
@@ -0,0 +1,1003 @@
+/* Getopt for GNU.
+   NOTE: getopt is now part of the C library, so if you don't know what
+   "Keep this file name-space clean" means, talk to roland@gnu.ai.mit.edu
+   before changing it!
+
+   Copyright (C) 1987, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97
+   Free Software Foundation, Inc.
+
+   This file is part of the GNU C Library.  Its master source is NOT part of
+   the C library, however.  The master source lives in /gd/gnu/lib.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Library General Public License as
+   published by the Free Software Foundation; either version 2 of the
+   License, or (at your option) any later version.
+
+   The GNU C Library 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
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public
+   License along with the GNU C Library; see the file COPYING.LIB.  If not,
+   write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+   Boston, MA 02111-1307, USA.  */
+\f
+/* This tells Alpha OSF/1 not to define a getopt prototype in <stdio.h>.
+   Ditto for AIX 3.2 and <stdlib.h>.  */
+
+#include "common.h"
+
+#ifndef IGNORE_GETOPT
+
+#ifndef _NO_PROTO
+#define _NO_PROTO
+#endif
+
+#if !defined (__STDC__) || !__STDC__
+/* This is a separate conditional since some stdc systems
+   reject `defined (const)'.  */
+#ifndef const
+#define const
+#endif
+#endif
+
+/* Comment out all this code if we are using the GNU C Library, and are not
+   actually compiling the library itself.  This code is part of the GNU C
+   Library, but also included in many other GNU distributions.  Compiling
+   and linking in this code is a waste when using the GNU C library
+   (especially if it is a shared library).  Rather than having every GNU
+   program understand `configure --with-gnu-libc' and omit the object files,
+   it is simpler to just do this in the source for each such file.  */
+
+#define GETOPT_INTERFACE_VERSION 2
+#if !defined (_LIBC) && defined (__GLIBC__) && __GLIBC__ >= 2
+#include <gnu-versions.h>
+#if _GNU_GETOPT_INTERFACE_VERSION == GETOPT_INTERFACE_VERSION
+#define ELIDE_CODE
+#endif
+#endif
+
+#ifndef ELIDE_CODE
+
+
+/* This needs to come after some library #include
+   to get __GNU_LIBRARY__ defined.  */
+#ifdef __GNU_LIBRARY__
+/* Don't include stdlib.h for non-GNU C libraries because some of them
+   contain conflicting prototypes for getopt.  */
+#include <stdlib.h>
+#include <unistd.h>
+#endif /* GNU C library.  */
+
+#ifdef VMS
+#include <unixlib.h>
+#endif
+
+#if defined (WIN32) && !defined (__CYGWIN32__)
+/* It's not Unix, really.  See?  Capital letters.  */
+#include <windows.h>
+#define getpid() GetCurrentProcessId()
+#endif
+
+#ifndef _
+/* This is for other GNU distributions with internationalized messages.
+   When compiling libc, the _ macro is predefined.  */
+#ifdef HAVE_LIBINTL_H
+# include <libintl.h>
+# define _(msgid)      gettext (msgid)
+#else
+# define _(msgid)      (msgid)
+#endif
+#endif
+
+/* This version of `getopt' appears to the caller like standard Unix `getopt'
+   but it behaves differently for the user, since it allows the user
+   to intersperse the options with the other arguments.
+
+   As `getopt' works, it permutes the elements of ARGV so that,
+   when it is done, all the options precede everything else.  Thus
+   all application programs are extended to handle flexible argument order.
+
+   Setting the environment variable POSIXLY_CORRECT disables permutation.
+   Then the behavior is completely standard.
+
+   GNU application programs can use a third alternative mode in which
+   they can distinguish the relative order of options and other arguments.  */
+
+#include "getopt.h"
+
+/* For communication from `getopt' to the caller.
+   When `getopt' finds an option that takes an argument,
+   the argument value is returned here.
+   Also, when `ordering' is RETURN_IN_ORDER,
+   each non-option ARGV-element is returned here.  */
+
+char *optarg = NULL;
+
+/* Index in ARGV of the next element to be scanned.
+   This is used for communication to and from the caller
+   and for communication between successive calls to `getopt'.
+
+   On entry to `getopt', zero means this is the first call; initialize.
+
+   When `getopt' returns -1, this is the index of the first of the
+   non-option elements that the caller should itself scan.
+
+   Otherwise, `optind' communicates from one call to the next
+   how much of ARGV has been scanned so far.  */
+
+/* 1003.2 says this must be 1 before any call.  */
+int optind = 1;
+
+/* Formerly, initialization of getopt depended on optind==0, which
+   causes problems with re-calling getopt as programs generally don't
+   know that. */
+
+int __getopt_initialized = 0;
+
+/* The next char to be scanned in the option-element
+   in which the last option character we returned was found.
+   This allows us to pick up the scan where we left off.
+
+   If this is zero, or a null string, it means resume the scan
+   by advancing to the next ARGV-element.  */
+
+static char *nextchar;
+
+/* Callers store zero here to inhibit the error message
+   for unrecognized options.  */
+
+int opterr = 1;
+
+/* Set to an option character which was unrecognized.
+   This must be initialized on some systems to avoid linking in the
+   system's own getopt implementation.  */
+
+int optopt = '?';
+
+/* Describe how to deal with options that follow non-option ARGV-elements.
+
+   If the caller did not specify anything,
+   the default is REQUIRE_ORDER if the environment variable
+   POSIXLY_CORRECT is defined, PERMUTE otherwise.
+
+   REQUIRE_ORDER means don't recognize them as options;
+   stop option processing when the first non-option is seen.
+   This is what Unix does.
+   This mode of operation is selected by either setting the environment
+   variable POSIXLY_CORRECT, or using `+' as the first character
+   of the list of option characters.
+
+   PERMUTE is the default.  We permute the contents of ARGV as we scan,
+   so that eventually all the non-options are at the end.  This allows options
+   to be given in any order, even with programs that were not written to
+   expect this.
+
+   RETURN_IN_ORDER is an option available to programs that were written
+   to expect options and other ARGV-elements in any order and that care about
+   the ordering of the two.  We describe each non-option ARGV-element
+   as if it were the argument of an option with character code 1.
+   Using `-' as the first character of the list of option characters
+   selects this mode of operation.
+
+   The special argument `--' forces an end of option-scanning regardless
+   of the value of `ordering'.  In the case of RETURN_IN_ORDER, only
+   `--' can cause `getopt' to return -1 with `optind' != ARGC.  */
+
+static enum
+{
+  REQUIRE_ORDER, PERMUTE, RETURN_IN_ORDER
+} ordering;
+
+/* Value of POSIXLY_CORRECT environment variable.  */
+static char *posixly_correct;
+\f
+
+#ifdef __GNU_LIBRARY__
+/* We want to avoid inclusion of string.h with non-GNU libraries
+   because there are many ways it can cause trouble.
+   On some systems, it contains special magic macros that don't work
+   in GCC.  */
+#define        my_index        strchr
+#else
+
+/* Avoid depending on library functions or files
+   whose names are inconsistent.  */
+
+char *getenv ();
+
+static char *
+my_index (str, chr)
+     const char *str;
+     int chr;
+{
+  while (*str)
+    {
+      if (*str == chr)
+       return (char *) str;
+      str++;
+    }
+  return 0;
+}
+
+/* If using GCC, we can safely declare strlen this way.
+   If not using GCC, it is ok not to declare it.  */
+#ifdef __GNUC__
+/* Note that Motorola Delta 68k R3V7 comes with GCC but not stddef.h.
+   That was relevant to code that was here before.  */
+#if !defined (__STDC__) || !__STDC__
+/* gcc with -traditional declares the built-in strlen to return int,
+   and has done so at least since version 2.4.5. -- rms.  */
+extern int strlen (const char *);
+#endif /* not __STDC__ */
+#endif /* __GNUC__ */
+
+#endif /* not __GNU_LIBRARY__ */
+\f
+/* Handle permutation of arguments.  */
+
+/* Describe the part of ARGV that contains non-options that have
+   been skipped.  `first_nonopt' is the index in ARGV of the first of them;
+   `last_nonopt' is the index after the last of them.  */
+
+static int first_nonopt;
+static int last_nonopt;
+
+#ifdef _LIBC
+/* Bash 2.0 gives us an environment variable containing flags
+   indicating ARGV elements that should not be considered arguments.  */
+
+static const char *nonoption_flags;
+static int nonoption_flags_len;
+
+static int original_argc;
+static char *const *original_argv;
+
+/* Make sure the environment variable bash 2.0 puts in the environment
+   is valid for the getopt call we must make sure that the ARGV passed
+   to getopt is that one passed to the process.  */
+static void store_args (int argc, char *const *argv) __attribute__ ((unused));
+static void
+store_args (int argc, char *const *argv)
+{
+  /* XXX This is no good solution.  We should rather copy the args so
+     that we can compare them later.  But we must not use malloc(3).  */
+  original_argc = argc;
+  original_argv = argv;
+}
+text_set_element (__libc_subinit, store_args);
+#endif
+
+/* Exchange two adjacent subsequences of ARGV.
+   One subsequence is elements [first_nonopt,last_nonopt)
+   which contains all the non-options that have been skipped so far.
+   The other is elements [last_nonopt,optind), which contains all
+   the options processed since those non-options were skipped.
+
+   `first_nonopt' and `last_nonopt' are relocated so that they describe
+   the new indices of the non-options in ARGV after they are moved.  */
+
+#if defined (__STDC__) && __STDC__
+static void exchange (char **);
+#endif
+
+static void
+exchange (argv)
+     char **argv;
+{
+  int bottom = first_nonopt;
+  int middle = last_nonopt;
+  int top = optind;
+  char *tem;
+
+  /* Exchange the shorter segment with the far end of the longer segment.
+     That puts the shorter segment into the right place.
+     It leaves the longer segment in the right place overall,
+     but it consists of two parts that need to be swapped next.  */
+
+  while (top > middle && middle > bottom)
+    {
+      if (top - middle > middle - bottom)
+       {
+         /* Bottom segment is the short one.  */
+         int len = middle - bottom;
+         register int i;
+
+         /* Swap it with the top part of the top segment.  */
+         for (i = 0; i < len; i++)
+           {
+             tem = argv[bottom + i];
+             argv[bottom + i] = argv[top - (middle - bottom) + i];
+             argv[top - (middle - bottom) + i] = tem;
+           }
+         /* Exclude the moved bottom segment from further swapping.  */
+         top -= len;
+       }
+      else
+       {
+         /* Top segment is the short one.  */
+         int len = top - middle;
+         register int i;
+
+         /* Swap it with the bottom part of the bottom segment.  */
+         for (i = 0; i < len; i++)
+           {
+             tem = argv[bottom + i];
+             argv[bottom + i] = argv[middle + i];
+             argv[middle + i] = tem;
+           }
+         /* Exclude the moved top segment from further swapping.  */
+         bottom += len;
+       }
+    }
+
+  /* Update records for the slots the non-options now occupy.  */
+
+  first_nonopt += (optind - last_nonopt);
+  last_nonopt = optind;
+}
+
+/* Initialize the internal data when the first call is made.  */
+
+#if defined (__STDC__) && __STDC__
+static const char *_getopt_initialize (int, char *const *, const char *);
+#endif
+static const char *
+_getopt_initialize (argc, argv, optstring)
+     int argc;
+     char *const *argv;
+     const char *optstring;
+{
+#ifndef _LIBC
+  (void)argc; (void)argv;
+#endif
+  /* Start processing options with ARGV-element 1 (since ARGV-element 0
+     is the program name); the sequence of previously skipped
+     non-option ARGV-elements is empty.  */
+
+  first_nonopt = last_nonopt = optind = 1;
+
+  nextchar = NULL;
+
+  posixly_correct = getenv ("POSIXLY_CORRECT");
+
+  /* Determine how to handle the ordering of options and nonoptions.  */
+
+  if (optstring[0] == '-')
+    {
+      ordering = RETURN_IN_ORDER;
+      ++optstring;
+    }
+  else if (optstring[0] == '+')
+    {
+      ordering = REQUIRE_ORDER;
+      ++optstring;
+    }
+  else if (posixly_correct != NULL)
+    ordering = REQUIRE_ORDER;
+  else
+    ordering = PERMUTE;
+
+#ifdef _LIBC
+  if (posixly_correct == NULL
+      && argc == original_argc && argv == original_argv)
+    {
+      /* Bash 2.0 puts a special variable in the environment for each
+        command it runs, specifying which ARGV elements are the results of
+        file name wildcard expansion and therefore should not be
+        considered as options.  */
+      char var[100];
+      sprintf (var, "_%d_GNU_nonoption_argv_flags_", getpid ());
+      nonoption_flags = getenv (var);
+      if (nonoption_flags == NULL)
+       nonoption_flags_len = 0;
+      else
+       nonoption_flags_len = strlen (nonoption_flags);
+    }
+  else
+    nonoption_flags_len = 0;
+#endif
+
+  return optstring;
+}
+\f
+/* Scan elements of ARGV (whose length is ARGC) for option characters
+   given in OPTSTRING.
+
+   If an element of ARGV starts with '-', and is not exactly "-" or "--",
+   then it is an option element.  The characters of this element
+   (aside from the initial '-') are option characters.  If `getopt'
+   is called repeatedly, it returns successively each of the option characters
+   from each of the option elements.
+
+   If `getopt' finds another option character, it returns that character,
+   updating `optind' and `nextchar' so that the next call to `getopt' can
+   resume the scan with the following option character or ARGV-element.
+
+   If there are no more option characters, `getopt' returns -1.
+   Then `optind' is the index in ARGV of the first ARGV-element
+   that is not an option.  (The ARGV-elements have been permuted
+   so that those that are not options now come last.)
+
+   OPTSTRING is a string containing the legitimate option characters.
+   If an option character is seen that is not listed in OPTSTRING,
+   return '?' after printing an error message.  If you set `opterr' to
+   zero, the error message is suppressed but we still return '?'.
+
+   If a char in OPTSTRING is followed by a colon, that means it wants an arg,
+   so the following text in the same ARGV-element, or the text of the following
+   ARGV-element, is returned in `optarg'.  Two colons mean an option that
+   wants an optional arg; if there is text in the current ARGV-element,
+   it is returned in `optarg', otherwise `optarg' is set to zero.
+
+   If OPTSTRING starts with `-' or `+', it requests different methods of
+   handling the non-option ARGV-elements.
+   See the comments about RETURN_IN_ORDER and REQUIRE_ORDER, above.
+
+   Long-named options begin with `--' instead of `-'.
+   Their names may be abbreviated as long as the abbreviation is unique
+   or is an exact match for some defined option.  If they have an
+   argument, it follows the option name in the same ARGV-element, separated
+   from the option name by a `=', or else the in next ARGV-element.
+   When `getopt' finds a long-named option, it returns 0 if that option's
+   `flag' field is nonzero, the value of the option's `val' field
+   if the `flag' field is zero.
+
+   The elements of ARGV aren't really const, because we permute them.
+   But we pretend they're const in the prototype to be compatible
+   with other systems.
+
+   LONGOPTS is a vector of `struct option' terminated by an
+   element containing a name which is zero.
+
+   LONGIND returns the index in LONGOPT of the long-named option found.
+   It is only valid when a long-named option has been found by the most
+   recent call.
+
+   If LONG_ONLY is nonzero, '-' as well as '--' can introduce
+   long-named options.  */
+
+int
+_getopt_internal (argc, argv, optstring, longopts, longind, long_only)
+     int argc;
+     char *const *argv;
+     const char *optstring;
+     const struct option *longopts;
+     int *longind;
+     int long_only;
+{
+  optarg = NULL;
+
+  if (!__getopt_initialized || optind == 0)
+    {
+      optstring = _getopt_initialize (argc, argv, optstring);
+      optind = 1;              /* Don't scan ARGV[0], the program name.  */
+      __getopt_initialized = 1;
+    }
+
+  /* Test whether ARGV[optind] points to a non-option argument.
+     Either it does not have option syntax, or there is an environment flag
+     from the shell indicating it is not an option.  The later information
+     is only used when the used in the GNU libc.  */
+#ifdef _LIBC
+#define NONOPTION_P (argv[optind][0] != '-' || argv[optind][1] == '\0'       \
+                    || (optind < nonoption_flags_len                         \
+                        && nonoption_flags[optind] == '1'))
+#else
+#define NONOPTION_P (argv[optind][0] != '-' || argv[optind][1] == '\0')
+#endif
+
+  if (nextchar == NULL || *nextchar == '\0')
+    {
+      /* Advance to the next ARGV-element.  */
+
+      /* Give FIRST_NONOPT & LAST_NONOPT rational values if OPTIND has been
+        moved back by the user (who may also have changed the arguments).  */
+      if (last_nonopt > optind)
+       last_nonopt = optind;
+      if (first_nonopt > optind)
+       first_nonopt = optind;
+
+      if (ordering == PERMUTE)
+       {
+         /* If we have just processed some options following some non-options,
+            exchange them so that the options come first.  */
+
+         if (first_nonopt != last_nonopt && last_nonopt != optind)
+           exchange ((char **) argv);
+         else if (last_nonopt != optind)
+           first_nonopt = optind;
+
+         /* Skip any additional non-options
+            and extend the range of non-options previously skipped.  */
+
+         while (optind < argc && NONOPTION_P)
+           optind++;
+         last_nonopt = optind;
+       }
+
+      /* The special ARGV-element `--' means premature end of options.
+        Skip it like a null option,
+        then exchange with previous non-options as if it were an option,
+        then skip everything else like a non-option.  */
+
+      if (optind != argc && !strcmp (argv[optind], "--"))
+       {
+         optind++;
+
+         if (first_nonopt != last_nonopt && last_nonopt != optind)
+           exchange ((char **) argv);
+         else if (first_nonopt == last_nonopt)
+           first_nonopt = optind;
+         last_nonopt = argc;
+
+         optind = argc;
+       }
+
+      /* If we have done all the ARGV-elements, stop the scan
+        and back over any non-options that we skipped and permuted.  */
+
+      if (optind == argc)
+       {
+         /* Set the next-arg-index to point at the non-options
+            that we previously skipped, so the caller will digest them.  */
+         if (first_nonopt != last_nonopt)
+           optind = first_nonopt;
+         return -1;
+       }
+
+      /* If we have come to a non-option and did not permute it,
+        either stop the scan or describe it to the caller and pass it by.  */
+
+      if (NONOPTION_P)
+       {
+         if (ordering == REQUIRE_ORDER)
+           return -1;
+         optarg = argv[optind++];
+         return 1;
+       }
+
+      /* We have found another option-ARGV-element.
+        Skip the initial punctuation.  */
+
+      nextchar = (argv[optind] + 1
+                 + (longopts != NULL && argv[optind][1] == '-'));
+    }
+
+  /* Decode the current option-ARGV-element.  */
+
+  /* Check whether the ARGV-element is a long option.
+
+     If long_only and the ARGV-element has the form "-f", where f is
+     a valid short option, don't consider it an abbreviated form of
+     a long option that starts with f.  Otherwise there would be no
+     way to give the -f short option.
+
+     On the other hand, if there's a long option "fubar" and
+     the ARGV-element is "-fu", do consider that an abbreviation of
+     the long option, just like "--fu", and not "-f" with arg "u".
+
+     This distinction seems to be the most useful approach.  */
+
+  if (longopts != NULL
+      && (argv[optind][1] == '-'
+         || (long_only && (argv[optind][2] || !my_index (optstring, argv[optind][1])))))
+    {
+      char *nameend;
+      const struct option *p;
+      const struct option *pfound = NULL;
+      int exact = 0;
+      int ambig = 0;
+      int indfound = -1;
+      int option_index;
+
+      for (nameend = nextchar; *nameend && *nameend != '='; nameend++)
+       /* Do nothing.  */ ;
+
+      /* Test all long options for either exact match
+        or abbreviated matches.  */
+      for (p = longopts, option_index = 0; p->name; p++, option_index++)
+       if (!strncmp (p->name, nextchar, nameend - nextchar))
+         {
+           if ((unsigned int) (nameend - nextchar)
+               == (unsigned int) strlen (p->name))
+             {
+               /* Exact match found.  */
+               pfound = p;
+               indfound = option_index;
+               exact = 1;
+               break;
+             }
+           else if (pfound == NULL)
+             {
+               /* First nonexact match found.  */
+               pfound = p;
+               indfound = option_index;
+             }
+           else
+             /* Second or later nonexact match found.  */
+             ambig = 1;
+         }
+
+      if (ambig && !exact)
+       {
+         if (opterr)
+           fprintf (stderr, _("%s: option `%s' is ambiguous\n"),
+                    argv[0], argv[optind]);
+         nextchar += strlen (nextchar);
+         optind++;
+         optopt = 0;
+         return '?';
+       }
+
+      if (pfound != NULL)
+       {
+         option_index = indfound;
+         optind++;
+         if (*nameend)
+           {
+             /* Don't test has_arg with >, because some C compilers don't
+                allow it to be used on enums.  */
+             if (pfound->has_arg)
+               optarg = nameend + 1;
+             else
+               {
+                 if (opterr) {
+                  if (argv[optind - 1][1] == '-')
+                   /* --option */
+                   fprintf (stderr,
+                    _("%s: option `--%s' doesn't allow an argument\n"),
+                    argv[0], pfound->name);
+                  else
+                   /* +option or -option */
+                   fprintf (stderr,
+                    _("%s: option `%c%s' doesn't allow an argument\n"),
+                    argv[0], argv[optind - 1][0], pfound->name);
+                 }
+                 nextchar += strlen (nextchar);
+
+                 optopt = pfound->val;
+                 return '?';
+               }
+           }
+         else if (pfound->has_arg == 1)
+           {
+             if (optind < argc)
+               optarg = argv[optind++];
+             else
+               {
+                 if (opterr)
+                   fprintf (stderr,
+                          _("%s: option `%s' requires an argument\n"),
+                          argv[0], argv[optind - 1]);
+                 nextchar += strlen (nextchar);
+                 optopt = pfound->val;
+                 return optstring[0] == ':' ? ':' : '?';
+               }
+           }
+         nextchar += strlen (nextchar);
+         if (longind != NULL)
+           *longind = option_index;
+         if (pfound->flag)
+           {
+             *(pfound->flag) = pfound->val;
+             return 0;
+           }
+         return pfound->val;
+       }
+
+      /* Can't find it as a long option.  If this is not getopt_long_only,
+        or the option starts with '--' or is not a valid short
+        option, then it's an error.
+        Otherwise interpret it as a short option.  */
+      if (!long_only || argv[optind][1] == '-'
+         || my_index (optstring, *nextchar) == NULL)
+       {
+         if (opterr)
+           {
+             if (argv[optind][1] == '-')
+               /* --option */
+               fprintf (stderr, _("%s: unrecognized option `--%s'\n"),
+                        argv[0], nextchar);
+             else
+               /* +option or -option */
+               fprintf (stderr, _("%s: unrecognized option `%c%s'\n"),
+                        argv[0], argv[optind][0], nextchar);
+           }
+         nextchar = (char *) "";
+         optind++;
+         optopt = 0;
+         return '?';
+       }
+    }
+
+  /* Look at and handle the next short option-character.  */
+
+  {
+    char c = *nextchar++;
+    char *temp = my_index (optstring, c);
+
+    /* Increment `optind' when we start to process its last character.  */
+    if (*nextchar == '\0')
+      ++optind;
+
+    if (temp == NULL || c == ':')
+      {
+       if (opterr)
+         {
+           if (posixly_correct)
+             /* 1003.2 specifies the format of this message.  */
+             fprintf (stderr, _("%s: illegal option -- %c\n"),
+                      argv[0], c);
+           else
+             fprintf (stderr, _("%s: invalid option -- %c\n"),
+                      argv[0], c);
+         }
+       optopt = c;
+       return '?';
+      }
+    /* Convenience. Treat POSIX -W foo same as long option --foo */
+    if (temp[0] == 'W' && temp[1] == ';')
+      {
+       char *nameend;
+       const struct option *p;
+       const struct option *pfound = NULL;
+       int exact = 0;
+       int ambig = 0;
+       int indfound = 0;
+       int option_index;
+
+       /* This is an option that requires an argument.  */
+       if (*nextchar != '\0')
+         {
+           optarg = nextchar;
+           /* If we end this ARGV-element by taking the rest as an arg,
+              we must advance to the next element now.  */
+           optind++;
+         }
+       else if (optind == argc)
+         {
+           if (opterr)
+             {
+               /* 1003.2 specifies the format of this message.  */
+               fprintf (stderr, _("%s: option requires an argument -- %c\n"),
+                        argv[0], c);
+             }
+           optopt = c;
+           if (optstring[0] == ':')
+             c = ':';
+           else
+             c = '?';
+           return c;
+         }
+       else
+         /* We already incremented `optind' once;
+            increment it again when taking next ARGV-elt as argument.  */
+         optarg = argv[optind++];
+
+       /* optarg is now the argument, see if it's in the
+          table of longopts.  */
+
+       for (nextchar = nameend = optarg; *nameend && *nameend != '='; nameend++)
+         /* Do nothing.  */ ;
+
+       /* Test all long options for either exact match
+          or abbreviated matches.  */
+       for (p = longopts, option_index = 0; p->name; p++, option_index++)
+         if (!strncmp (p->name, nextchar, nameend - nextchar))
+           {
+             if ((size_t) (nameend - nextchar) == (size_t) strlen (p->name))
+               {
+                 /* Exact match found.  */
+                 pfound = p;
+                 indfound = option_index;
+                 exact = 1;
+                 break;
+               }
+             else if (pfound == NULL)
+               {
+                 /* First nonexact match found.  */
+                 pfound = p;
+                 indfound = option_index;
+               }
+             else
+               /* Second or later nonexact match found.  */
+               ambig = 1;
+           }
+       if (ambig && !exact)
+         {
+           if (opterr)
+             fprintf (stderr, _("%s: option `-W %s' is ambiguous\n"),
+                      argv[0], argv[optind]);
+           nextchar += strlen (nextchar);
+           optind++;
+           return '?';
+         }
+       if (pfound != NULL)
+         {
+           option_index = indfound;
+           if (*nameend)
+             {
+               /* Don't test has_arg with >, because some C compilers don't
+                  allow it to be used on enums.  */
+               if (pfound->has_arg)
+                 optarg = nameend + 1;
+               else
+                 {
+                   if (opterr)
+                     fprintf (stderr, _("\
+%s: option `-W %s' doesn't allow an argument\n"),
+                              argv[0], pfound->name);
+
+                   nextchar += strlen (nextchar);
+                   return '?';
+                 }
+             }
+           else if (pfound->has_arg == 1)
+             {
+               if (optind < argc)
+                 optarg = argv[optind++];
+               else
+                 {
+                   if (opterr)
+                     fprintf (stderr,
+                              _("%s: option `%s' requires an argument\n"),
+                              argv[0], argv[optind - 1]);
+                   nextchar += strlen (nextchar);
+                   return optstring[0] == ':' ? ':' : '?';
+                 }
+             }
+           nextchar += strlen (nextchar);
+           if (longind != NULL)
+             *longind = option_index;
+           if (pfound->flag)
+             {
+               *(pfound->flag) = pfound->val;
+               return 0;
+             }
+           return pfound->val;
+         }
+         nextchar = NULL;
+         return 'W';   /* Let the application handle it.   */
+      }
+    if (temp[1] == ':')
+      {
+       if (temp[2] == ':')
+         {
+           /* This is an option that accepts an argument optionally.  */
+           if (*nextchar != '\0')
+             {
+               optarg = nextchar;
+               optind++;
+             }
+           else
+             optarg = NULL;
+           nextchar = NULL;
+         }
+       else
+         {
+           /* This is an option that requires an argument.  */
+           if (*nextchar != '\0')
+             {
+               optarg = nextchar;
+               /* If we end this ARGV-element by taking the rest as an arg,
+                  we must advance to the next element now.  */
+               optind++;
+             }
+           else if (optind == argc)
+             {
+               if (opterr)
+                 {
+                   /* 1003.2 specifies the format of this message.  */
+                   fprintf (stderr,
+                          _("%s: option requires an argument -- %c\n"),
+                          argv[0], c);
+                 }
+               optopt = c;
+               if (optstring[0] == ':')
+                 c = ':';
+               else
+                 c = '?';
+             }
+           else
+             /* We already incremented `optind' once;
+                increment it again when taking next ARGV-elt as argument.  */
+             optarg = argv[optind++];
+           nextchar = NULL;
+         }
+      }
+    return c;
+  }
+}
+#ifndef HAVE_GETOPT
+#define HAVE_GETOPT
+int
+getopt (argc, argv, optstring)
+     int argc;
+     char *const *argv;
+     const char *optstring;
+{
+  return _getopt_internal (argc, argv, optstring,
+                          (const struct option *) 0,
+                          (int *) 0,
+                          0);
+}
+#endif /* HAVE_GETOPT */
+#endif /* Not ELIDE_CODE.  */
+\f
+#ifdef TEST
+
+/* Compile with -DTEST to make an executable for use in testing
+   the above definition of `getopt'.  */
+
+int
+main (argc, argv)
+     int argc;
+     char **argv;
+{
+  int c;
+  int digit_optind = 0;
+  int optind = 1;
+  char *optarg = NULL;
+
+  while (1)
+    {
+      int this_option_optind = optind ? optind : 1;
+
+      c = getopt (argc, argv, "abc:d:0123456789");
+      if (c == -1)
+       break;
+
+      switch (c)
+       {
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+         if (digit_optind != 0 && digit_optind != this_option_optind)
+           printf ("digits occur in two different argv-elements.\n");
+         digit_optind = this_option_optind;
+         printf ("option %c\n", c);
+         break;
+
+       case 'a':
+         printf ("option a\n");
+         break;
+
+       case 'b':
+         printf ("option b\n");
+         break;
+
+       case 'c':
+         printf ("option c with value `%s'\n", optarg);
+         break;
+
+       case '?':
+         break;
+
+       default:
+         printf ("?? getopt returned character code 0%o ??\n", c);
+       }
+    }
+
+  if (optind < argc)
+    {
+      printf ("non-option ARGV-elements: ");
+      while (optind < argc)
+       printf ("%s ", argv[optind++]);
+      printf ("\n");
+    }
+
+  exit (0);
+}
+
+#endif /* TEST */
+#endif /* IGNORE_GETOPT */
diff --git a/src/getopt.h b/src/getopt.h
new file mode 100644 (file)
index 0000000..7dad11b
--- /dev/null
@@ -0,0 +1,133 @@
+/* Declarations for getopt.
+   Copyright (C) 1989,90,91,92,93,94,96,97 Free Software Foundation, Inc.
+
+   This file is part of the GNU C Library.  Its master source is NOT part of
+   the C library, however.  The master source lives in /gd/gnu/lib.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Library General Public License as
+   published by the Free Software Foundation; either version 2 of the
+   License, or (at your option) any later version.
+
+   The GNU C Library 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
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public
+   License along with the GNU C Library; see the file COPYING.LIB.  If not,
+   write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+   Boston, MA 02111-1307, USA.  */
+
+#ifndef _GETOPT_H
+#define _GETOPT_H 1
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* For communication from `getopt' to the caller.
+   When `getopt' finds an option that takes an argument,
+   the argument value is returned here.
+   Also, when `ordering' is RETURN_IN_ORDER,
+   each non-option ARGV-element is returned here.  */
+
+extern char *optarg;
+
+/* Index in ARGV of the next element to be scanned.
+   This is used for communication to and from the caller
+   and for communication between successive calls to `getopt'.
+
+   On entry to `getopt', zero means this is the first call; initialize.
+
+   When `getopt' returns -1, this is the index of the first of the
+   non-option elements that the caller should itself scan.
+
+   Otherwise, `optind' communicates from one call to the next
+   how much of ARGV has been scanned so far.  */
+
+extern int optind;
+
+/* Callers store zero here to inhibit the error message `getopt' prints
+   for unrecognized options.  */
+
+extern int opterr;
+
+/* Set to an option character which was unrecognized.  */
+
+extern int optopt;
+
+/* Describe the long-named options requested by the application.
+   The LONG_OPTIONS argument to getopt_long or getopt_long_only is a vector
+   of `struct option' terminated by an element containing a name which is
+   zero.
+
+   The field `has_arg' is:
+   no_argument         (or 0) if the option does not take an argument,
+   required_argument   (or 1) if the option requires an argument,
+   optional_argument   (or 2) if the option takes an optional argument.
+
+   If the field `flag' is not NULL, it points to a variable that is set
+   to the value given in the field `val' when the option is found, but
+   left unchanged if the option is not found.
+
+   To have a long-named option do something other than set an `int' to
+   a compiled-in constant, such as set a value from `optarg', set the
+   option's `flag' field to zero and its `val' field to a nonzero
+   value (the equivalent single-letter option character, if there is
+   one).  For long options that have a zero `flag' field, `getopt'
+   returns the contents of the `val' field.  */
+
+struct option
+{
+#if defined (__STDC__) && __STDC__
+  const char *name;
+#else
+  char *name;
+#endif
+  /* has_arg can't be an enum because some compilers complain about
+     type mismatches in all the code that assumes it is an int.  */
+  int has_arg;
+  int *flag;
+  int val;
+};
+
+/* Names for the values of the `has_arg' field of `struct option'.  */
+
+#define        no_argument             0
+#define required_argument      1
+#define optional_argument      2
+
+#if defined (__STDC__) && __STDC__
+#ifdef __GNU_LIBRARY__
+/* Many other libraries have conflicting prototypes for getopt, with
+   differences in the consts, in stdlib.h.  To avoid compilation
+   errors, only prototype getopt for the GNU C library.  */
+extern int getopt (int argc, char *const *argv, const char *shortopts);
+#else /* not __GNU_LIBRARY__ */
+extern int getopt ();
+#endif /* __GNU_LIBRARY__ */
+extern int getopt_long (int argc, char *const *argv, const char *shortopts,
+                       const struct option *longopts, int *longind);
+extern int getopt_long_only (int argc, char *const *argv,
+                            const char *shortopts,
+                            const struct option *longopts, int *longind);
+
+/* Internal only.  Users should not call this directly.  */
+extern int _getopt_internal (int argc, char *const *argv,
+                            const char *shortopts,
+                            const struct option *longopts, int *longind,
+                            int long_only);
+#else /* not __STDC__ */
+extern int getopt ();
+extern int getopt_long ();
+extern int getopt_long_only ();
+
+extern int _getopt_internal ();
+#endif /* __STDC__ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _GETOPT_H */
diff --git a/src/getopt1.c b/src/getopt1.c
new file mode 100644 (file)
index 0000000..be20377
--- /dev/null
@@ -0,0 +1,188 @@
+/* getopt_long and getopt_long_only entry points for GNU getopt.
+   Copyright (C) 1987,88,89,90,91,92,93,94,96,97 Free Software Foundation, Inc.
+
+   This file is part of the GNU C Library.  Its master source is NOT part of
+   the C library, however.  The master source lives in /gd/gnu/lib.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Library General Public License as
+   published by the Free Software Foundation; either version 2 of the
+   License, or (at your option) any later version.
+
+   The GNU C Library 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
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public
+   License along with the GNU C Library; see the file COPYING.LIB.  If not,
+   write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+   Boston, MA 02111-1307, USA.  */
+\f
+#include "common.h"
+#include "getopt.h"
+
+#ifndef IGNORE_GETOPT
+
+#if !defined (__STDC__) || !__STDC__
+/* This is a separate conditional since some stdc systems
+   reject `defined (const)'.  */
+#ifndef const
+#define const
+#endif
+#endif
+
+/* Comment out all this code if we are using the GNU C Library, and are not
+   actually compiling the library itself.  This code is part of the GNU C
+   Library, but also included in many other GNU distributions.  Compiling
+   and linking in this code is a waste when using the GNU C library
+   (especially if it is a shared library).  Rather than having every GNU
+   program understand `configure --with-gnu-libc' and omit the object files,
+   it is simpler to just do this in the source for each such file.  */
+
+#define GETOPT_INTERFACE_VERSION 2
+#if !defined (_LIBC) && defined (__GLIBC__) && __GLIBC__ >= 2
+#include <gnu-versions.h>
+#if _GNU_GETOPT_INTERFACE_VERSION == GETOPT_INTERFACE_VERSION
+#define ELIDE_CODE
+#endif
+#endif
+
+#ifndef ELIDE_CODE
+
+
+/* This needs to come after some library #include
+   to get __GNU_LIBRARY__ defined.  */
+#ifdef __GNU_LIBRARY__
+#include <stdlib.h>
+#endif
+
+#ifndef        NULL
+#define NULL 0
+#endif
+
+int getopt_long (argc, argv, options, long_options, opt_index)
+     int argc;
+     char *const *argv;
+     const char *options;
+     const struct option *long_options;
+     int *opt_index;
+{
+  return _getopt_internal (argc, argv, options, long_options, opt_index, 0);
+}
+
+/* Like getopt_long, but '-' as well as '--' can indicate a long option.
+   If an option that starts with '-' (not '--') doesn't match a long option,
+   but does match a short option, it is parsed as a short option
+   instead.  */
+
+#ifndef HAVE_GETOPT_LONG
+#define HAVE_GETOPT_LONG
+extern int
+getopt_long_only (argc, argv, options, long_options, opt_index)
+     int argc;
+     char *const *argv;
+     const char *options;
+     const struct option *long_options;
+     int *opt_index;
+{
+  return _getopt_internal (argc, argv, options, long_options, opt_index, 1);
+}
+
+#endif /* HAVE_GETOPT_LONG */
+#endif /* Not ELIDE_CODE.  */
+\f
+#ifdef TEST
+
+#include <stdio.h>
+
+int
+main (argc, argv)
+     int argc;
+     char **argv;
+{
+  int c;
+  int digit_optind = 0;
+
+  while (1)
+    {
+      int this_option_optind = optind ? optind : 1;
+      int option_index = 0;
+      static struct option long_options[] =
+      {
+       {"add", 1, 0, 0},
+       {"append", 0, 0, 0},
+       {"delete", 1, 0, 0},
+       {"verbose", 0, 0, 0},
+       {"create", 0, 0, 0},
+       {"file", 1, 0, 0},
+       {0, 0, 0, 0}
+      };
+
+      c = getopt_long (argc, argv, "abc:d:0123456789",
+                      long_options, &option_index);
+      if (c == -1)
+       break;
+
+      switch (c)
+       {
+       case 0:
+         printf ("option %s", long_options[option_index].name);
+         if (optarg)
+           printf (" with arg %s", optarg);
+         printf ("\n");
+         break;
+
+       case '0':
+       case '1':
+       case '2':
+       case '3':
+       case '4':
+       case '5':
+       case '6':
+       case '7':
+       case '8':
+       case '9':
+         if (digit_optind != 0 && digit_optind != this_option_optind)
+           printf ("digits occur in two different argv-elements.\n");
+         digit_optind = this_option_optind;
+         printf ("option %c\n", c);
+         break;
+
+       case 'a':
+         printf ("option a\n");
+         break;
+
+       case 'b':
+         printf ("option b\n");
+         break;
+
+       case 'c':
+         printf ("option c with value `%s'\n", optarg);
+         break;
+
+       case 'd':
+         printf ("option d with value `%s'\n", optarg);
+         break;
+
+       case '?':
+         break;
+
+       default:
+         printf ("?? getopt returned character code 0%o ??\n", c);
+       }
+    }
+
+  if (optind < argc)
+    {
+      printf ("non-option ARGV-elements: ");
+      while (optind < argc)
+       printf ("%s ", argv[optind++]);
+      printf ("\n");
+    }
+
+  exit (0);
+}
+
+#endif /* TEST */
+#endif /* IGNORE_GETOPT */
diff --git a/src/gline.c b/src/gline.c
new file mode 100644 (file)
index 0000000..8effe88
--- /dev/null
@@ -0,0 +1,439 @@
+/* gline.c - Gline database
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "heap.h"
+#include "helpfile.h"
+#include "log.h"
+#include "saxdb.h"
+#include "timeq.h"
+#include "gline.h"
+
+#ifdef HAVE_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif
+#ifdef HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#endif
+#ifdef HAVE_ARPA_INET_H
+#include <arpa/inet.h>
+#endif
+#ifdef HAVE_NETDB_H
+#include <netdb.h>
+#endif
+
+#define KEY_REASON "reason"
+#define KEY_EXPIRES "expires"
+#define KEY_ISSUER "issuer"
+#define KEY_ISSUED "issued"
+
+static heap_t gline_heap; /* key: expiry time, data: struct gline_entry* */
+static dict_t gline_dict; /* key: target, data: struct gline_entry* */
+
+static int
+gline_comparator(const void *a, const void *b)
+{
+    const struct gline *ga=a, *gb=b;
+    return ga->expires - gb->expires;
+}
+
+static void
+free_gline_from_dict(void *data)
+{
+    struct gline *ent = data;
+    free(ent->issuer);
+    free(ent->target);
+    free(ent->reason);
+    free(ent);
+}
+
+static void
+free_gline(struct gline *ent)
+{
+    dict_remove(gline_dict, ent->target);
+}
+
+static int
+gline_for_p(UNUSED_ARG(void *key), void *data, void *extra)
+{
+    struct gline *ge = data;
+    return !irccasecmp(ge->target, extra);
+}
+
+static int
+delete_gline_for_p(UNUSED_ARG(void *key), void *data, void *extra)
+{
+    struct gline *ge = data;
+
+    if (!irccasecmp(ge->target, extra)) {
+        free_gline(ge);
+        return 1;
+    } else {
+        return 0;
+    }
+}
+
+static void
+gline_expire(UNUSED_ARG(void *data))
+{
+    time_t stopped;
+    void *wraa;
+
+    stopped = 0;
+    while (heap_size(gline_heap)) {
+        heap_peek(gline_heap, 0, &wraa);
+        stopped = ((struct gline*)wraa)->expires;
+        if (stopped > now)
+            break;
+        heap_pop(gline_heap);
+        free_gline(wraa);
+    }
+    if (heap_size(gline_heap))
+        timeq_add(stopped, gline_expire, NULL);
+}
+
+int
+gline_remove(const char *target, int announce)
+{
+    int res = dict_find(gline_dict, target, NULL) ? 1 : 0;
+    if (heap_remove_pred(gline_heap, delete_gline_for_p, (char*)target)) {
+        void *argh;
+        struct gline *new_first;
+        heap_peek(gline_heap, 0, &argh);
+        if (argh) {
+            new_first = argh;
+            timeq_del(0, gline_expire, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+            timeq_add(new_first->expires, gline_expire, 0);
+        }
+    }
+#ifdef WITH_PROTOCOL_BAHAMUT
+    /* Bahamut is sort of lame: It permanently remembers any AKILLs
+     * with durations longer than a day, and will never auto-expire
+     * them.  So when the time comes, we'd better remind it.  */
+    announce = 1;
+#endif
+    if (announce)
+        irc_ungline(target);
+    return res;
+}
+
+struct gline *
+gline_add(const char *issuer, const char *target, unsigned long duration, const char *reason, time_t issued, int announce)
+{
+    struct gline *ent;
+    struct gline *prev_first;
+    void *argh;
+
+    heap_peek(gline_heap, 0, &argh);
+    prev_first = argh;
+    ent = dict_find(gline_dict, target, NULL);
+    if (ent) {
+        heap_remove_pred(gline_heap, gline_for_p, (char*)target);
+        if (ent->expires < (time_t)(now + duration))
+            ent->expires = now + duration;
+    } else {
+        ent = malloc(sizeof(*ent));
+        ent->issued = issued;
+        ent->issuer = strdup(issuer);
+        ent->target = strdup(target);
+        ent->expires = now + duration;
+        ent->reason = strdup(reason);
+        dict_insert(gline_dict, ent->target, ent);
+    }
+    heap_insert(gline_heap, ent, ent);
+    if (!prev_first || (ent->expires < prev_first->expires)) {
+       timeq_del(0, gline_expire, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+       timeq_add(ent->expires, gline_expire, 0);
+    }
+    if (announce)
+        irc_gline(NULL, ent);
+    return ent;
+}
+
+static char *
+gline_alternate_target(const char *target)
+{
+    const char *hostname;
+    unsigned long ip;
+    char *res;
+
+    /* If no host part, bail. */
+    if (!(hostname = strchr(target, '@')))
+        return NULL;
+    /* If host part contains wildcards, bail. */
+    if (hostname[strcspn(hostname, "*?/")])
+        return NULL;
+    /* If host part looks like an IP, parse it that way. */
+    if (!hostname[strspn(hostname+1, "0123456789.")+1]) {
+        struct in_addr in;
+        struct hostent *he;
+        if (inet_aton(hostname+1, &in)
+            && (he = gethostbyaddr(&in, sizeof(in), AF_INET))) {
+            res = malloc((hostname - target) + 2 + strlen(he->h_name));
+            sprintf(res, "%.*s@%s", hostname - target, target, he->h_name);
+            return res;
+        } else
+            return NULL;
+    } else if (getipbyname(hostname+1, &ip)) {
+        res = malloc((hostname - target) + 18);
+        sprintf(res, "%.*s@%lu.%lu.%lu.%lu", hostname - target, target, ip & 255, (ip >> 8) & 255, (ip >> 16) & 255, (ip >> 24) & 255);
+        return res;
+    } else
+        return NULL;
+}
+
+struct gline *
+gline_find(const char *target)
+{
+    struct gline *res;
+    dict_iterator_t it;
+    char *alt_target;
+
+    res = dict_find(gline_dict, target, NULL);
+    if (res)
+        return res;
+    /* Stock ircu requires BADCHANs to match exactly. */
+    if ((target[0] == '#') || (target[0] == '&'))
+        return NULL;
+    else if (target[strcspn(target, "*?")]) {
+        /* Wildcard: do an obnoxiously long search. */
+        for (it = dict_first(gline_dict); it; it = iter_next(it)) {
+            res = iter_data(it);
+            if (match_ircglob(target, res->target))
+                return res;
+        }
+    }
+    /* See if we can resolve the hostname part of the mask. */
+    if ((alt_target = gline_alternate_target(target))) {
+        res = gline_find(alt_target);
+        free(alt_target);
+        return res;
+    }
+    return NULL;
+}
+
+static int
+gline_refresh_helper(UNUSED_ARG(void *key), void *data, void *extra)
+{
+    struct gline *ge = data;
+    irc_gline(extra, ge);
+    return 0;
+}
+
+void
+gline_refresh_server(struct server *srv)
+{
+    heap_remove_pred(gline_heap, gline_refresh_helper, srv);
+}
+
+void
+gline_refresh_all(void)
+{
+    heap_remove_pred(gline_heap, gline_refresh_helper, 0);
+}
+
+unsigned int
+gline_count(void)
+{
+    return dict_size(gline_dict);
+}
+
+static int
+gline_add_record(const char *key, void *data, UNUSED_ARG(void *extra))
+{
+    struct record_data *rd = data;
+    const char *issuer, *reason, *dstr;
+    time_t issued, expiration;
+
+    if (!(reason = database_get_data(rd->d.object, KEY_REASON, RECDB_QSTRING))) {
+       log_module(MAIN_LOG, LOG_ERROR, "Missing reason for gline %s", key);
+       return 0;
+    }
+    if (!(dstr = database_get_data(rd->d.object, KEY_EXPIRES, RECDB_QSTRING))) {
+       log_module(MAIN_LOG, LOG_ERROR, "Missing expiration for gline %s", key);
+       return 0;
+    }
+    expiration = strtoul(dstr, NULL, 0);
+    if ((dstr = database_get_data(rd->d.object, KEY_ISSUED, RECDB_QSTRING))) {
+        issued = strtoul(dstr, NULL, 0);
+    } else {
+        issued = now;
+    }
+    if (!(issuer = database_get_data(rd->d.object, KEY_ISSUER, RECDB_QSTRING))) {
+        issuer = "<unknown>";
+    }
+    if (expiration > now)
+        gline_add(issuer, key, expiration - now, reason, issued, 0);
+    return 0;
+}
+
+static int
+gline_saxdb_read(struct dict *db)
+{
+    return dict_foreach(db, gline_add_record, 0) != NULL;
+}
+
+static int
+gline_write_entry(UNUSED_ARG(void *key), void *data, void *extra)
+{
+    struct gline *ent = data;
+    struct saxdb_context *ctx = extra;
+
+    saxdb_start_record(ctx, ent->target, 0);
+    saxdb_write_int(ctx, KEY_EXPIRES, ent->expires);
+    saxdb_write_int(ctx, KEY_ISSUED, ent->issued);
+    saxdb_write_string(ctx, KEY_REASON, ent->reason);
+    saxdb_write_string(ctx, KEY_ISSUER, ent->issuer);
+    saxdb_end_record(ctx);
+    return 0;
+}
+
+static int
+gline_saxdb_write(struct saxdb_context *ctx)
+{
+    heap_remove_pred(gline_heap, gline_write_entry, ctx);
+    return 0;
+}
+
+static void
+gline_db_cleanup(void)
+{
+    heap_delete(gline_heap);
+    dict_delete(gline_dict);
+}
+
+void
+gline_init(void)
+{
+    gline_heap = heap_new(gline_comparator);
+    gline_dict = dict_new();
+    dict_set_free_data(gline_dict, free_gline_from_dict);
+    saxdb_register("gline", gline_saxdb_read, gline_saxdb_write);
+    reg_exit_func(gline_db_cleanup);
+}
+
+struct gline_discrim *
+gline_discrim_create(struct userNode *user, struct userNode *src, unsigned int argc, char *argv[])
+{
+    unsigned int i;
+    struct gline_discrim *discrim;
+
+    discrim = calloc(1, sizeof(*discrim));
+    discrim->max_issued = now;
+    discrim->limit = 50;
+
+    for (i=0; i<argc; i++) {
+        if (i + 2 > argc) {
+            send_message(user, src, "MSG_MISSING_PARAMS", argv[i]);
+            goto fail;
+        } else if (!irccasecmp(argv[i], "mask") || !irccasecmp(argv[i], "host")) {
+            if (!irccasecmp(argv[++i], "exact"))
+                discrim->target_mask_type = EXACT;
+            else if (!irccasecmp(argv[i], "subset"))
+                discrim->target_mask_type = SUBSET;
+            else if (!irccasecmp(argv[i], "superset"))
+                discrim->target_mask_type = SUPERSET;
+            else
+                discrim->target_mask_type = SUBSET, i--;
+            if (++i == argc) {
+                send_message(user, src, "MSG_MISSING_PARAMS", argv[i-1]);
+                goto fail;
+            }
+            if (!is_gline(argv[i]) && !IsChannelName(argv[i])) {
+                send_message(user, src, "MSG_INVALID_GLINE", argv[i]);
+                goto fail;
+            }
+            discrim->target_mask = argv[i];
+            discrim->alt_target_mask = gline_alternate_target(discrim->target_mask);
+        } else if (!irccasecmp(argv[i], "limit"))
+            discrim->limit = strtoul(argv[++i], NULL, 0);
+        else if (!irccasecmp(argv[i], "reason"))
+            discrim->reason_mask = argv[++i];
+        else if (!irccasecmp(argv[i], "issuer"))
+            discrim->issuer_mask = argv[++i];
+        else if (!irccasecmp(argv[i], "after"))
+            discrim->min_expire = now + ParseInterval(argv[++i]);
+        else if (!irccasecmp(argv[i], "before"))
+            discrim->max_issued = now - ParseInterval(argv[++i]);
+        else {
+            send_message(user, src, "MSG_INVALID_CRITERIA", argv[i]);
+            goto fail;
+        }
+    }
+    return discrim;
+  fail:
+    free(discrim->alt_target_mask);
+    free(discrim);
+    return NULL;
+}
+
+struct gline_search {
+    struct gline_discrim *discrim;
+    gline_search_func func;
+    void *data;
+    unsigned int hits;
+};
+
+static int
+gline_discrim_match(struct gline *gline, struct gline_discrim *discrim)
+{
+    if ((discrim->issuer_mask && !match_ircglob(gline->issuer, discrim->issuer_mask))
+        || (discrim->reason_mask && !match_ircglob(gline->reason, discrim->reason_mask))
+        || (discrim->target_mask
+            && (((discrim->target_mask_type == SUBSET)
+                 && !match_ircglobs(discrim->target_mask, gline->target)
+                 && (!discrim->alt_target_mask
+                     || !match_ircglobs(discrim->alt_target_mask, gline->target)))
+                || ((discrim->target_mask_type == EXACT)
+                    && irccasecmp(discrim->target_mask, gline->target)
+                    && (!discrim->alt_target_mask
+                        || !irccasecmp(discrim->alt_target_mask, gline->target)))
+                || ((discrim->target_mask_type == SUPERSET)
+                    && !match_ircglobs(gline->target, discrim->target_mask)
+                    && (!discrim->alt_target_mask
+                        || !match_ircglobs(discrim->alt_target_mask, gline->target)))))
+        || (discrim->max_issued < gline->issued)
+        || (discrim->min_expire > gline->expires)) {
+        return 0;
+    }
+    return 1;
+}
+
+static int
+gline_search_helper(UNUSED_ARG(void *key), void *data, void *extra)
+{
+    struct gline *gline = data;
+    struct gline_search *search = extra;
+
+    if (gline_discrim_match(gline, search->discrim)
+        && (search->hits++ < search->discrim->limit)) {
+        search->func(gline, search->data);
+    }
+    return 0;
+}
+
+unsigned int
+gline_discrim_search(struct gline_discrim *discrim, gline_search_func gsf, void *data)
+{
+    struct gline_search search;
+    search.discrim = discrim;
+    search.func = gsf;
+    search.data = data;
+    search.hits = 0;
+    heap_remove_pred(gline_heap, gline_search_helper, &search);
+    return search.hits;
+}
diff --git a/src/gline.h b/src/gline.h
new file mode 100644 (file)
index 0000000..d8de859
--- /dev/null
@@ -0,0 +1,55 @@
+/* gline.h - Gline database
+ * Copyright 2001-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef GLINE_H
+#define GLINE_H
+
+#include "hash.h"
+
+struct gline {
+    time_t issued;
+    time_t expires;
+    char *issuer;
+    char *target;
+    char *reason;
+};
+
+struct gline_discrim {
+    unsigned int limit;
+    enum { SUBSET, EXACT, SUPERSET } target_mask_type;
+    char *issuer_mask;
+    char *target_mask;
+    char *alt_target_mask;
+    char *reason_mask;
+    time_t max_issued;
+    time_t min_expire;
+};
+
+void gline_init(void);
+struct gline *gline_add(const char *issuer, const char *target, unsigned long duration, const char *reason, time_t issued, int announce);
+struct gline *gline_find(const char *target);
+int gline_remove(const char *target, int announce);
+void gline_refresh_server(struct server *srv);
+void gline_refresh_all(void);
+unsigned int gline_count(void);
+
+typedef void (*gline_search_func)(struct gline *gline, void *extra);
+struct gline_discrim *gline_discrim_create(struct userNode *user, struct userNode *src, unsigned int argc, char *argv[]);
+unsigned int gline_discrim_search(struct gline_discrim *discrim, gline_search_func gsf, void *data);
+
+#endif /* !defined(GLINE_H) */
diff --git a/src/global.c b/src/global.c
new file mode 100644 (file)
index 0000000..575dac2
--- /dev/null
@@ -0,0 +1,681 @@
+/* global.c - Global notice service
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "conf.h"
+#include "global.h"
+#include "modcmd.h"
+#include "nickserv.h"
+#include "saxdb.h"
+#include "timeq.h"
+
+#define GLOBAL_CONF_NAME       "services/global"
+
+#define GLOBAL_DB              "global.db"
+#define GLOBAL_TEMP_DB         "global.db.new"
+
+/* Global options */
+#define KEY_DB_BACKUP_FREQ     "db_backup_freq"
+#define KEY_ANNOUNCEMENTS_DEFAULT "announcements_default"
+#define KEY_NICK               "nick"
+
+/* Message data */
+#define KEY_FLAGS              "flags"
+#define KEY_POSTED             "posted"
+#define KEY_DURATION           "duration"
+#define KEY_FROM               "from"
+#define KEY_MESSAGE            "message"
+
+/* Clarification: Notices are immediate, they are sent to matching users
+   _once_, then forgotten. Messages are stored in Global's database and
+   continually sent to users as they match the target specification until
+   they are deleted. */
+static const struct message_entry msgtab[] = {
+    { "GMSG_INVALID_TARGET", "$b%s$b is an invalid message target." },
+    { "GMSG_MESSAGE_REQUIRED", "You $bmust$b provide a message to send." },
+    { "GMSG_MESSAGE_SENT", "Message to $b%s$b sent." },
+    { "GMSG_MESSAGE_ADDED", "Message to $b%s$b with ID %ld added." },
+    { "GMSG_MESSAGE_DELETED", "Message $b%s$b deleted." },
+    { "GMSG_ID_INVALID", "$b%s$b is an invalid message ID." },
+    { "GMSG_MESSAGE_COUNT", "$b%d$b messages found." },
+    { "GMSG_NO_MESSAGES", "There are no messages for you." },
+    { "GMSG_NOTICE_SOURCE", "[$b%s$b] Notice from %s:" },
+    { "GMSG_MESSAGE_SOURCE", "[$b%s$b] Notice from %s, posted %s:" },
+    { "GMSG_MOTD_HEADER", "$b------------- MESSAGE(S) OF THE DAY --------------$b" },
+    { "GMSG_MOTD_FOOTER", "$b---------- END OF MESSAGE(S) OF THE DAY ----------$b" },
+    { NULL, NULL }
+};
+
+#define GLOBAL_SYNTAX()   svccmd_send_help(user, global, cmd)
+#define GLOBAL_FUNC(NAME) MODCMD_FUNC(NAME)
+
+struct userNode *global;
+
+static struct module *global_module;
+static struct service *global_service;
+static struct globalMessage *messageList;
+static long messageCount;
+static time_t last_max_alert;
+static struct log_type *G_LOG;
+
+static struct
+{
+    unsigned long db_backup_frequency;
+    unsigned int announcements_default : 1;
+} global_conf;
+
+#define global_notice(target, format...) send_message(target , global , ## format)
+
+void message_expire(void *data);
+
+static struct globalMessage*
+message_add(long flags, time_t posted, unsigned long duration, char *from, const char *msg)
+{
+    struct globalMessage *message;
+
+    message = malloc(sizeof(struct globalMessage));
+
+    if(!message)
+    {
+       return NULL;
+    }
+
+    message->id = messageCount++;
+    message->flags = flags;
+    message->posted = posted;
+    message->duration = duration;
+    message->from = strdup(from);
+    message->message = strdup(msg);
+
+    if(messageList)
+    {
+       messageList->prev = message;
+    }
+
+    message->prev = NULL;
+    message->next = messageList;
+
+    messageList = message;
+
+    if(duration)
+    {
+       timeq_add(now + duration, message_expire, message);
+    }
+
+    return message;
+}
+
+static void
+message_del(struct globalMessage *message)
+{
+    if(message->duration)
+    {
+       timeq_del(0, NULL, message, TIMEQ_IGNORE_FUNC | TIMEQ_IGNORE_WHEN);
+    }
+
+    if(message->prev) message->prev->next = message->next;
+    else messageList = message->next;
+
+    if(message->next) message->next->prev = message->prev;
+
+    free(message->from);
+    free(message->message);
+    free(message);
+}
+
+void message_expire(void *data)
+{
+    struct globalMessage *message = data;
+
+    message->duration = 0;
+    message_del(message);
+}
+
+static struct globalMessage*
+message_create(struct userNode *user, unsigned int argc, char *argv[])
+{
+    unsigned long duration = 0;
+    char *text = NULL;
+    long flags = 0;
+    unsigned int i;
+
+    for(i = 0; i < argc; i++)
+    {
+       if((i + 1) > argc)
+       {
+           global_notice(user, "MSG_MISSING_PARAMS", argv[argc]);
+           return NULL;
+       }
+
+       if(!irccasecmp(argv[i], "text"))
+       {
+           i++;
+           text = unsplit_string(argv + i, argc - i, NULL);
+           break;
+       } else if (!irccasecmp(argv[i], "sourceless")) {
+           i++;
+           flags |= MESSAGE_OPTION_SOURCELESS;
+       } else if (!irccasecmp(argv[i], "target")) {
+           i++;
+
+           if(!irccasecmp(argv[i], "all")) {
+               flags |= MESSAGE_RECIPIENT_ALL;
+           } else if(!irccasecmp(argv[i], "users")) {
+               flags |= MESSAGE_RECIPIENT_LUSERS;
+           } else if(!irccasecmp(argv[i], "helpers")) {
+               flags |= MESSAGE_RECIPIENT_HELPERS;
+           } else if(!irccasecmp(argv[i], "opers")) {
+               flags |= MESSAGE_RECIPIENT_OPERS;
+           } else if(!irccasecmp(argv[i], "staff") || !irccasecmp(argv[i], "privileged")) {
+               flags |= MESSAGE_RECIPIENT_STAFF;
+           } else if(!irccasecmp(argv[i], "channels")) {
+               flags |= MESSAGE_RECIPIENT_CHANNELS;
+            } else if(!irccasecmp(argv[i], "announcement") || !irccasecmp(argv[i], "announce")) {
+                flags |= MESSAGE_RECIPIENT_ANNOUNCE;
+           } else {
+               global_notice(user, "GMSG_INVALID_TARGET", argv[i]);
+               return NULL;
+           }
+       } else if (irccasecmp(argv[i], "duration") == 0) {
+           duration = ParseInterval(argv[++i]);
+       } else {
+           global_notice(user, "MSG_INVALID_CRITERIA", argv[i]);
+           return NULL;
+       }
+    }
+
+    if(!flags)
+    {
+       flags = MESSAGE_RECIPIENT_LUSERS;
+    }
+
+    if(!text) {
+       global_notice(user, "GMSG_MESSAGE_REQUIRED");
+       return NULL;
+    }
+
+    return message_add(flags, now, duration, user->handle_info->handle, text);
+}
+
+static const char *
+messageType(const struct globalMessage *message)
+{
+    if((message->flags & MESSAGE_RECIPIENT_ALL) == MESSAGE_RECIPIENT_ALL)
+    {
+       return "all";
+    }
+    if((message->flags & MESSAGE_RECIPIENT_STAFF) == MESSAGE_RECIPIENT_STAFF)
+    {
+       return "staff";
+    }
+    else if(message->flags & MESSAGE_RECIPIENT_ANNOUNCE)
+    {
+        return "announcement";
+    }
+    else if(message->flags & MESSAGE_RECIPIENT_OPERS)
+    {
+       return "opers";
+    }
+    else if(message->flags & MESSAGE_RECIPIENT_HELPERS)
+    {
+       return "helpers";
+    }
+    else if(message->flags & MESSAGE_RECIPIENT_LUSERS)
+    {
+       return "users";
+    }
+    else
+    {
+       return "channels";
+    }
+}
+
+static void
+notice_target(const char *target, struct globalMessage *message)
+{
+    if(!(message->flags & MESSAGE_OPTION_SOURCELESS))
+    {
+       if(message->flags & MESSAGE_OPTION_IMMEDIATE)
+       {
+           send_target_message(0, target, global, "GMSG_NOTICE_SOURCE", messageType(message), message->from);
+       }
+       else
+       {
+           char posted[24];
+           struct tm tm;
+
+           localtime_r(&message->posted, &tm);
+           strftime(posted, sizeof(posted), "%I:%M %p, %m/%d/%Y", &tm);
+           send_target_message(0, target, global, "GMSG_MESSAGE_SOURCE", messageType(message), message->from, posted);
+       }
+    }
+
+    send_target_message(4, target, global, "%s", message->message);
+}
+
+static int
+notice_channel(const char *key, void *data, void *extra)
+{
+    struct chanNode *channel = data;
+    /* It should be safe to assume channel is not NULL. */
+    if(channel->channel_info)
+       notice_target(key, extra);
+    return 0;
+}
+
+static void
+message_send(struct globalMessage *message)
+{
+    struct userNode *user;
+    unsigned long n;
+    dict_iterator_t it;
+
+    if(message->flags & MESSAGE_RECIPIENT_CHANNELS)
+    {
+       dict_foreach(channels, notice_channel, message);
+    }
+
+    if(message->flags & MESSAGE_RECIPIENT_LUSERS)
+    {
+       notice_target("$*", message);
+       return;
+    }
+
+    if(message->flags & MESSAGE_RECIPIENT_ANNOUNCE)
+    {
+        char announce;
+
+        for (it = dict_first(clients); it; it = iter_next(it)) {
+            user = iter_data(it);
+            if (user->uplink == self) continue;
+            announce = user->handle_info ? user->handle_info->announcements : '?';
+            if (announce == 'n') continue;
+            if ((announce == '?') && !global_conf.announcements_default) continue;
+            notice_target(user->nick, message);
+        }
+    }
+
+    if(message->flags & MESSAGE_RECIPIENT_OPERS)
+    {
+       for(n = 0; n < curr_opers.used; n++)
+       {
+           user = curr_opers.list[n];
+
+           if(user->uplink != self)
+           {
+               notice_target(user->nick, message);
+           }
+       }
+    }
+
+    if(message->flags & MESSAGE_RECIPIENT_HELPERS)
+    {
+       for(n = 0; n < curr_helpers.used; n++)
+       {
+           user = curr_helpers.list[n];
+            if (IsOper(user))
+                continue;
+           notice_target(user->nick, message);
+       }
+    }
+}
+
+void
+global_message(long targets, char *text)
+{
+    struct globalMessage *message;
+
+    if(!targets || !global)
+       return;
+
+    message = message_add(targets | MESSAGE_OPTION_SOURCELESS, now, 0, "", text);
+    if(!message)
+       return;
+
+    message_send(message);
+    message_del(message);
+}
+
+static GLOBAL_FUNC(cmd_notice)
+{
+    struct globalMessage *message = NULL;
+    const char *recipient = NULL, *text;
+    long target = 0;
+
+    assert(argc >= 3);
+    if(!irccasecmp(argv[1], "all")) {
+       target = MESSAGE_RECIPIENT_ALL;
+    } else if(!irccasecmp(argv[1], "users")) {
+       target = MESSAGE_RECIPIENT_LUSERS;
+    } else if(!irccasecmp(argv[1], "helpers")) {
+       target = MESSAGE_RECIPIENT_HELPERS;
+    } else if(!irccasecmp(argv[1], "opers")) {
+       target = MESSAGE_RECIPIENT_OPERS;
+    } else if(!irccasecmp(argv[1], "staff") || !irccasecmp(argv[1], "privileged")) {
+       target |= MESSAGE_RECIPIENT_HELPERS | MESSAGE_RECIPIENT_OPERS;
+    } else if(!irccasecmp(argv[1], "announcement") || !irccasecmp(argv[1], "announce")) {
+        target |= MESSAGE_RECIPIENT_ANNOUNCE;
+    } else if(!irccasecmp(argv[1], "channels")) {
+       target = MESSAGE_RECIPIENT_CHANNELS;
+    } else {
+       global_notice(user, "GMSG_INVALID_TARGET", argv[1]);
+       return 0;
+    }
+
+    text = unsplit_string(argv + 2, argc - 2, NULL);
+    message = message_add(target | MESSAGE_OPTION_IMMEDIATE, now, 0, user->handle_info->handle, text);
+
+    if(!message)
+    {
+       return 0;
+    }
+
+    recipient = messageType(message);
+
+    message_send(message);
+    message_del(message);
+
+    global_notice(user, "GMSG_MESSAGE_SENT", recipient);
+    return 1;
+}
+
+static GLOBAL_FUNC(cmd_message)
+{
+    struct globalMessage *message = NULL;
+    const char *recipient = NULL;
+
+    assert(argc >= 3);
+    message = message_create(user, argc - 1, argv + 1);
+    if(!message)
+        return 0;
+    recipient = messageType(message);
+    global_notice(user, "GMSG_MESSAGE_ADDED", recipient, message->id);
+    return 1;
+}
+
+static GLOBAL_FUNC(cmd_list)
+{
+    struct globalMessage *message;
+    struct helpfile_table table;
+    unsigned int length, nn;
+
+    if(!messageList)
+    {
+       global_notice(user, "GMSG_NO_MESSAGES");
+        return 1;
+    }
+
+    for(nn=0, message = messageList; message; nn++, message=message->next) ;
+    table.length = nn+1;
+    table.width = 5;
+    table.flags = TABLE_NO_FREE;
+    table.contents = calloc(table.length, sizeof(char**));
+    table.contents[0] = calloc(table.width, sizeof(char*));
+    table.contents[0][0] = "ID";
+    table.contents[0][1] = "Target";
+    table.contents[0][2] = "Expires";
+    table.contents[0][3] = "From";
+    table.contents[0][4] = "Message";
+
+    for(nn=1, message = messageList; message; nn++, message = message->next)
+    {
+        char buffer[64];
+
+        table.contents[nn] = calloc(table.width, sizeof(char*));
+        snprintf(buffer, sizeof(buffer), "%lu", message->id);
+        table.contents[nn][0] = strdup(buffer);
+        table.contents[nn][1] = messageType(message);
+        if(message->duration)
+        {
+            intervalString(buffer, message->posted + message->duration - now);
+        }
+        else
+        {
+            strcpy(buffer, "Never.");
+        }
+        table.contents[nn][2] = strdup(buffer);
+        table.contents[nn][3] = message->from;
+       length = strlen(message->message);
+       safestrncpy(buffer, message->message, sizeof(buffer));
+       if(length > (sizeof(buffer) - 4))
+       {
+           buffer[sizeof(buffer) - 1] = 0;
+           buffer[sizeof(buffer) - 2] = buffer[sizeof(buffer) - 3] = buffer[sizeof(buffer) - 4] = '.';
+       }
+        table.contents[nn][4] = strdup(buffer);
+    }
+    table_send(global, user->nick, 0, NULL, table);
+    for (nn=1; nn<table.length; nn++)
+    {
+        free((char*)table.contents[nn][0]);
+        free((char*)table.contents[nn][2]);
+        free((char*)table.contents[nn][4]);
+        free(table.contents[nn]);
+    }
+    free(table.contents[0]);
+    free(table.contents);
+
+    return 1;
+}
+
+static GLOBAL_FUNC(cmd_remove)
+{
+    struct globalMessage *message = NULL;
+    unsigned long id;
+
+    assert(argc >= 2);
+    id = strtoul(argv[1], NULL, 0);
+
+    for(message = messageList; message; message = message->next)
+    {
+       if(message->id == id)
+       {
+           message_del(message);
+           global_notice(user, "GMSG_MESSAGE_DELETED", argv[1]);
+           return 1;
+       }
+    }
+
+    global_notice(user, "GMSG_ID_INVALID", argv[1]);
+    return 0;
+}
+
+static unsigned int
+send_messages(struct userNode *user, long mask, int obstreperize)
+{
+    struct globalMessage *message = messageList;
+    unsigned int count = 0;
+
+    while(message)
+    {
+       if(message->flags & mask)
+       {
+            if (obstreperize && !count)
+                send_target_message(0, user->nick, global, "GMSG_MOTD_HEADER");
+           notice_target(user->nick, message);
+           count++;
+       }
+
+       message = message->next;
+    }
+    if (obstreperize && count)
+        send_target_message(0, user->nick, global, "GMSG_MOTD_FOOTER");
+    return count;
+}
+
+static GLOBAL_FUNC(cmd_messages)
+{
+    long mask = MESSAGE_RECIPIENT_LUSERS | MESSAGE_RECIPIENT_CHANNELS;
+    unsigned int count;
+
+    if(IsOper(user))
+       mask |= MESSAGE_RECIPIENT_OPERS;
+
+    if(IsHelper(user))
+       mask |= MESSAGE_RECIPIENT_HELPERS;
+
+    count = send_messages(user, mask, 0);
+    if(count)
+       global_notice(user, "GMSG_MESSAGE_COUNT", count);
+    else
+       global_notice(user, "GMSG_NO_MESSAGES");
+
+    return 1;
+}
+
+static int
+global_process_user(struct userNode *user)
+{
+    if(IsLocal(user) || self->uplink->burst || user->uplink->burst)
+        return 0;
+    send_messages(user, MESSAGE_RECIPIENT_LUSERS, 1);
+
+    /* only alert on new usercount if the record was broken in the last
+     * 30 seconds, and no alert had been sent in that time.
+     */
+    if((now - max_clients_time) <= 30 && (now - last_max_alert) > 30)
+    {
+       char *message;
+       message = alloca(36);
+       sprintf(message, "New user count record: %d", max_clients);
+       global_message(MESSAGE_RECIPIENT_OPERS, message);
+       last_max_alert = now;
+    }
+
+    return 0;
+}
+
+static void
+global_process_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
+{
+    if(IsHelper(user))
+       send_messages(user, MESSAGE_RECIPIENT_HELPERS, 0);
+}
+
+static void
+global_process_oper(struct userNode *user)
+{
+    if(user->uplink->burst)
+        return;
+    send_messages(user, MESSAGE_RECIPIENT_OPERS, 0);
+}
+
+static void
+global_conf_read(void)
+{
+    dict_t conf_node;
+    const char *str;
+
+    if (!(conf_node = conf_get_data(GLOBAL_CONF_NAME, RECDB_OBJECT))) {
+       log_module(G_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", GLOBAL_CONF_NAME);
+       return;
+    }
+
+    str = database_get_data(conf_node, KEY_DB_BACKUP_FREQ, RECDB_QSTRING);
+    global_conf.db_backup_frequency = str ? ParseInterval(str) : 7200;
+    str = database_get_data(conf_node, KEY_ANNOUNCEMENTS_DEFAULT, RECDB_QSTRING);
+    global_conf.announcements_default = str ? enabled_string(str) : 1;
+
+    str = database_get_data(conf_node, KEY_NICK, RECDB_QSTRING);
+    if(str)
+        NickChange(global, str, 0);
+}
+
+static int
+global_saxdb_read(struct dict *db)
+{
+    struct record_data *hir;
+    time_t posted;
+    long flags;
+    unsigned long duration;
+    char *str, *from, *message;
+    dict_iterator_t it;
+
+    for(it=dict_first(db); it; it=iter_next(it))
+    {
+        hir = iter_data(it);
+       if(hir->type != RECDB_OBJECT)
+       {
+           log_module(G_LOG, LOG_WARNING, "Unexpected rectype %d for %s.", hir->type, iter_key(it));
+            continue;
+       }
+
+        str = database_get_data(hir->d.object, KEY_FLAGS, RECDB_QSTRING);
+        flags = str ? strtoul(str, NULL, 0) : 0;
+
+        str = database_get_data(hir->d.object, KEY_POSTED, RECDB_QSTRING);
+        posted = str ? strtoul(str, NULL, 0) : 0;
+
+        str = database_get_data(hir->d.object, KEY_DURATION, RECDB_QSTRING);
+        duration = str ? strtoul(str, NULL, 0) : 0;
+
+        from = database_get_data(hir->d.object, KEY_FROM, RECDB_QSTRING);
+        message = database_get_data(hir->d.object, KEY_MESSAGE, RECDB_QSTRING);
+
+       message_add(flags, posted, duration, from, message);
+    }
+    return 0;
+}
+
+static int
+global_saxdb_write(struct saxdb_context *ctx)
+{
+    struct globalMessage *message;
+    char str[16];
+
+    for(message = messageList; message; message = message->next) {
+        snprintf(str, sizeof(str), "%li", message->id);
+        saxdb_start_record(ctx, str, 0);
+        saxdb_write_int(ctx, KEY_FLAGS, message->flags);
+        saxdb_write_int(ctx, KEY_POSTED, message->posted);
+        saxdb_write_int(ctx, KEY_DURATION, message->duration);
+        saxdb_write_string(ctx, KEY_FROM, message->from);
+        saxdb_write_string(ctx, KEY_MESSAGE, message->message);
+        saxdb_end_record(ctx);
+    }
+    return 0;
+}
+
+static void
+global_db_cleanup(void)
+{
+    while (messageList) message_del(messageList);
+}
+
+void
+init_global(const char *nick)
+{
+    global = AddService(nick, "Global Services");
+    G_LOG = log_register_type("Global", "file:global.log");
+    reg_new_user_func(global_process_user);
+    reg_auth_func(global_process_auth);
+    reg_oper_func(global_process_oper);
+
+    conf_register_reload(global_conf_read);
+
+    global_module = module_register("Global", G_LOG, "global.help", NULL);
+    modcmd_register(global_module, "LIST", cmd_list, 1, 0, "flags", "+oper", NULL);
+    modcmd_register(global_module, "MESSAGE", cmd_message, 3, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
+    modcmd_register(global_module, "MESSAGES", cmd_messages, 1, 0, NULL);
+    modcmd_register(global_module, "NOTICE", cmd_notice, 3, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
+    modcmd_register(global_module, "REMOVE", cmd_remove, 2, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
+
+    global_service = service_register(global, 0);
+    saxdb_register("Global", global_saxdb_read, global_saxdb_write);
+    reg_exit_func(global_db_cleanup);
+    message_register_table(msgtab);
+}
diff --git a/src/global.h b/src/global.h
new file mode 100644 (file)
index 0000000..d7001c7
--- /dev/null
@@ -0,0 +1,53 @@
+/* global.h - Global notice service
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef _global_h
+#define _global_h
+
+#define MESSAGE_RECIPIENT_LUSERS               0x001
+#define MESSAGE_RECIPIENT_HELPERS              0x002
+#define MESSAGE_RECIPIENT_OPERS                        0x004
+#define MESSAGE_RECIPIENT_CHANNELS                     0x008
+#define MESSAGE_RECIPIENT_ANNOUNCE                     0x040
+
+#define MESSAGE_OPTION_SOURCELESS              0x010
+#define MESSAGE_OPTION_IMMEDIATE               0x020
+
+#define MESSAGE_RECIPIENT_STAFF                        (MESSAGE_RECIPIENT_HELPERS | MESSAGE_RECIPIENT_OPERS)
+#define MESSAGE_RECIPIENT_ALL                  (MESSAGE_RECIPIENT_LUSERS | MESSAGE_RECIPIENT_CHANNELS)
+
+struct globalMessage
+{
+    unsigned long                              id;
+    long                               flags;
+
+    time_t                             posted;
+    unsigned long                      duration;
+
+    char                               *from;
+    char                               *message;
+
+    struct globalMessage               *prev;
+    struct globalMessage               *next;
+};
+
+void init_global(const char *nick);
+
+void global_message(long targets, char *text);
+
+#endif
diff --git a/src/global.help b/src/global.help
new file mode 100644 (file)
index 0000000..19396d9
--- /dev/null
@@ -0,0 +1,37 @@
+"<INDEX>" ("$b$G Help$b",
+        "The $b$G$b service allows network administrators to manage and send important notices to users. It also allows users to retrieve, at once, all messages addressed to them.",
+        "$bUser Commands:$b",
+        "  MESSAGES   Sends you all messages addressed to your user class.",
+        "  VERSION    Prints the srvx and $G version information.",
+        "$bPrivileged Commands:$b",
+        "  NOTICE     Immediately sends a message.",
+        "  MESSAGE    Adds a message to $G's database.",
+        "  LIST       Displays active messages.",
+        "  REMOVE     Removes a message.");
+"LIST" ("/msg $G LIST",
+        "Displays all active messages and information pertaining to them, such as the target, message ID, expiration time, and who the message is from.",
+        "$uSee Also:$u message, messages, remove");
+"MESSAGE" ("/msg $G MESSAGE [<options> <value>] text <message>",
+        "Adds a notice to the $b$G$b database. Messages are sent to users as they enter the network or the target class. $bMessage$b takes several options, which must be preceded by the name of the option being used. Options include:",
+        "$bTARGET$b:   Controls the recipients of the message. This option may be used multiple times. See the $bTARGET$b help entry for details.",
+        "$bDURATION$b: This option sets the length of time for which the message is valid. After this time, the message will be deleted from the $b$G$b database.",
+        "$uSee Also:$u list, messages, remove, target");
+"MESSAGES" ("/msg $G MESSAGES",
+        "Sends you all messages addressed to your user class.");
+"NOTICE" ("/msg $G NOTICE <target> <message>",
+        "Immediately sends a notice to a specific target. See $btarget$b for a list of targets.",
+        "$uSee Also:$u target");
+"REMOVE" ("/msg $G REMOVE <message id>",
+        "Remove a message before it expires. The message ID can be found in the message you received when using $bsend$b to first add the message, or by using $blist$b.",
+        "$uSee Also:$u list, message");
+"TARGET" ("$bTARGET$b",
+        "$bTarget$b is used as a sub-command in many commands. It's values are:",
+        "$bUSERS$b:      The message will be sent to all users on the network including opers and helpers, but not channels.",
+        "$bANNOUNCEMENT$b: The message will be sent to all users who are configured to receive community announcements.",
+        "$bHELPERS$b:    The message will be sent to helpers only.",
+        "$bOPERS$b:      The message will be sent to opers only.",
+        "$bSTAFF$b:      The message will be sent to helpers and opers.",
+        "$bCHANNELS$b:   The message will be sent to all channels.",
+        "$bALL$b:        A combination of USERS and CHANNELS.");
+"VERSION" ("/msg $G VERSION",
+        "$bVERSION$b causes $b$G$b to to send you the srvx version and some additional version information that is specific to $b$G$b.");
diff --git a/src/globtest.c b/src/globtest.c
new file mode 100644 (file)
index 0000000..99660b9
--- /dev/null
@@ -0,0 +1,82 @@
+#include "hash.h"
+#include "log.h"
+
+struct glob_test {
+    const char *glob;
+    const char *texts[8];
+};
+
+struct glob_test glob_yes[] = {
+    { "*Zoot*!*@*.org", { "Zoot!Zoot@services.org",
+                         "zoot!bleh@j00.are.r00t3d.org",
+                          0 } },
+    { "*!*@*", { "DK-Entrope!entrope@clan-dk.dyndns.org",
+                 0 } },
+    { "*", { "anything at all!",
+            0 } },
+    { 0, { 0 } }
+};
+
+struct glob_test glob_no[] = {
+    { "*Zoot*!*@*.org", { "Zoot!Zoot@services.net",
+                          0 } },
+    { "*!*@*", { "luser@host.com",
+                0 } },
+    { 0, { 0 } }
+};
+
+struct glob_test glob_globs[] = {
+    { "*@foo", { "foo@bar",
+                 "bar@foo",
+                 0 } },
+    { "foo@bar", { "*@foo",
+                   "bar@foo",
+                   "foo@bar",
+                   0 } },
+    { 0, { 0 } }
+};
+
+int
+main(UNUSED_ARG(int argc), UNUSED_ARG(char *argv[]))
+{
+    int i, j;
+
+    for (i = 0; glob_yes[i].glob; i++) {
+       for (j=0; glob_yes[i].texts[j]; j++) {
+           if (!match_ircglob(glob_yes[i].texts[j], glob_yes[i].glob)) {
+               fprintf(stderr, "%s did not match glob %s!\n",
+                       glob_yes[i].texts[j], glob_yes[i].glob);
+           }
+       }
+    }
+
+    for (i = 0; glob_no[i].glob; i++) {
+       for (j=0; glob_no[i].texts[j]; j++) {
+           if (match_ircglob(glob_no[i].texts[j], glob_no[i].glob)) {
+               fprintf(stderr, "%s matched glob %s!\n",
+                       glob_no[i].texts[j], glob_no[i].glob);
+           }
+       }
+    }
+
+    for (i=0; glob_globs[i].glob; i++) {
+        for (j=0; glob_globs[i].texts[j]; j++) {
+            fprintf(stdout, "match_ircglobs(\"%s\", \"%s\") -> %d\n",
+                    glob_globs[i].glob, glob_globs[i].texts[j],
+                    match_ircglobs(glob_globs[i].glob, glob_globs[i].texts[j]));
+        }
+    }
+
+    return 0;
+}
+
+/* because tools.c likes to log stuff.. */
+void log(UNUSED_ARG(enum log_type lt), UNUSED_ARG(enum log_severity ls), char *format, ...)
+{
+    va_list va;
+    va_start(va, format);
+    vfprintf(stderr, format, va);
+    va_end(va);
+}
+
+const char *hidden_host_suffix;
diff --git a/src/hash.c b/src/hash.c
new file mode 100644 (file)
index 0000000..8952ac0
--- /dev/null
@@ -0,0 +1,720 @@
+/* hash.c - IRC network state database
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "conf.h"
+#include "global.h"
+#include "hash.h"
+#include "log.h"
+
+struct server *self;
+dict_t channels;
+dict_t clients;
+dict_t servers;
+unsigned int max_clients, invis_clients;
+time_t max_clients_time;
+struct userList curr_opers;
+
+static void hash_cleanup(void);
+
+void init_structs(void)
+{
+    channels = dict_new();
+    clients = dict_new();
+    servers = dict_new();
+    userList_init(&curr_opers);
+    reg_exit_func(hash_cleanup);
+}
+
+server_link_func_t *slf_list;
+unsigned int slf_size = 0, slf_used = 0;
+
+void
+reg_server_link_func(server_link_func_t handler)
+{
+    if (slf_used == slf_size) {
+       if (slf_size) {
+           slf_size <<= 1;
+           slf_list = realloc(slf_list, slf_size*sizeof(server_link_func_t));
+       } else {
+           slf_size = 8;
+           slf_list = malloc(slf_size*sizeof(server_link_func_t));
+       }
+    }
+    slf_list[slf_used++] = handler;
+}
+
+struct server*
+GetServerH(const char *name)
+{
+    return dict_find(servers, name, NULL);
+}
+
+new_user_func_t *nuf_list;
+unsigned int nuf_size = 0, nuf_used = 0;
+
+void
+reg_new_user_func(new_user_func_t handler)
+{
+    if (nuf_used == nuf_size) {
+       if (nuf_size) {
+           nuf_size <<= 1;
+           nuf_list = realloc(nuf_list, nuf_size*sizeof(new_user_func_t));
+       } else {
+           nuf_size = 8;
+           nuf_list = malloc(nuf_size*sizeof(new_user_func_t));
+       }
+    }
+    nuf_list[nuf_used++] = handler;
+}
+
+static nick_change_func_t *ncf2_list;
+static unsigned int ncf2_size = 0, ncf2_used = 0;
+
+void
+reg_nick_change_func(nick_change_func_t handler)
+{
+    if (ncf2_used == ncf2_size) {
+        if (ncf2_size) {
+            ncf2_size <<= 1;
+            ncf2_list = realloc(ncf2_list, ncf2_size*sizeof(nick_change_func_t));
+        } else {
+            ncf2_size = 8;
+            ncf2_list = malloc(ncf2_size*sizeof(nick_change_func_t));
+        }
+    }
+    ncf2_list[ncf2_used++] = handler;
+}
+
+
+del_user_func_t *duf_list;
+unsigned int duf_size = 0, duf_used = 0;
+
+void
+reg_del_user_func(del_user_func_t handler)
+{
+    if (duf_used == duf_size) {
+       if (duf_size) {
+           duf_size <<= 1;
+           duf_list = realloc(duf_list, duf_size*sizeof(del_user_func_t));
+       } else {
+           duf_size = 8;
+           duf_list = malloc(duf_size*sizeof(del_user_func_t));
+       }
+    }
+    duf_list[duf_used++] = handler;
+}
+
+void
+unreg_del_user_func(del_user_func_t handler)
+{
+    unsigned int i;
+    for (i=0; i<duf_used; i++) {
+        if (duf_list[i] == handler) break;
+    }
+    if (i == duf_used) return;
+    memmove(duf_list+i, duf_list+i+1, (duf_used-i-1)*sizeof(duf_list[0]));
+    duf_used--;
+}
+
+/* reintroduces a user after it has been killed. */
+void
+ReintroduceUser(struct userNode *user)
+{
+    struct mod_chanmode change;
+    unsigned int n;
+       
+    irc_user(user);
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    for (n = 0; n < user->channels.used; n++) {
+        struct modeNode *mn = user->channels.list[n];
+       irc_join(user, mn->channel);
+        if (mn->modes) {
+            change.args[0].mode = mn->modes;
+            change.args[0].member = mn;
+            mod_chanmode_announce(user, mn->channel, &change);
+        }
+    }
+}
+
+void
+NickChange(struct userNode* user, const char *new_nick, int no_announce)
+{
+    char *old_nick;
+    unsigned int nn;
+
+    /* don't do anything if there's no change */
+    old_nick = user->nick;
+    if (!strncmp(new_nick, old_nick, NICKLEN))
+        return;
+
+    /* remove old entry from clients dictionary */
+    dict_remove(clients, old_nick);
+#if !defined(WITH_PROTOCOL_P10)
+    /* Remove from uplink's clients dict */
+    dict_remove(user->uplink->users, old_nick);
+#endif
+    /* and reinsert */
+    user->nick = strdup(new_nick);
+    dict_insert(clients, user->nick, user);
+#if !defined(WITH_PROTOCOL_P10)
+    dict_insert(user->uplink->users, user->nick, user);
+#endif
+
+    /* Make callbacks for nick changes.  Do this with new nick in
+     * place because that is slightly more useful.
+     */
+    for (nn=0; nn<ncf2_used; nn++)
+        ncf2_list[nn](user, old_nick);
+    user->timestamp = now;
+    if (IsLocal(user) && !no_announce)
+        irc_nick(user, old_nick);
+    free(old_nick);
+}
+
+struct userNode *
+GetUserH(const char *nick)
+{
+    return dict_find(clients, nick, NULL);
+}
+
+static account_func_t account_func;
+
+void
+reg_account_func(account_func_t handler)
+{
+    if (account_func) {
+        log_module(MAIN_LOG, LOG_WARNING, "Reregistering ACCOUNT handler.");
+    }
+    account_func = handler;
+}
+
+void
+call_account_func(struct userNode *user, const char *stamp)
+{
+    /* We've received an account stamp for a user; notify
+       NickServ, which registers the sole account_func
+       right now.
+
+       P10 Protocol violation if (user->modes & FLAGS_STAMPED) here.
+    */
+    if (account_func)
+        account_func(user, stamp);
+
+#ifdef WITH_PROTOCOL_P10
+    /* Mark the user so we don't stamp it again. */
+    user->modes |= FLAGS_STAMPED;
+#endif
+}
+
+void
+StampUser(struct userNode *user, const char *stamp)
+{
+#ifdef WITH_PROTOCOL_P10
+    /* The P10 protocol says we can't stamp users who already
+       have a stamp. */
+    if (IsStamped(user))
+        return;
+#endif
+
+    irc_account(user, stamp);
+    user->modes |= FLAGS_STAMPED;
+}
+
+static new_channel_func_t *ncf_list;
+static unsigned int ncf_size = 0, ncf_used = 0;
+
+void
+reg_new_channel_func(new_channel_func_t handler)
+{
+    if (ncf_used == ncf_size) {
+       if (ncf_size) {
+           ncf_size <<= 1;
+           ncf_list = realloc(ncf_list, ncf_size*sizeof(ncf_list[0]));
+       } else {
+           ncf_size = 8;
+           ncf_list = malloc(ncf_size*sizeof(ncf_list[0]));
+       }
+    }
+    ncf_list[ncf_used++] = handler;
+}
+
+static join_func_t *jf_list;
+static unsigned int jf_size = 0, jf_used = 0;
+
+void
+reg_join_func(join_func_t handler)
+{
+    if (jf_used == jf_size) {
+       if (jf_size) {
+           jf_size <<= 1;
+           jf_list = realloc(jf_list, jf_size*sizeof(join_func_t));
+       } else {
+           jf_size = 8;
+           jf_list = malloc(jf_size*sizeof(join_func_t));
+       }
+    }
+    jf_list[jf_used++] = handler;
+}
+
+int rel_age;
+
+static void
+wipeout_channel(struct chanNode *cNode, time_t new_time, char **modes, unsigned int modec) {
+    unsigned int orig_limit;
+    chan_mode_t orig_modes;
+    char orig_key[KEYLEN+1];
+    unsigned int nn, argc;
+
+    /* remember the old modes, and update them with the new */
+    orig_modes = cNode->modes;
+    orig_limit = cNode->limit;
+    strcpy(orig_key, cNode->key);
+    cNode->modes = 0;
+    mod_chanmode(NULL, cNode, modes, modec, 0);
+    cNode->timestamp = new_time;
+
+    /* remove our old ban list, replace it with the new one */
+    for (nn=0; nn<cNode->banlist.used; nn++)
+        free(cNode->banlist.list[nn]);
+    cNode->banlist.used = 0;
+
+    /* deop anybody in the channel now, but count services to reop */
+    for (nn=argc=0; nn<cNode->members.used; nn++) {
+        struct modeNode *mn = cNode->members.list[nn];
+        if (mn->modes & MODE_CHANOP && IsService(mn->user) && IsLocal(mn->user))
+            argc++;
+    }
+    if (argc) {
+        extern struct userNode *opserv;
+        struct mod_chanmode *change;
+
+        change = mod_chanmode_alloc(nn);
+        change->modes_clear = 0;
+        change->modes_set = orig_modes;
+        change->new_limit = orig_limit;
+        strcpy(change->new_key, orig_key);
+        for (nn = argc = 0; nn < cNode->members.used; ++nn) {
+            struct modeNode *mn = cNode->members.list[nn];
+            if ((mn->modes & MODE_CHANOP) && IsService(mn->user) && IsLocal(mn->user)) {
+                change->args[argc].mode = MODE_CHANOP;
+                change->args[argc].member = mn;
+                argc++;
+            }
+        }
+        if (change->argc > 0)
+            mod_chanmode_announce(change->args[0].member->user, cNode, change);
+        else
+            mod_chanmode_announce(opserv, cNode, change);
+        mod_chanmode_free(change);
+    }
+}
+
+struct chanNode *
+AddChannel(const char *name, time_t time_, const char *modes, char *banlist)
+{
+    struct chanNode *cNode;
+    char new_modes[MAXLEN], *argv[MAXNUMPARAMS];
+    unsigned int nn;
+
+    if (!IsChannelName(name)) {
+        log_module(MAIN_LOG, LOG_ERROR, "Somebody asked to add channel '%s', which isn't a channel name!", name);
+        return NULL;
+    }
+    if (!modes)
+        modes = "";
+
+    safestrncpy(new_modes, modes, sizeof(new_modes));
+    nn = split_line(new_modes, 0, ArrayLength(argv), argv);
+    if (!(cNode = GetChannel(name))) {
+        cNode = calloc(1, sizeof(*cNode) + strlen(name));
+        strcpy(cNode->name, name);
+        banList_init(&cNode->banlist);
+        modeList_init(&cNode->members);
+        mod_chanmode(NULL, cNode, argv, nn, 0);
+        dict_insert(channels, cNode->name, cNode);
+        cNode->timestamp = time_;
+        rel_age = 1;
+    } else if (cNode->timestamp > time_) {
+        wipeout_channel(cNode, time_, argv, nn);
+        rel_age = 1;
+    } else if (cNode->timestamp == time_) {
+        mod_chanmode(NULL, cNode, argv, nn, 0);
+        rel_age = 0;
+    } else {
+        rel_age = -1;
+    }
+
+    /* rel_age is the relative ages of our channel data versus what is
+     * in a BURST command.  1 means ours is younger, 0 means both are
+     * the same age, -1 means ours is older. */
+
+    /* if it's a new or updated channel, make callbacks */
+    if (rel_age > 0)
+        for (nn=0; nn<ncf_used; nn++)
+            ncf_list[nn](cNode);
+
+    /* go through list of bans and add each one */
+    if (banlist && (rel_age >= 0)) {
+        for (nn=0; banlist[nn];) {
+            char *ban = banlist + nn;
+            struct banNode *bn;
+            while (banlist[nn] != ' ' && banlist[nn])
+                nn++;
+            while (banlist[nn] == ' ')
+                banlist[nn++] = 0;
+            bn = calloc(1, sizeof(*bn));
+            safestrncpy(bn->ban, ban, sizeof(bn->ban));
+            safestrncpy(bn->who, "<unknown>", sizeof(bn->who));
+            bn->set = now;
+            banList_append(&cNode->banlist, bn);
+        }
+    }
+
+    return cNode;
+}
+
+static del_channel_func_t *dcf_list;
+static unsigned int dcf_size = 0, dcf_used = 0;
+
+void
+reg_del_channel_func(del_channel_func_t handler)
+{
+    if (dcf_used == dcf_size) {
+       if (dcf_size) {
+           dcf_size <<= 1;
+           dcf_list = realloc(dcf_list, dcf_size*sizeof(dcf_list[0]));
+       } else {
+           dcf_size = 8;
+           dcf_list = malloc(dcf_size*sizeof(dcf_list[0]));
+       }
+    }
+    dcf_list[dcf_used++] = handler;
+}
+
+static void
+DelChannel(struct chanNode *channel)
+{
+    unsigned int n;
+
+    dict_remove(channels, channel->name);
+
+    if (channel->members.used || channel->locks) {
+        log_module(MAIN_LOG, LOG_ERROR, "Warning: deleting channel %s with %d users and %d locks remaining.", channel->name, channel->members.used, channel->locks);
+    }
+
+    /* go through all channel members and delete them from the channel */
+    for (n=channel->members.used; n>0; )
+       DelChannelUser(channel->members.list[--n]->user, channel, false, 1);
+
+    /* delete all channel bans */
+    for (n=channel->banlist.used; n>0; )
+        free(channel->banlist.list[--n]);
+    channel->banlist.used = 0;
+
+    for (n=0; n<dcf_used; n++)
+        dcf_list[n](channel);
+
+    modeList_clean(&channel->members);
+    banList_clean(&channel->banlist);
+    free(channel);
+}
+
+struct modeNode *
+AddChannelUser(struct userNode *user, struct chanNode* channel)
+{
+       struct modeNode *mNode;
+       unsigned int n;
+
+       mNode = GetUserMode(channel, user);
+       if (mNode)
+            return mNode;
+
+       mNode = malloc(sizeof(*mNode));
+
+       /* set up modeNode */
+       mNode->channel = channel;
+       mNode->user = user;
+       mNode->modes = 0;
+        mNode->idle_since = now;
+
+       /* Add modeNode to channel and to user.
+         * We have to do this before calling join funcs in case the
+         * modeNode is manipulated (e.g. chanserv ops the user).
+         */
+       modeList_append(&channel->members, mNode);
+       modeList_append(&user->channels, mNode);
+
+        for (n=0; n<jf_used; n++) {
+            /* Callbacks return true if they kick or kill the user,
+             * and we can continue without removing mNode. */
+            if (jf_list[n](mNode))
+                return NULL;
+        }
+
+       if (IsLocal(user))
+            irc_join(user, channel);
+
+       return mNode;
+}
+
+static part_func_t *pf_list;
+static unsigned int pf_size = 0, pf_used = 0;
+
+void
+reg_part_func(part_func_t handler)
+{
+    if (pf_used == pf_size) {
+       if (pf_size) {
+           pf_size <<= 1;
+           pf_list = realloc(pf_list, pf_size*sizeof(part_func_t));
+       } else {
+           pf_size = 8;
+           pf_list = malloc(pf_size*sizeof(part_func_t));
+       }
+    }
+    pf_list[pf_used++] = handler;
+}
+
+void
+unreg_part_func(part_func_t handler)
+{
+    unsigned int i;
+    for (i=0; i<pf_used; i++)
+        if (pf_list[i] == handler)
+            break;
+    if (i == pf_used)
+        return;
+    memmove(pf_list+i, pf_list+i+1, (pf_used-i-1)*sizeof(pf_list[0]));
+    pf_used--;
+}
+
+void
+LockChannel(struct chanNode* channel)
+{
+    channel->locks++;
+}
+
+void
+UnlockChannel(struct chanNode *channel)
+{
+    if (!channel->locks)
+        return;
+    if (!--channel->locks && !channel->members.used)
+        DelChannel(channel);
+}
+
+void
+DelChannelUser(struct userNode* user, struct chanNode* channel, const char *reason, int deleting)
+{
+    struct modeNode* mNode;
+    unsigned int n;
+
+    if (reason) {
+        irc_part(user, channel, reason);
+    }
+
+    mNode = GetUserMode(channel, user);
+
+    /* Sometimes we get a PART when the user has been KICKed.
+     * In this case, we get no usermode, and should not try to free it.
+     */
+    if (!mNode)
+        return;
+
+    /* remove modeNode from channel and user */
+    modeList_remove(&channel->members, mNode);
+    modeList_remove(&user->channels, mNode);
+    free(mNode);
+
+    for (n=0; n<pf_used; n++)
+        pf_list[n](user, channel, reason);
+
+    if (!deleting && !channel->members.used && !channel->locks)
+        DelChannel(channel);
+}
+
+void
+KickChannelUser(struct userNode* target, struct chanNode* channel, struct userNode *kicker, const char *why)
+{
+    if (!target || !channel || IsService(target) || !GetUserMode(channel, target))
+        return;
+    /* don't remove them from the channel, since the server will send a PART */
+    irc_kick(kicker, target, channel, why);
+
+    if (IsLocal(target))
+    {
+       /* NULL reason because we don't want a PART message to be
+          sent by DelChannelUser. */
+       DelChannelUser(target, channel, NULL, 0);
+    }
+}
+
+static kick_func_t *kf_list;
+static unsigned int kf_size = 0, kf_used = 0;
+
+void
+reg_kick_func(kick_func_t handler)
+{
+    if (kf_used == kf_size) {
+       if (kf_size) {
+           kf_size <<= 1;
+           kf_list = realloc(kf_list, kf_size*sizeof(kick_func_t));
+       } else {
+           kf_size = 8;
+           kf_list = malloc(kf_size*sizeof(kick_func_t));
+       }
+    }
+    kf_list[kf_used++] = handler;
+}
+
+void
+ChannelUserKicked(struct userNode* kicker, struct userNode* victim, struct chanNode* channel)
+{
+    unsigned int n;
+    struct modeNode *mn;
+
+    if (!victim || !channel || IsService(victim) || !GetUserMode(channel, victim))
+        return;
+
+    /* Update the kicker's idle time (kicker may be null if it was a server) */
+    if (kicker && (mn = GetUserMode(channel, kicker)))
+        mn->idle_since = now;
+
+    for (n=0; n<kf_used; n++)
+       kf_list[n](kicker, victim, channel);
+
+    DelChannelUser(victim, channel, 0, 0);
+
+    if (IsLocal(victim))
+       irc_part(victim, channel, NULL);
+}
+
+int ChannelBanExists(struct chanNode *channel, const char *ban)
+{
+    unsigned int n;
+
+    for (n = 0; n < channel->banlist.used; n++)
+       if (match_ircglobs(channel->banlist.list[n]->ban, ban))
+           return 1;
+    return 0;
+}
+
+static topic_func_t *tf_list;
+static unsigned int tf_size = 0, tf_used = 0;
+
+void
+reg_topic_func(topic_func_t handler)
+{
+    if (tf_used == tf_size) {
+       if (tf_size) {
+           tf_size <<= 1;
+           tf_list = realloc(tf_list, tf_size*sizeof(topic_func_t));
+       } else {
+           tf_size = 8;
+           tf_list = malloc(tf_size*sizeof(topic_func_t));
+       }
+    }
+    tf_list[tf_used++] = handler;
+}
+
+void
+SetChannelTopic(struct chanNode *channel, struct userNode *user, const char *topic, int announce)
+{
+    unsigned int n;
+    struct modeNode *mn;
+    char old_topic[TOPICLEN+1];
+
+    safestrncpy(old_topic, channel->topic, sizeof(old_topic));
+    safestrncpy(channel->topic, topic, sizeof(channel->topic));
+    channel->topic_time = now;
+
+    if (user) {
+        safestrncpy(channel->topic_nick, user->nick, sizeof(channel->topic_nick));
+
+        /* Update the setter's idle time */
+        if ((mn = GetUserMode(channel, user)))
+            mn->idle_since = now;
+    }
+
+    if (announce) {
+       /* We don't really care if a local user messes with the topic,
+         * so don't call the tf_list functions. */
+       irc_topic(user, channel, topic);
+    } else {
+       for (n=0; n<tf_used; n++)
+           if (tf_list[n](user, channel, old_topic))
+                break;
+    }
+}
+
+struct chanNode *
+GetChannel(const char *name)
+{
+    return dict_find(channels, name, NULL);
+}
+
+struct modeNode *
+GetUserMode(struct chanNode *channel, struct userNode *user)
+{
+    unsigned int n;
+    struct modeNode *mn = NULL;
+    if (channel->members.used < user->channels.used) {
+       for (n=0; n<channel->members.used; n++) {
+           if (user == channel->members.list[n]->user) {
+               mn = channel->members.list[n];
+               break;
+           }
+       }
+    } else {
+       for (n=0; n<user->channels.used; n++) {
+           if (channel == user->channels.list[n]->channel) {
+               mn = user->channels.list[n];
+               break;
+           }
+       }
+    }
+    return mn;
+}
+
+DEFINE_LIST(userList, struct userNode*)
+DEFINE_LIST(modeList, struct modeNode*)
+DEFINE_LIST(banList, struct banNode*)
+DEFINE_LIST(channelList, struct chanNode*)
+DEFINE_LIST(serverList, struct server*)
+
+static void
+hash_cleanup(void)
+{
+    DelServer(self, 0, NULL);
+    dict_delete(channels);
+    dict_delete(clients);
+    dict_delete(servers);
+    userList_clean(&curr_opers);
+
+    free(slf_list);
+    free(nuf_list);
+    free(ncf2_list);
+    free(duf_list);
+    free(ncf_list);
+    free(jf_list);
+    free(dcf_list);
+    free(pf_list);
+    free(kf_list);
+    free(tf_list);
+}
diff --git a/src/hash.h b/src/hash.h
new file mode 100644 (file)
index 0000000..ede9e37
--- /dev/null
@@ -0,0 +1,236 @@
+/* hash.h - IRC network state database
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef HASH_H
+#define HASH_H
+
+#include "common.h"
+#include "dict.h"
+#include "policer.h"
+
+#define MODE_CHANOP            0x0001 /* +o USER */
+#define MODE_VOICE             0x0002 /* +v USER */
+#define MODE_PRIVATE           0x0004 /* +p */
+#define MODE_SECRET            0x0008 /* +s */
+#define MODE_MODERATED         0x0010 /* +m */
+#define MODE_TOPICLIMIT                0x0020 /* +t */
+#define MODE_INVITEONLY                0x0040 /* +i */
+#define MODE_NOPRIVMSGS                0x0080 /* +n */
+#define MODE_KEY               0x0100 /* +k KEY */
+#define MODE_BAN               0x0200 /* +b BAN */
+#define MODE_LIMIT             0x0400 /* +l LIMIT */
+#define MODE_DELAYJOINS         0x0800 /* +D */
+#define MODE_REGONLY            0x1000 /* +r */
+#define MODE_NOCOLORS           0x2000 /* +c */
+#define MODE_NOCTCPS            0x4000 /* +C */
+#define MODE_REMOVE             0x80000000
+
+#define FLAGS_OPER             0x0001 /* Operator +O */
+#define FLAGS_LOCOP            0x0002 /* Local operator +o */
+#define FLAGS_INVISIBLE                0x0004 /* invisible +i */
+#define FLAGS_WALLOP           0x0008 /* receives wallops +w */
+#define FLAGS_SERVNOTICE       0x0010 /* receives server notices +s */
+#define FLAGS_DEAF             0x0020 /* deaf +d */
+#define FLAGS_SERVICE          0x0040 /* cannot be kicked, killed or deoped +k */
+#define FLAGS_GLOBAL           0x0080 /* receives global messages +g */
+#define FLAGS_HELPER           0x0100 /* (network?) helper +h */
+#define FLAGS_PERSISTENT       0x0200 /* for reserved nicks, this isn't just one-shot */
+#define FLAGS_GAGGED           0x0400 /* for gagged users */
+#define FLAGS_AWAY             0x0800 /* for away users */
+#define FLAGS_STAMPED           0x1000 /* for users who have been stamped */
+#define FLAGS_HIDDEN_HOST       0x2000 /* user's host is masked by their account */
+#define FLAGS_REGNICK           0x4000 /* user owns their current nick */
+
+#define IsOper(x)               ((x)->modes & FLAGS_OPER)
+#define IsService(x)            ((x)->modes & FLAGS_SERVICE)
+#define IsDeaf(x)               ((x)->modes & FLAGS_DEAF)
+#define IsInvisible(x)          ((x)->modes & FLAGS_INVISIBLE)
+#define IsGlobal(x)             ((x)->modes & FLAGS_GLOBAL)
+#define IsWallOp(x)             ((x)->modes & FLAGS_WALLOP)
+#define IsServNotice(x)         ((x)->modes & FLAGS_SERVNOTICE)
+#define IsHelperIrcu(x)         ((x)->modes & FLAGS_HELPER)
+#define IsGagged(x)             ((x)->modes & FLAGS_GAGGED)
+#define IsPersistent(x)         ((x)->modes & FLAGS_PERSISTENT) 
+#define IsAway(x)               ((x)->modes & FLAGS_AWAY)
+#define IsStamped(x)            ((x)->modes & FLAGS_STAMPED)
+#define IsHiddenHost(x)         ((x)->modes & FLAGS_HIDDEN_HOST)
+#define IsReggedNick(x)         ((x)->modes & FLAGS_REGNICK)
+#define IsLocal(x)              ((x)->uplink == self)
+
+#define NICKLEN         30
+#define USERLEN         10
+#define HOSTLEN         63
+#define REALLEN         50
+#define TOPICLEN        250
+#define CHANNELLEN      200
+
+#define MAXMODEPARAMS  6
+#define MAXBANS                45
+
+/* IDLEN is 6 because it takes 5.33 Base64 digits to store 32 bytes. */
+#define IDLEN           6
+
+DECLARE_LIST(userList, struct userNode*);
+DECLARE_LIST(modeList, struct modeNode*);
+DECLARE_LIST(banList, struct banNode*);
+DECLARE_LIST(channelList, struct chanNode*);
+DECLARE_LIST(serverList, struct server*);
+
+struct userNode {
+    char *nick;                   /* Unique name of the client, nick or host */
+    char ident[USERLEN + 1];      /* Per-host identification for user */
+    char info[REALLEN + 1];       /* Free form additional client information */
+    char hostname[HOSTLEN + 1];   /* DNS name or IP address */
+#ifdef WITH_PROTOCOL_P10
+    char numeric[COMBO_NUMERIC_LEN+1];
+    unsigned int num_local : 18;
+#endif
+    unsigned int dead : 1;        /* Is user waiting to be recycled? */
+    struct in_addr ip;            /* User's IP address */
+    long modes;                   /* user flags +isw etc... */
+
+    time_t timestamp;             /* Time of last nick change */
+    struct server *uplink;        /* Server that user is connected to */
+    struct modeList channels;     /* Vector of channels user is in */
+
+    /* from nickserv */
+    struct handle_info *handle_info;
+    struct userNode *next_authed;
+    struct policer auth_policer;
+};
+
+struct chanNode {
+    chan_mode_t modes;
+    unsigned int limit, locks;
+    char key[KEYLEN + 1];
+    time_t timestamp; /* creation time */
+  
+    char topic[TOPICLEN + 1];
+    char topic_nick[NICKLEN + 1];
+    time_t topic_time;
+
+    struct modeList members;
+    struct banList banlist;
+    struct policer join_policer;
+    unsigned int join_flooded : 1;
+    unsigned int bad_channel : 1;
+
+    struct chanData *channel_info;
+    struct channel_help *channel_help;
+    char name[1];
+};
+
+struct banNode {
+    char ban[NICKLEN + USERLEN + HOSTLEN + 3]; /* 1 for '\0', 1 for ! and 1 for @ = 3 */
+    char who[NICKLEN + 1]; /* who set ban */
+    time_t set; /* time ban was set */
+};
+
+struct modeNode {
+    struct chanNode *channel;
+    struct userNode *user;
+    long modes;
+    time_t idle_since;
+};
+
+#define SERVERNAMEMAX 64
+#define SERVERDESCRIPTMAX 128
+
+struct server {
+    char name[SERVERNAMEMAX+1];
+    time_t boot;
+    time_t link;
+    char description[SERVERDESCRIPTMAX+1];
+#ifdef WITH_PROTOCOL_P10
+    char numeric[COMBO_NUMERIC_LEN+1];
+    unsigned int num_mask;
+#endif
+    unsigned int hops, clients, max_clients;
+    unsigned int burst : 1, self_burst : 1;
+    struct server *uplink;
+#ifdef WITH_PROTOCOL_P10
+    struct userNode **users; /* flat indexed by numeric */
+#else
+    dict_t users; /* indexed by nick */
+#endif
+    struct serverList children;
+};
+
+extern struct server *self;
+extern dict_t channels;
+extern dict_t clients;
+extern dict_t servers;
+extern unsigned int max_clients, invis_clients;
+extern time_t max_clients_time;
+extern struct userList curr_opers, curr_helpers;
+
+struct server* GetServerH(const char *name); /* using full name */
+struct userNode* GetUserH(const char *nick);   /* using nick */
+struct chanNode* GetChannel(const char *name);
+struct modeNode* GetUserMode(struct chanNode* channel, struct userNode* user);
+
+typedef void (*server_link_func_t) (struct server *server);
+void reg_server_link_func(server_link_func_t handler);
+
+typedef int (*new_user_func_t) (struct userNode *user);
+void reg_new_user_func(new_user_func_t handler);
+typedef void (*del_user_func_t) (struct userNode *user, struct userNode *killer, const char *why);
+void reg_del_user_func(del_user_func_t handler);
+void unreg_del_user_func(del_user_func_t handler);
+void ReintroduceUser(struct userNode* user);
+typedef void (*nick_change_func_t)(struct userNode *user, const char *old_nick);
+void reg_nick_change_func(nick_change_func_t handler);
+void NickChange(struct userNode* user, const char *new_nick, int no_announce);
+
+typedef void (*account_func_t) (struct userNode *user, const char *stamp);
+void reg_account_func(account_func_t handler);
+void call_account_func(struct userNode *user, const char *stamp);
+void StampUser(struct userNode *user, const char *stamp);
+
+typedef void (*new_channel_func_t) (struct chanNode *chan);
+void reg_new_channel_func(new_channel_func_t handler);
+typedef int (*join_func_t) (struct modeNode *mNode);
+void reg_join_func(join_func_t handler);
+typedef void (*del_channel_func_t) (struct chanNode *chan);
+void reg_del_channel_func(del_channel_func_t handler);
+
+struct chanNode* AddChannel(const char *name, time_t time_, const char *modes, char *banlist);
+void LockChannel(struct chanNode *channel);
+void UnlockChannel(struct chanNode *channel);
+
+struct modeNode* AddChannelUser(struct userNode* user, struct chanNode* channel);
+
+typedef void (*part_func_t) (struct userNode *user, struct chanNode *chan, const char *reason);
+void reg_part_func(part_func_t handler);
+void unreg_part_func(part_func_t handler);
+void DelChannelUser(struct userNode* user, struct chanNode* channel, const char *reason, int deleting);
+void KickChannelUser(struct userNode* target, struct chanNode* channel, struct userNode *kicker, const char *why);
+
+typedef void (*kick_func_t) (struct userNode *kicker, struct userNode *user, struct chanNode *chan);
+void reg_kick_func(kick_func_t handler);
+void ChannelUserKicked(struct userNode* kicker, struct userNode* victim, struct chanNode* channel);
+
+int ChannelBanExists(struct chanNode *channel, const char *ban);
+
+typedef int (*topic_func_t)(struct userNode *who, struct chanNode *chan, const char *old_topic);
+void reg_topic_func(topic_func_t handler);
+void SetChannelTopic(struct chanNode *channel, struct userNode *user, const char *topic, int announce);
+
+void init_structs(void);
+
+#endif
diff --git a/src/heap.c b/src/heap.c
new file mode 100644 (file)
index 0000000..5ace5d1
--- /dev/null
@@ -0,0 +1,214 @@
+/* heap.c - Abstract heap type
+ * Copyright 2000-2002 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "common.h"
+#include "heap.h"
+
+/* Possible optimizations:
+ *
+ * Use another type of heap (rather than binary) if our heaps are big enough.
+ *
+ * Coalesce multiple entries with the same key into the same chunk, and have
+ * a new API function to return all of the entries at the top of the heap.
+ */
+
+struct heap {
+    comparator_f comparator;
+    void **data;
+    unsigned int data_used, data_alloc;
+};
+
+/*
+ *  Allocate a new heap.
+ */
+heap_t
+heap_new(comparator_f comparator)
+{
+    heap_t heap = malloc(sizeof(struct heap));
+    heap->comparator = comparator;
+    heap->data_used = 0;
+    heap->data_alloc = 8;
+    heap->data = malloc(2*heap->data_alloc*sizeof(void*));
+    return heap;
+}
+
+/*
+ *  Move the element at "index" in the heap as far up the heap as is
+ *  proper (i.e., as long as its parent node is less than or equal to
+ *  its value).
+ */
+static void
+heap_heapify_up(heap_t heap, unsigned int index)
+{
+    int res;
+    unsigned int parent;
+    void *last_key, *last_data;
+    last_key = heap->data[index*2];
+    last_data = heap->data[index*2+1];
+    while (index > 0) {
+       parent = (index - 1) >> 1;
+       res = heap->comparator(last_key, heap->data[parent*2]);
+       if (res > 0) break;
+       heap->data[index*2] = heap->data[parent*2];
+       heap->data[index*2+1] = heap->data[parent*2+1];
+       index = parent;
+    }
+    heap->data[index*2] = last_key;
+    heap->data[index*2+1] = last_data;
+}
+
+/*
+ *  Insert a key/data pair into the heap.
+ */
+void
+heap_insert(heap_t heap, void *key, void *data)
+{
+    if (heap->data_used == heap->data_alloc) {
+       heap->data_alloc *= 2;
+       heap->data = realloc(heap->data, 2*heap->data_alloc*sizeof(void*));
+    }
+    heap->data[heap->data_used*2] = key;
+    heap->data[heap->data_used*2+1] = data;
+    heap_heapify_up(heap, heap->data_used++);
+}
+
+/*
+ *  Return what's on top of the heap.
+ *  If the heap is empty, put NULL into *key and *data.
+ *  (Either key or data may be NULL, in which case the relevant
+ *  data will not be returned to the caller.)
+ */
+void
+heap_peek(heap_t heap, void **key, void **data)
+{
+    if (key) *key = heap->data_used ? heap->data[0] : NULL;
+    if (data) *data = heap->data_used ? heap->data[1] : NULL;
+}
+
+/*
+ * Push the element at "pos" down the heap as far as it will go.
+ */
+static void
+heap_heapify_down(heap_t heap, int pos)
+{
+    int res;
+    unsigned int child;
+    void *last_key, *last_data;
+    last_key = heap->data[pos*2];
+    last_data = heap->data[pos*2+1];
+    /* start at left child */
+    while ((child=pos*2+1) < heap->data_used) {
+       /* use right child if it exists and is smaller */
+       if (child+1 < heap->data_used) {
+           res = heap->comparator(heap->data[(child+1)*2], heap->data[child*2]);
+           if (res < 0) child = child+1;
+       }
+       res = heap->comparator(last_key, heap->data[child*2]);
+       if (res <= 0) break;
+       heap->data[pos*2] = heap->data[child*2];
+       heap->data[pos*2+1] = heap->data[child*2+1];
+       pos = child;
+    }
+    heap->data[pos*2] = last_key;
+    heap->data[pos*2+1] = last_data;
+}
+
+/*
+ * Remove the element at "index" from the heap (preserving the heap ordering).
+ */
+static void
+heap_remove(heap_t heap, unsigned int index)
+{
+    /* sanity check */
+    if (heap->data_used <= index) return;
+    /* swap index with last element */
+    heap->data_used--;
+    heap->data[index*2] = heap->data[heap->data_used*2];
+    heap->data[index*2+1] = heap->data[heap->data_used*2+1];
+    /* heapify down if index has children */
+    if (heap->data_used >= 2*index+1) heap_heapify_down(heap, index);
+    if ((index > 0) && (index < heap->data_used)) heap_heapify_up(heap, index);
+}
+
+/*
+ *  Pop the topmost element from the heap (preserving the heap ordering).
+ */
+void
+heap_pop(heap_t heap)
+{
+    heap_remove(heap, 0);
+}
+
+/*
+ *  Remove all elements from the heap if pred(key, data, extra) returns
+ *  non-zero on the element's key/data pair.  Can be abused to iterate
+ *  over the entire heap, by always returning 0 from pred.
+ *
+ *  Returns non-zero if the predicate causes the top of the heap to be
+ *  removed.
+ */
+int
+heap_remove_pred(heap_t heap, int (*pred)(void *key, void *data, void *extra), void *extra)
+{
+    unsigned int pos, rem_first;
+
+    if (heap->data_used == 0) return 0;
+    if (pred(heap->data[0], heap->data[1], extra)) {
+        heap_remove(heap, 0);
+        rem_first = 1;
+        pos = 0;
+    } else {
+        rem_first = 0;
+        pos = 1;
+    }
+    while (pos < heap->data_used) {
+       if (pred(heap->data[pos*2], heap->data[pos*2+1], extra)) {
+            heap_remove(heap, pos);
+            pos = 0;
+        } else {
+            pos++;
+        }
+    }
+    return rem_first;
+}
+
+/*
+ *  Remove all entries from a heap.
+ */
+void
+heap_delete(heap_t heap)
+{
+    free(heap->data);
+    free(heap);
+}
+
+/*
+ *  Return number of entries in the heap.
+ */
+unsigned int
+heap_size(heap_t heap)
+{
+    return heap->data_used;
+}
+
+/* prepackaged comparators */
+int
+int_comparator(const void *a, const void *b)
+{
+    return (time_t)a-(time_t)b;
+}
diff --git a/src/heap.h b/src/heap.h
new file mode 100644 (file)
index 0000000..68b8fc9
--- /dev/null
@@ -0,0 +1,42 @@
+/* heap.h - Abstract heap type
+ * Copyright 2000-2001 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef HEAP_H
+#define HEAP_H
+
+typedef int (*comparator_f)(const void *a, const void *b);
+
+/* a heap is implemented using a dynamically sized array */
+typedef struct heap *heap_t;
+
+/* operations on a heap */
+heap_t heap_new(comparator_f comp);
+void heap_insert(heap_t heap, void *key, void *data);
+void heap_peek(heap_t heap, void **key, void **data);
+void heap_pop(heap_t heap);
+void heap_delete(heap_t heap);
+unsigned int heap_size(heap_t heap);
+int heap_remove_pred(heap_t heap, int (*pred)(void *key, void *data, void *extra), void *extra);
+
+/* useful comparators */
+
+/* int strcmp(const char *s1, const char *s2); from <string.h> can be used */
+int int_comparator(const void*, const void*);
+int timeval_comparator(const void*, const void*);
+
+#endif /* ndef HEAP_H */
diff --git a/src/helpfile.c b/src/helpfile.c
new file mode 100644 (file)
index 0000000..71dcd91
--- /dev/null
@@ -0,0 +1,776 @@
+/* helpfile.c - Help file loading and display
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "helpfile.h"
+#include "log.h"
+#include "nickserv.h"
+#include "recdb.h"
+
+#include <dirent.h>
+
+static const struct message_entry msgtab[] = {
+    { "HFMSG_MISSING_HELPFILE", "The help file could not be found.  Sorry!" },
+    { "HFMSG_HELP_NOT_STRING", "Help file error (help data was not a string)." },
+    { NULL, NULL }
+};
+
+#define DEFAULT_LINE_SIZE      MAX_LINE_SIZE
+#define DEFAULT_TABLE_SIZE      80
+
+extern struct userNode *global, *chanserv, *opserv, *nickserv;
+struct userNode *message_dest;
+struct userNode *message_source;
+struct language *lang_C;
+struct dict *languages;
+
+static void language_cleanup(void)
+{
+    dict_delete(languages);
+}
+
+static void language_free_helpfile(void *data)
+{
+    struct helpfile *hf = data;
+    close_helpfile(hf);
+}
+
+static void language_free(void *data)
+{
+    struct language *lang = data;
+    dict_delete(lang->messages);
+    dict_delete(lang->helpfiles);
+    free(lang->name);
+    free(lang);
+}
+
+static struct language *language_alloc(const char *name)
+{
+    struct language *lang = calloc(1, sizeof(*lang));
+    lang->name = strdup(name);
+    if (!languages) {
+        languages = dict_new();
+        dict_set_free_data(languages, language_free);
+    }
+    dict_insert(languages, lang->name, lang);
+    return lang;
+}
+
+/* Language names should use a lang or lang_COUNTRY type system, where
+ * lang is a two-letter code according to ISO-639-1 (or three-letter
+ * code according to ISO-639-2 for languages not in ISO-639-1), and
+ * COUNTRY is the ISO 3166 country code in all upper case.
+ * 
+ * See also:
+ * http://www.loc.gov/standards/iso639-2/
+ * http://www.loc.gov/standards/iso639-2/langhome.html
+ * http://www.iso.ch/iso/en/prods-services/iso3166ma/index.html
+ */
+struct language *language_find(const char *name)
+{
+    struct language *lang;
+    char alt_name[MAXLEN];
+    const char *uscore;
+
+    if ((lang = dict_find(languages, name, NULL)))
+        return lang;
+    if ((uscore = strchr(name, '_'))) {
+        strncpy(alt_name, name, uscore-name);
+        alt_name[uscore-name] = 0;
+        if ((lang = dict_find(languages, alt_name, NULL)))
+            return lang;
+    }
+    if (!lang_C) {
+        lang_C = language_alloc("C");
+        lang_C->messages = dict_new();
+        lang_C->helpfiles = dict_new();
+    }
+    return lang_C;
+}
+
+static void language_set_messages(struct language *lang, dict_t dict)
+{
+    dict_iterator_t it, it2;
+    struct record_data *rd;
+    const char *msgid;
+    char *msg;
+    int diff, extra, missing;
+
+    extra = missing = 0;
+    for (it = dict_first(dict), it2 = dict_first(lang_C->messages); it || it2; ) {
+        msgid = iter_key(it);
+        if (it && it2)
+            diff = irccasecmp(msgid, iter_key(it2));
+        else if (it)
+            diff = -1;
+        else
+            diff = 1;
+        if (diff < 0) {
+            extra++;
+            it = iter_next(it);
+            continue;
+        } else if (diff > 0) {
+            missing++;
+            it2 = iter_next(it2);
+            continue;
+        }
+        msgid = iter_key(it);
+        rd = iter_data(it);
+        switch (rd->type) {
+        case RECDB_QSTRING:
+            msg = strdup(rd->d.qstring);
+            break;
+        case RECDB_STRING_LIST:
+            /* XXX: maybe do an unlistify_help() type thing */
+        default:
+            log_module(MAIN_LOG, LOG_WARNING, "Unsupported record type for message %s in language %s", msgid, lang->name);
+            continue;
+        }
+        dict_insert(lang->messages, iter_key(it2), msg);
+        it = iter_next(it);
+        it2 = iter_next(it2);
+    }
+    log_module(MAIN_LOG, LOG_WARNING, "In language %s, %d extra and %d missing messages", lang->name, extra, missing);
+}
+
+static struct language *language_read(const char *name)
+{
+    DIR *dir;
+    struct dirent *dirent;
+    struct language *lang;
+    struct helpfile *hf;
+    char filename[MAXLEN], *uscore;
+    FILE *file;
+    dict_t dict;
+
+    /* Never try to read the C language from disk. */
+    if (!irccasecmp(name, "C"))
+        return lang_C;
+
+    /* Open the directory stream; if we can't, fail. */
+    snprintf(filename, sizeof(filename), "languages/%s", name);
+    if (!(dir = opendir(filename))) {
+        
+        return NULL;
+    }
+    if (!(lang = dict_find(languages, name, NULL)))
+        lang = language_alloc(name);
+
+    /* Find the parent language. */
+    snprintf(filename, sizeof(filename), "languages/%s/parent", name);
+    if (!(file = fopen(filename, "r"))
+        || !fgets(filename, sizeof(filename), file)) {
+        strcpy(filename, "C");
+    }
+    if (!(lang->parent = language_find(filename))) {
+        uscore = strchr(filename, '_');
+        if (uscore) {
+            *uscore = 0;
+            lang->parent = language_find(filename);
+        }
+        if (!lang->parent)
+            lang->parent = lang_C;
+    }
+
+    /* (Re-)initialize the language's dicts. */
+    dict_delete(lang->messages);
+    lang->messages = dict_new();
+    dict_set_free_data(lang->messages, free);
+    lang->helpfiles = dict_new();
+    dict_set_free_data(lang->helpfiles, language_free_helpfile);
+
+    /* Read all the translations from the directory. */
+    while ((dirent = readdir(dir))) {
+        snprintf(filename, sizeof(filename), "languages/%s/%s", name, dirent->d_name);
+        if (!strcmp(dirent->d_name,"parent")) {
+            continue;
+        } else if (!strcmp(dirent->d_name, "strings.db")) {
+            dict = parse_database(filename);
+            language_set_messages(lang, dict);
+            free_database(dict);
+        } else if ((hf = dict_find(lang_C->helpfiles, dirent->d_name, NULL))) {
+            hf = open_helpfile(filename, hf->expand);
+            dict_insert(lang->helpfiles, hf->name, hf);
+        }
+    }
+
+    /* All done. */
+    closedir(dir);
+    return lang;
+}
+
+static void language_read_all(void)
+{
+    struct string_list *slist;
+    struct dirent *dirent;
+    DIR *dir;
+    unsigned int ii;
+
+    /* Read into an in-memory list and sort so we are likely to load
+     * parent languages before their children (de_DE sorts after de).
+     */
+    slist = alloc_string_list(4);
+    if (!(dir = opendir("languages")))
+        return;
+    while ((dirent = readdir(dir)))
+        string_list_append(slist, strdup(dirent->d_name));
+    closedir(dir);
+    string_list_sort(slist);
+    for (ii = 0; ii < slist->used; ++ii) {
+        if (!strcmp(slist->list[ii], ".") || !strcmp(slist->list[ii], ".."))
+            continue;
+        language_read(slist->list[ii]);
+    }
+    free_string_list(slist);
+}
+
+const char *language_find_message(struct language *lang, const char *msgid) {
+    struct language *curr;
+    const char *msg;
+    if (!lang)
+        lang = lang_C;
+    for (curr = lang; curr; curr = curr->parent)
+        if ((msg = dict_find(curr->messages, msgid, NULL)))
+            return msg;
+    log_module(MAIN_LOG, LOG_ERROR, "Tried to find unregistered message \"%s\" (original language %s)", msgid, lang->name);
+    return NULL;
+}
+
+void
+table_send(struct userNode *from, const char *to, unsigned int size, irc_send_func irc_send, struct helpfile_table table) {
+    unsigned int ii, jj, len, nreps, reps, tot_width, pos, spaces, *max_width;
+    char line[MAX_LINE_SIZE+1];
+    struct handle_info *hi;
+
+    if (IsChannelName(to) || *to == '$') {
+        message_dest = NULL;
+        hi = NULL;
+    } else {
+        message_dest = GetUserH(to);
+        if (!message_dest) {
+            log_module(MAIN_LOG, LOG_ERROR, "Unable to find user with nickname %s (in table_send from %s).", to, from->nick);
+            return;
+        }
+        hi = message_dest->handle_info;
+#ifdef WITH_PROTOCOL_P10
+        to = message_dest->numeric;
+#endif
+    }
+    message_source = from;
+
+    /* If size or irc_send are 0, we should try to use a default. */
+    if (size)
+        {} /* keep size */
+    else if (!hi)
+        size = DEFAULT_TABLE_SIZE;
+    else if (hi->table_width)
+        size = hi->table_width;
+    else if (hi->screen_width)
+        size = hi->screen_width;
+    else
+        size = DEFAULT_TABLE_SIZE;
+
+    if (irc_send)
+        {} /* use that function */
+    else if (hi)
+        irc_send = HANDLE_FLAGGED(hi, USE_PRIVMSG) ? irc_privmsg : irc_notice;
+    else
+        irc_send = IsChannelName(to) ? irc_privmsg : irc_notice;
+
+    /* Limit size to how much we can show at once */
+    if (size > sizeof(line))
+        size = sizeof(line);
+
+    /* Figure out how wide columns should be */
+    max_width = alloca(table.width * sizeof(int));
+    for (jj=tot_width=0; jj<table.width; jj++) {
+        /* Find the widest width for this column */
+        max_width[jj] = 0;
+        for (ii=0; ii<table.length; ii++) {
+            len = strlen(table.contents[ii][jj]);
+            if (len > max_width[jj])
+                max_width[jj] = len;
+        }
+        /* Separate columns with spaces */
+        tot_width += max_width[jj] + 1;
+    }
+    /* How many rows to put in a line? */
+    if ((table.flags & TABLE_REPEAT_ROWS) && (size > tot_width))
+        nreps = size / tot_width;
+    else
+        nreps = 1;
+    /* Send headers line.. */
+    if (table.flags & TABLE_NO_HEADERS) {
+        ii = 0;
+    } else {
+        /* Sending headers needs special treatment: either show them
+         * once, or repeat them as many times as we repeat the columns
+         * in a row. */
+        for (pos=ii=0; ii<((table.flags & TABLE_REPEAT_HEADERS)?nreps:1); ii++) {
+            for (jj=0; 1; ) {
+                len = strlen(table.contents[0][jj]);
+                spaces = max_width[jj] - len;
+                if (table.flags & TABLE_PAD_LEFT)
+                    while (spaces--)
+                        line[pos++] = ' ';
+                memcpy(line+pos, table.contents[0][jj], len);
+                pos += len;
+                if (++jj == table.width)
+                    break;
+                if (!(table.flags & TABLE_PAD_LEFT))
+                    while (spaces--)
+                        line[pos++] = ' ';
+                line[pos++] = ' ';
+            }
+        }
+        line[pos] = 0;
+        irc_send(from, to, line);
+        ii = 1;
+    }
+    /* Send the table. */
+    for (jj=0, pos=0, reps=0; ii<table.length; ) {
+        while (1) {
+            len = strlen(table.contents[ii][jj]);
+            spaces = max_width[jj] - len;
+            if (table.flags & TABLE_PAD_LEFT)
+                while (spaces--) line[pos++] = ' ';
+            memcpy(line+pos, table.contents[ii][jj], len);
+            pos += len;
+            if (++jj == table.width) {
+                jj = 0, ++ii, ++reps;
+                if ((reps == nreps) || (ii == table.length)) {
+                    line[pos] = 0;
+                    irc_send(from, to, line);
+                    pos = reps = 0;
+                    break;
+                }
+            }
+            if (!(table.flags & TABLE_PAD_LEFT))
+                while (spaces--)
+                    line[pos++] = ' ';
+            line[pos++] = ' ';
+        }
+    }
+    if (!(table.flags & TABLE_NO_FREE)) {
+        /* Deallocate table memory (but not the string memory). */
+        for (ii=0; ii<table.length; ii++)
+            free(table.contents[ii]);
+        free(table.contents);
+    }
+}
+
+static int
+vsend_message(const char *dest, struct userNode *src, struct handle_info *handle, int msg_type, expand_func_t expand_f, const char *format, va_list al)
+{
+    void (*irc_send)(struct userNode *from, const char *to, const char *msg);
+    static struct string_buffer input;
+    unsigned int size, ipos, pos, length, chars_sent, use_color;
+    unsigned int expand_pos, expand_ipos, newline_ipos;
+    char line[MAX_LINE_SIZE];
+
+    if (IsChannelName(dest) || *dest == '$') {
+        message_dest = NULL;
+    } else if (!(message_dest = GetUserH(dest))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find user with nickname %s (in vsend_message from %s).", dest, src->nick);
+        return 0;
+    } else if (message_dest->dead) {
+        /* No point in sending to a user who is leaving. */
+        return 0;
+    } else {
+#ifdef WITH_PROTOCOL_P10
+        dest = message_dest->numeric;
+#endif
+    }
+    message_source = src;
+    if (!(msg_type & 4) && !(format = handle_find_message(handle, format)))
+        return 0;
+    /* fill in a buffer with the string */
+    input.used = 0;
+    string_buffer_append_vprintf(&input, format, al);
+
+    /* figure out how to send the messages */
+    if (handle) {
+       msg_type |= (HANDLE_FLAGGED(handle, USE_PRIVMSG) ? 1 : 0);
+       use_color = HANDLE_FLAGGED(handle, MIRC_COLOR);
+        size = handle->screen_width;
+        if (size > sizeof(line))
+            size = sizeof(line);
+    } else {
+        size = sizeof(line);
+        use_color = 1;
+    }
+    if (!size)
+        size = DEFAULT_LINE_SIZE;
+    switch (msg_type & 3) {
+        case 0:
+            irc_send = irc_notice;
+            break;
+        case 2:
+            irc_send = irc_wallchops;
+            break;
+        case 1:
+        default:
+            irc_send = irc_privmsg;
+    }
+
+    /* This used to be two passes, but if you do that and allow
+     * arbitrary sizes for ${}-expansions (as with help indexes),
+     * that requires a very big intermediate buffer.
+     */
+    expand_ipos = newline_ipos = ipos = 0;
+    expand_pos = pos = 0;
+    chars_sent = 0;
+    while (input.list[ipos]) {
+       char ch, *value, *free_value;
+
+        while ((ch = input.list[ipos]) && (ch != '$') && (ch != '\n') && (pos < size)) {
+           line[pos++] = ch;
+            ipos++;
+       }
+
+       if (!input.list[ipos])
+            goto send_line;
+        if (input.list[ipos] == '\n') {
+            ipos++;
+            goto send_line;
+        }
+       if (pos == size) {
+            unsigned int new_ipos;
+            /* Scan backwards for a space in the input, until we hit
+             * either the last newline or the last variable expansion.
+             * Print the line up to that point, and start from there.
+             */
+            for (new_ipos = ipos;
+                 (new_ipos > expand_ipos) && (new_ipos > newline_ipos);
+                 --new_ipos)
+                if (input.list[new_ipos] == ' ')
+                    break;
+            pos -= ipos - new_ipos;
+            if (new_ipos == newline_ipos) {
+                /* Single word was too big to fit on one line; skip
+                 * forward to its end and print it as a whole.
+                 */
+                while (input.list[new_ipos]
+                       && (input.list[new_ipos] != ' ')
+                       && (input.list[new_ipos] != '\n')
+                       && (input.list[new_ipos] != '$'))
+                    line[pos++] = input.list[new_ipos++];
+            }
+            ipos = new_ipos;
+            while (input.list[ipos] == ' ')
+                ipos++;
+           goto send_line;
+       }
+
+        free_value = 0;
+       switch (input.list[++ipos]) {
+        /* Literal '$' or end of string. */
+       case 0:
+           ipos--;
+       case '$':
+           value = "$";
+           break;
+       /* The following two expand to mIRC color codes if enabled
+          by the user. */
+       case 'b':
+           value = use_color ? "\002" : "";
+           break;
+       case 'o':
+           value = use_color ? "\017" : "";
+           break;
+        case 'r':
+            value = use_color ? "\026" : "";
+            break;
+       case 'u':
+           value = use_color ? "\037" : "";
+           break;
+       /* Service nicks. */
+        case 'S':
+            value = src->nick;
+            break;
+       case 'G':
+           value = global ? global->nick : "Global";
+           break;
+       case 'C':
+           value = chanserv ? chanserv->nick : "ChanServ";
+           break;
+       case 'O':
+           value = opserv ? opserv->nick : "OpServ";
+           break;
+       case 'N':
+            value = nickserv ? nickserv->nick : "NickServ";
+            break;
+        case 's':
+            value = self->name;
+            break;
+       case 'H':
+           value = handle ? handle->handle : "Account";
+           break;
+#define SEND_LINE() do { line[pos] = 0; if (pos > 0) irc_send(src, dest, line); chars_sent += pos; pos = 0; newline_ipos = ipos; } while (0)
+       /* Custom expansion handled by helpfile-specific function. */
+       case '{':
+       case '(':
+           if (expand_f) {
+               char *name_end = input.list + ipos + 1;
+
+               while (*name_end != '}' && *name_end != ')' && *name_end) name_end++;
+               if (*name_end) {
+                    struct helpfile_expansion exp;
+                   *name_end = 0;
+                   exp = expand_f(input.list + ipos + 1);
+                    switch (exp.type) {
+                    case HF_STRING:
+                        free_value = value = exp.value.str;
+                        if (!value) value = "";
+                        break;
+                    case HF_TABLE:
+                        /* Must send current line, then emit table. */
+                        SEND_LINE();
+                        table_send(src, (message_dest ? message_dest->nick : dest), 0, irc_send, exp.value.table);
+                        value = "";
+                        break;
+                    default:
+                        value = "";
+                        log_module(MAIN_LOG, LOG_ERROR, "Invalid exp.type %d from expansion function %p.", exp.type, expand_f);
+                        break;
+                    }
+                   ipos = name_end - input.list;
+                   break;
+               }
+           }
+
+       /* Let it fall through when there's no expansion function or
+       terminating ')'. */
+       default:
+               value = alloca(3);
+               value[0] = '$';
+               value[1] = input.list[ipos];
+               value[2] = 0;
+       }
+       ipos++;
+        while ((pos + strlen(value) > size) || strchr(value, '\n')) {
+            unsigned int avail;
+            avail = size - pos - 1;
+            length = strcspn(value, "\n ");
+            if (length <= avail) {
+                strncpy(line+pos, value, length);
+                pos += length;
+                value += length;
+                /* copy over spaces, until (possible) end of line */
+                while (*value == ' ') {
+                    if (pos < size-1)
+                        line[pos++] = *value;
+                    value++;
+                }
+            } else {
+                /* word to send is too big to send now.. what to do? */
+                if (pos > 0) {
+                    /* try to put it on a separate line */
+                    SEND_LINE();
+                } else {
+                    /* already at start of line; only send part of it */
+                    strncpy(line, value, avail);
+                    pos += avail;
+                    value += length;
+                    /* skip any trailing spaces */
+                    while (*value == ' ')
+                        value++;
+                }
+            }
+            /* if we're looking at a newline, send the accumulated text */
+            if (*value == '\n') {
+                SEND_LINE();
+                value++;
+            }
+        }
+        length = strlen(value);
+       memcpy(line + pos, value, length);
+        if (free_value)
+            free(free_value);
+       pos += length;
+        if ((pos < size-1) && input.list[ipos]) {
+            expand_pos = pos;
+            expand_ipos = ipos;
+            continue;
+        }
+      send_line:
+        expand_pos = pos;
+        expand_ipos = ipos;
+        SEND_LINE();
+#undef SEND_LINE
+    }
+    return chars_sent;
+}
+
+int
+send_message(struct userNode *dest, struct userNode *src, const char *format, ...)
+{
+    int res;
+    va_list ap;
+
+    if (IsLocal(dest)) return 0;
+    va_start(ap, format);
+    res = vsend_message(dest->nick, src, dest->handle_info, 0, NULL, format, ap);
+    va_end(ap);
+    return res;
+}
+
+int
+send_message_type(int msg_type, struct userNode *dest, struct userNode *src, const char *format, ...) {
+    int res;
+    va_list ap;
+
+    if (IsLocal(dest)) return 0;
+    va_start(ap, format);
+    res = vsend_message(dest->nick, src, dest->handle_info, msg_type, NULL, format, ap);
+    va_end(ap);
+    return res;
+}
+
+int
+send_target_message(int msg_type, const char *dest, struct userNode *src, const char *format, ...)
+{
+    int res;
+    va_list ap;
+
+    va_start(ap, format);
+    res = vsend_message(dest, src, NULL, msg_type, NULL, format, ap);
+    va_end(ap);
+    return res;
+}
+
+int
+_send_help(struct userNode *dest, struct userNode *src, expand_func_t expand, const char *format, ...)
+{
+    int res;
+
+    va_list ap;
+    va_start(ap, format);
+    res = vsend_message(dest->nick, src, dest->handle_info, 4, expand, format, ap);
+    va_end(ap);
+    return res;
+}
+
+int
+send_help(struct userNode *dest, struct userNode *src, struct helpfile *hf, const char *topic)
+{
+    struct helpfile *lang_hf;
+    struct record_data *rec;
+    struct language *curr;
+
+    if (!topic)
+        topic = "<index>";
+    if (!hf) {
+        _send_help(dest, src, NULL, "HFMSG_MISSING_HELPFILE");
+        return 0;
+    }
+    for (curr = (dest->handle_info ? dest->handle_info->language : lang_C);
+         curr;
+         curr = curr->parent) {
+        lang_hf = dict_find(curr->helpfiles, hf->name, NULL);
+        if (!lang_hf)
+            continue;
+        rec = dict_find(lang_hf->db, topic, NULL);
+        if (rec && rec->type == RECDB_QSTRING)
+            return _send_help(dest, src, hf->expand, rec->d.qstring);
+    }
+    rec = dict_find(hf->db, "<missing>", NULL);
+    if (!rec)
+        return send_message(dest, src, "MSG_TOPIC_UNKNOWN");
+    if (rec->type != RECDB_QSTRING)
+       return send_message(dest, src, "HFMSG_HELP_NOT_STRING");
+    return _send_help(dest, src, hf->expand, rec->d.qstring);
+}
+
+int
+unlistify_help(const char *key, void *data, void *extra)
+{
+    struct record_data *rd = data;
+    dict_t newdb = extra;
+    key = strdup(key);
+    if (rd->type == RECDB_QSTRING) {
+       dict_insert(newdb, key, alloc_record_data_qstring(GET_RECORD_QSTRING(rd)));
+       return 0;
+    } else if (rd->type == RECDB_STRING_LIST) {
+       struct string_list *slist = GET_RECORD_STRING_LIST(rd);
+       char *dest;
+       unsigned int totlen, len, i;
+       for (i=totlen=0; i<slist->used; i++) {
+           totlen = totlen + strlen(slist->list[i]) + 1;
+       }
+       dest = alloca(totlen+1);
+       for (i=totlen=0; i<slist->used; i++) {
+           len = strlen(slist->list[i]);
+           memcpy(dest+totlen, slist->list[i], len);
+           dest[totlen+len] = '\n';
+           totlen = totlen + len + 1;
+       }
+       dest[totlen] = 0;
+       dict_insert(newdb, key, alloc_record_data_qstring(dest));
+       return 0;
+    } else {
+       return 1;
+    }
+}
+
+struct helpfile *
+open_helpfile(const char *fname, expand_func_t expand)
+{
+    struct helpfile *hf;
+    char *slash;
+    dict_t db = parse_database(fname);
+    hf = calloc(1, sizeof(*hf));
+    hf->expand = expand;
+    hf->db = alloc_database();
+    dict_set_free_keys(hf->db, free);
+    if ((slash = strrchr(fname, '/'))) {
+        hf->name = strdup(slash + 1);
+    } else {
+        hf->name = strdup(fname);
+        dict_insert(language_find("C")->helpfiles, hf->name, hf);
+    }
+    if (db) {
+       dict_foreach(db, unlistify_help, hf->db);
+       free_database(db);
+    }
+    return hf;
+}
+
+void close_helpfile(struct helpfile *hf)
+{
+    if (!hf) return;
+    free((char*)hf->name);
+    free_database(hf->db);
+    free(hf);
+}
+
+void message_register_table(const struct message_entry *table)
+{
+    if (!lang_C)
+        language_find("C");
+    while (table->msgid) {
+        dict_insert(lang_C->messages, table->msgid, (char*)table->format);
+        table++;
+    }
+}
+
+void helpfile_finalize(void)
+{
+    message_register_table(msgtab);
+    language_read_all();
+    reg_exit_func(language_cleanup);
+}
diff --git a/src/helpfile.h b/src/helpfile.h
new file mode 100644 (file)
index 0000000..d7336fa
--- /dev/null
@@ -0,0 +1,100 @@
+/* helpfile.h - Help file loading and display
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#if !defined(HELPFILE_H)
+#define HELPFILE_H
+
+#include "common.h"
+
+struct userNode;
+struct handle_info;
+struct string_list;
+
+extern struct userNode *message_dest; /* message destination; useful in expansion callbacks */
+
+#define MIN_LINE_SIZE          40
+#define MAX_LINE_SIZE          450
+
+#define TABLE_REPEAT_HEADERS 0x0001 /* repeat the headers for each columnset? */
+#define TABLE_PAD_LEFT       0x0002 /* pad cells on the left? */
+#define TABLE_REPEAT_ROWS    0x0004 /* put more than one row on a line? */
+#define TABLE_NO_FREE        0x0008 /* don't free the contents? */
+#define TABLE_NO_HEADERS     0x0010 /* is there actually no header? */
+
+struct helpfile_table {
+    unsigned int length : 16;
+    unsigned int width : 8;
+    unsigned int flags : 8;
+    const char ***contents;
+};
+
+struct helpfile_expansion {
+    enum { HF_STRING, HF_TABLE } type;
+    union {
+        char *str;
+        struct helpfile_table table;
+    } value;
+};
+
+typedef struct helpfile_expansion (*expand_func_t)(const char *variable);
+typedef void (*irc_send_func)(struct userNode *from, const char *to, const char *msg);
+
+struct helpfile {
+    const char *name;
+    struct dict *db;
+    expand_func_t expand;
+};
+
+struct language
+{
+    char *name;
+    struct language *parent;
+    struct dict *messages; /* const char* -> const char* */
+    struct dict *helpfiles; /* phelpfile->name -> phelpfile */
+};
+extern struct language *lang_C;
+extern struct dict *languages;
+
+int send_message(struct userNode *dest, struct userNode *src, const char *message, ...);
+int send_message_type(int msg_type, struct userNode *dest, struct userNode *src, const char *message, ...);
+int send_target_message(int msg_type, const char *dest, struct userNode *src, const char *format, ...);
+int send_help(struct userNode *dest, struct userNode *src, struct helpfile *hf, const char *topic);
+/* size is maximum line width (up to MAX_LINE_SIZE); 0 means figure it out.
+ * irc_send is either irc_privmsg or irc_notice; NULL means figure it out. */
+void table_send(struct userNode *from, const char *to, unsigned int size, irc_send_func irc_send, struct helpfile_table table);
+
+#define send_channel_message(CHANNEL, ARGS...) send_target_message(5, (CHANNEL)->name, ARGS)
+#define send_channel_notice(CHANNEL, ARGS...) send_target_message(4, (CHANNEL)->name, ARGS)
+#define send_channel_wallchops(CHANNEL, ARGS...) send_target_message(6, (CHANNEL)->name, ARGS)
+
+struct message_entry
+{
+    const char *msgid;
+    const char *format;
+};
+void message_register_table(const struct message_entry *table);
+struct language *language_find(const char *name);
+const char *language_find_message(struct language *lang, const char *msgid);
+#define handle_find_message(HANDLE, MSGID) language_find_message((HANDLE) ? (HANDLE)->language : lang_C, (MSGID))
+#define user_find_message(USER, MSGID) language_find_message((USER)->handle_info ? (USER)->handle_info->language : lang_C, (MSGID))
+void helpfile_finalize(void);
+
+struct helpfile *open_helpfile(const char *fname, expand_func_t expand);
+void close_helpfile(struct helpfile *hf);
+
+#endif
diff --git a/src/ioset.c b/src/ioset.c
new file mode 100644 (file)
index 0000000..beb4504
--- /dev/null
@@ -0,0 +1,442 @@
+/* ioset.h - srvx event loop
+ * Copyright 2002-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "ioset.h"
+#include "log.h"
+#include "timeq.h"
+#include "saxdb.h"
+#include "conf.h"
+
+#ifdef HAVE_FCNTL_H
+#include <fcntl.h>
+#endif
+#ifdef HAVE_SYS_SELECT_H
+#include <sys/select.h>
+#endif
+#ifdef HAVE_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif
+
+#ifndef IOSET_DEBUG
+#define IOSET_DEBUG 0
+#endif
+
+#define IS_EOL(CH) ((CH) == '\n')
+
+extern int uplink_connect(void);
+static int clock_skew;
+int do_write_dbs;
+int do_reopen;
+
+static struct io_fd **fds;
+static unsigned int fds_size;
+static fd_set read_fds, write_fds;
+
+static void
+ioq_init(struct ioq *ioq, int size) {
+    ioq->buf = malloc(size);
+    ioq->get = ioq->put = 0;
+    ioq->size = size;
+}
+
+static unsigned int
+ioq_put_avail(const struct ioq *ioq) {
+    /* Subtract 1 from ioq->get to be sure we don't fill the buffer
+     * and make it look empty even when there's data in it. */
+    if (ioq->put < ioq->get) {
+        return ioq->get - ioq->put - 1;
+    } else if (ioq->get == 0) {
+        return ioq->size - ioq->put - 1;
+    } else {
+        return ioq->size - ioq->put;
+    }
+}
+
+static unsigned int
+ioq_get_avail(const struct ioq *ioq) {
+    return ((ioq->put < ioq->get) ? ioq->size : ioq->put) - ioq->get;
+}
+
+static unsigned int
+ioq_used(const struct ioq *ioq) {
+    return ((ioq->put < ioq->get) ? ioq->size : 0) + ioq->put - ioq->get;
+}
+
+static unsigned int
+ioq_grow(struct ioq *ioq) {
+    int new_size = ioq->size << 1;
+    char *new_buf = malloc(new_size);
+    int get_avail = ioq_get_avail(ioq);
+    memcpy(new_buf, ioq->buf + ioq->get, get_avail);
+    if (ioq->put < ioq->get)
+        memcpy(new_buf + get_avail, ioq->buf, ioq->put);
+    free(ioq->buf);
+    ioq->put = ioq_used(ioq);
+    ioq->get = 0;
+    ioq->buf = new_buf;
+    ioq->size = new_size;
+    return new_size - ioq->put;
+}
+
+void
+ioset_cleanup(void) {
+    free(fds);
+}
+
+struct io_fd *
+ioset_add(int fd) {
+    struct io_fd *res;
+    int flags;
+
+    if (fd < 0) {
+        log_module(MAIN_LOG, LOG_ERROR, "Somebody called ioset_add(%d) on a negative fd!", fd);
+        return 0;
+    }
+    res = calloc(1, sizeof(*res));
+    if (!res)
+        return 0;
+    res->fd = fd;
+    ioq_init(&res->send, 1024);
+    ioq_init(&res->recv, 1024);
+    if ((unsigned)fd >= fds_size) {
+        unsigned int old_size = fds_size;
+        fds_size = fd + 8;
+        fds = realloc(fds, fds_size*sizeof(*fds));
+        memset(fds+old_size, 0, (fds_size-old_size)*sizeof(*fds));
+    }
+    fds[fd] = res;
+    flags = fcntl(fd, F_GETFL);
+    fcntl(fd, F_SETFL, flags|O_NONBLOCK);
+    return res;
+}
+
+struct io_fd *
+ioset_connect(struct sockaddr *local, unsigned int sa_size, const char *peer, unsigned int port, int blocking, void *data, void (*connect_cb)(struct io_fd *fd, int error)) {
+    int fd, res;
+    struct io_fd *io_fd;
+    struct sockaddr_in sin;
+    unsigned long ip;
+
+    if (!getipbyname(peer, &ip)) {
+        log_module(MAIN_LOG, LOG_ERROR, "getipbyname(%s) failed.", peer);
+        return NULL;
+    }
+    sin.sin_addr.s_addr = ip;
+    if (local) {
+        if ((fd = socket(local->sa_family, SOCK_STREAM, 0)) < 0) {
+            log_module(MAIN_LOG, LOG_ERROR, "socket() for %s returned errno %d (%s)", peer, errno, strerror(errno));
+            return NULL;
+        }
+        if (bind(fd, local, sa_size) < 0) {
+            log_module(MAIN_LOG, LOG_ERROR, "bind() of socket for %s (fd %d) returned errno %d (%s).  Will let operating system choose.", peer, fd, errno, strerror(errno));
+        }
+    } else {
+        if ((fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
+            log_module(MAIN_LOG, LOG_ERROR, "socket() for %s returned errno %d (%s).", peer, errno, strerror(errno));
+            return NULL;
+        }
+    }
+    sin.sin_family = AF_INET;
+    sin.sin_port = htons(port);
+    if (blocking) {
+        res = connect(fd, (struct sockaddr*)&sin, sizeof(sin));
+        io_fd = ioset_add(fd);
+    } else {
+        io_fd = ioset_add(fd);
+        res = connect(fd, (struct sockaddr*)&sin, sizeof(sin));
+    }
+    if (!io_fd) {
+        close(fd);
+        return NULL;
+    }
+    io_fd->data = data;
+    io_fd->connect_cb = connect_cb;
+    if (res < 0) {
+        switch (errno) {
+        case EINPROGRESS: /* only if !blocking */
+            return io_fd;
+        default:
+            log_module(MAIN_LOG, LOG_ERROR, "connect(%s:%d) (fd %d) returned errno %d (%s).", peer, port, io_fd->fd, errno, strerror(errno));
+            /* then fall through */
+        case EHOSTUNREACH:
+        case ECONNREFUSED:
+            ioset_close(io_fd->fd, 1);
+            return NULL;
+        }
+    }
+    if (connect_cb)
+        connect_cb(io_fd, ((res < 0) ? errno : 0));
+    return io_fd;
+}
+
+static void
+ioset_try_write(struct io_fd *fd) {
+    int res;
+    unsigned int req = ioq_get_avail(&fd->send);
+    res = write(fd->fd, fd->send.buf+fd->send.get, req);
+    if (res < 0) {
+        switch (errno) {
+        case EAGAIN: break;
+        default:
+            log_module(MAIN_LOG, LOG_ERROR, "write() on fd %d error %d: %s", fd->fd, errno, strerror(errno));
+        }
+    } else {
+        fd->send.get += res;
+        if (fd->send.get == fd->send.size) fd->send.get = 0;
+    }
+}
+
+void
+ioset_close(int fd, int os_close) {
+    struct io_fd *fdp;
+    if (!(fdp = fds[fd])) return;
+    fds[fd] = NULL;
+    if (fdp->destroy_cb) fdp->destroy_cb(fdp);
+    if (fdp->send.get != fdp->send.put) {
+        int flags = fcntl(fd, F_GETFL);
+        fcntl(fd, F_SETFL, flags&~O_NONBLOCK);
+        ioset_try_write(fdp);
+        /* it may need to send the beginning of the buffer now.. */
+        if (fdp->send.get != fdp->send.put) ioset_try_write(fdp);
+    }
+    free(fdp->send.buf);
+    free(fdp->recv.buf);
+    if (os_close) close(fd);
+    free(fdp);
+    FD_CLR(fd, &read_fds);
+    FD_CLR(fd, &write_fds);
+}
+
+static int
+ioset_find_line_length(struct io_fd *fd) {
+    unsigned int pos, max, len;
+    len = 0;
+    max = (fd->recv.put < fd->recv.get) ? fd->recv.size : fd->recv.put;
+    for (pos = fd->recv.get; pos < max; ++pos, ++len) {
+        if (IS_EOL(fd->recv.buf[pos])) return fd->line_len = len + 1;
+    }
+    if (fd->recv.put < fd->recv.get) {
+        for (pos = 0; pos < fd->recv.put; ++pos, ++len) {
+            if (IS_EOL(fd->recv.buf[pos])) return fd->line_len = len + 1;
+        }
+    }
+    return fd->line_len = 0;
+}
+
+static void
+ioset_buffered_read(struct io_fd *fd) {
+    int put_avail, nbr, fdnum;
+    
+    if (!(put_avail = ioq_put_avail(&fd->recv))) put_avail = ioq_grow(&fd->recv);
+    nbr = read(fd->fd, fd->recv.buf + fd->recv.put, put_avail);
+    if (nbr < 0) {
+        switch (errno) {
+        case EAGAIN: break;
+        default:
+            log_module(MAIN_LOG, LOG_ERROR, "Unexpected read() error %d on fd %d: %s", errno, fd->fd, strerror(errno));
+            /* Just flag it as EOF and call readable_cb() to notify the fd's owner. */
+            fd->eof = 1;
+            fd->wants_reads = 0;
+            fd->readable_cb(fd);
+        }
+    } else if (nbr == 0) {
+        fd->eof = 1;
+        fd->wants_reads = 0;
+        fd->readable_cb(fd);
+    } else {
+        if (fd->line_len == 0) {
+            unsigned int pos;
+            for (pos = fd->recv.put; pos < fd->recv.put + nbr; ++pos) {
+                if (IS_EOL(fd->recv.buf[pos])) {
+                    if (fd->recv.put < fd->recv.get) {
+                        fd->line_len = fd->recv.size + pos + 1 - fd->recv.get;
+                    } else {
+                        fd->line_len = pos + 1 - fd->recv.get;
+                    }
+                    break;
+                }
+            }
+        }
+        fd->recv.put += nbr;
+        if (fd->recv.put == fd->recv.size) fd->recv.put = 0;
+        fdnum = fd->fd;
+        while (fd->wants_reads && (fd->line_len > 0)) {
+            fd->readable_cb(fd);
+            if (!fds[fdnum]) break; /* make sure they didn't close on us */
+            ioset_find_line_length(fd);
+        }
+    }
+}
+
+int
+ioset_line_read(struct io_fd *fd, char *dest, int max) {
+    int avail, done;
+    if (fd->eof && (!ioq_get_avail(&fd->recv) ||  (fd->line_len < 0))) return 0;
+    if (fd->line_len < 0) return -1;
+    if (fd->line_len < max) max = fd->line_len;
+    avail = ioq_get_avail(&fd->recv);
+    if (max > avail) {
+        memcpy(dest, fd->recv.buf + fd->recv.get, avail);
+        fd->recv.get += avail;
+        assert(fd->recv.get == fd->recv.size);
+        fd->recv.get = 0;
+        done = avail;
+    } else {
+        done = 0;
+    }
+    memcpy(dest + done, fd->recv.buf + fd->recv.get, max - done);
+    fd->recv.get += max - done;
+    if (fd->recv.get == fd->recv.size) fd->recv.get = 0;
+    dest[max] = 0;
+    ioset_find_line_length(fd);
+    return max;
+}
+
+#if 1
+#define debug_fdsets(MSG, NFDS, READ_FDS, WRITE_FDS, EXCEPT_FDS, SELECT_TIMEOUT) (void)0
+#else
+static void
+debug_fdsets(const char *msg, int nfds, fd_set *read_fds, fd_set *write_fds, fd_set *except_fds, struct timeval *select_timeout) {
+    static const char *flag_text[8] = { "---", "r", "w", "rw", "e", "er", "ew", "erw" };
+    char buf[MAXLEN];
+    int pos, ii, flags;
+    struct timeval now;
+
+    for (pos=ii=0; ii<nfds; ++ii) {
+        flags  = (read_fds && FD_ISSET(ii, read_fds)) ? 1 : 0;
+        flags |= (write_fds && FD_ISSET(ii, write_fds)) ? 2 : 0;
+        flags |= (except_fds && FD_ISSET(ii, except_fds)) ? 4 : 0;
+        if (!flags) continue;
+        pos += sprintf(buf+pos, " %d%s", ii, flag_text[flags]);
+    }
+    gettimeofday(&now, NULL);
+    if (select_timeout) {
+        log_module(MAIN_LOG, LOG_DEBUG, "%s, at "FMT_TIME_T".%06ld:%s (timeout "FMT_TIME_T".%06ld)", msg, now.tv_sec, now.tv_usec, buf, select_timeout->tv_sec, select_timeout->tv_usec);
+    } else {
+        log_module(MAIN_LOG, LOG_DEBUG, "%s, at "FMT_TIME_T".%06ld:%s (no timeout)", msg, now.tv_sec, now.tv_usec, buf);
+    }
+}
+#endif
+
+void
+ioset_run(void) {
+    extern struct io_fd *socket_io_fd;
+    struct timeval select_timeout;
+    unsigned int nn;
+    int select_result, max_fd;
+    time_t wakey;
+    struct io_fd *fd;
+
+    while (!quit_services) {
+        while (!socket_io_fd) uplink_connect();
+
+        /* How long to sleep? (fill in select_timeout) */
+        wakey = timeq_next();
+        if ((wakey - now) < 0) {
+            select_timeout.tv_sec = 0;
+        } else {
+            select_timeout.tv_sec = wakey - now;
+        }
+        select_timeout.tv_usec = 0;
+
+        /* Set up read_fds and write_fds fdsets. */
+        FD_ZERO(&read_fds);
+        FD_ZERO(&write_fds);
+        max_fd = 0;
+        for (nn=0; nn<fds_size; nn++) {
+            if (!(fd = fds[nn])) continue;
+            max_fd = nn;
+            if (fd->wants_reads) FD_SET(nn, &read_fds);
+            if ((fd->send.get != fd->send.put) || !fd->connected) FD_SET(nn, &write_fds);
+        }
+
+        /* Check for activity, update time. */
+        debug_fdsets("Entering select", max_fd+1, &read_fds, &write_fds, NULL, &select_timeout);
+        select_result = select(max_fd + 1, &read_fds, &write_fds, NULL, &select_timeout);
+        debug_fdsets("After select", max_fd+1, &read_fds, &write_fds, NULL, &select_timeout);
+        now = time(NULL) + clock_skew;
+        if (select_result < 0) {
+            if (errno != EINTR) {
+                log_module(MAIN_LOG, LOG_ERROR, "select() error %d: %s", errno, strerror(errno));
+                close_socket();
+            }
+            continue;
+        }
+
+        /* Call back anybody that has connect or read activity and wants to know. */
+        for (nn=0; nn<fds_size; nn++) {
+            if (!(fd = fds[nn])) continue;
+            if (FD_ISSET(nn, &read_fds)) {
+                if (fd->line_reads) {
+                    ioset_buffered_read(fd);
+                } else {
+                    fd->readable_cb(fd);
+                }
+            }
+            if (FD_ISSET(nn, &write_fds) && !fd->connected) {
+                int rc, arglen = sizeof(rc);
+                if (getsockopt(fd->fd, SOL_SOCKET, SO_ERROR, &rc, &arglen) < 0) rc = errno;
+                fd->connected = 1;
+                if (fd->connect_cb) fd->connect_cb(fd, rc);
+            }
+            /* Note: check whether write FD is still set, since the
+             * connect_cb() might close the FD, making us dereference
+             * a free()'d pointer for the fd.
+             */
+            if (FD_ISSET(nn, &write_fds) && (fd->send.get != fd->send.put)) {
+                ioset_try_write(fd);
+            }
+        }
+
+        /* Call any timeq events we need to call. */
+        timeq_run();
+        if (do_write_dbs) {
+            saxdb_write_all();
+            do_write_dbs = 0;
+        }
+        if (do_reopen) {
+            extern char *services_config;
+            conf_read(services_config);
+            do_reopen = 0;
+        }
+    }
+}
+
+void
+ioset_write(struct io_fd *fd, const char *buf, unsigned int nbw) {
+    unsigned int avail;
+    while (ioq_used(&fd->send) + nbw >= fd->send.size) {
+        ioq_grow(&fd->send);
+    }
+    avail = ioq_put_avail(&fd->send);
+    if (nbw > avail) {
+        memcpy(fd->send.buf + fd->send.put, buf, avail);
+        buf += avail;
+        nbw -= avail;
+        fd->send.put = 0;
+    }
+    memcpy(fd->send.buf + fd->send.put, buf, nbw);
+    fd->send.put += nbw;
+    if (fd->send.put == fd->send.size) fd->send.put = 0;
+}
+
+void
+ioset_set_time(unsigned long new_now) {
+    clock_skew = new_now - time(NULL);
+    now = new_now;
+}
diff --git a/src/ioset.h b/src/ioset.h
new file mode 100644 (file)
index 0000000..b422d67
--- /dev/null
@@ -0,0 +1,58 @@
+/* ioset.h - srvx event loop
+ * Copyright 2002-2003 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#if !defined(IOSET_H)
+#define IOSET_H
+
+/* Forward declare, since ioset_connect() takes a sockaddr argument. */
+struct sockaddr;
+
+struct ioq {
+    char *buf;
+    unsigned int size, get, put;
+};
+
+struct io_fd {
+    int fd;
+    void *data;
+    unsigned int connected : 1;
+    unsigned int wants_reads : 1;
+    unsigned int line_reads : 1;
+    unsigned int eof : 1;
+    int line_len;
+    struct ioq send;
+    struct ioq recv;
+    void (*connect_cb)(struct io_fd *fd, int error);
+    void (*readable_cb)(struct io_fd *fd);
+    void (*destroy_cb)(struct io_fd *fd);
+};
+
+extern int clock_skew;
+extern int do_write_dbs;
+extern int do_reopen;
+
+struct io_fd *ioset_add(int fd);
+struct io_fd *ioset_connect(struct sockaddr *local, unsigned int sa_size, const char *hostname, unsigned int port, int blocking, void *data, void (*connect_cb)(struct io_fd *fd, int error));
+void ioset_run(void);
+void ioset_write(struct io_fd *fd, const char *buf, unsigned int nbw);
+int ioset_line_read(struct io_fd *fd, char *buf, int maxlen);
+void ioset_close(int fd, int os_close);
+void ioset_cleanup(void);
+void ioset_set_time(unsigned long new_now);
+
+#endif /* !defined(IOSET_H) */
diff --git a/src/log.c b/src/log.c
new file mode 100644 (file)
index 0000000..15d8e14
--- /dev/null
+++ b/src/log.c
@@ -0,0 +1,999 @@
+/* log.c - Diagnostic and error logging
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "conf.h"
+#include "log.h"
+#include "helpfile.h" /* send_message, message_register, etc */
+#include "nickserv.h"
+
+struct logDestination;
+
+struct logDest_vtable {
+    const char *type_name;
+    struct logDestination* (*open)(const char *args);
+    void (*reopen)(struct logDestination *self);
+    void (*close)(struct logDestination *self);
+    void (*log_audit)(struct logDestination *self, struct log_type *type, struct logEntry *entry);
+    void (*log_replay)(struct logDestination *self, struct log_type *type, int is_write, const char *line);
+    void (*log_module)(struct logDestination *self, struct log_type *type, enum log_severity sev, const char *message);
+};
+
+struct logDestination {
+    struct logDest_vtable *vtbl;
+    char *name;
+    int refcnt;
+};
+
+DECLARE_LIST(logList, struct logDestination*);
+
+struct log_type {
+    char *name;
+    struct logList logs[LOG_NUM_SEVERITIES];
+    struct logEntry *log_oldest;
+    struct logEntry *log_newest;
+    unsigned int log_count;
+    unsigned int max_age;
+    unsigned int max_count;
+    unsigned int default_set : 1;
+};
+
+static const char *log_severity_names[] = {
+    "replay",   /* 0 */
+    "debug",
+    "command",
+    "info",
+    "override",
+    "staff",    /* 5 */
+    "warning",
+    "error",
+    "fatal",
+    0
+};
+
+static struct dict *log_dest_types;
+static struct dict *log_dests;
+static struct dict *log_types;
+static struct log_type *log_default;
+static int log_inited, log_debugged;
+
+DEFINE_LIST(logList, struct logDestination*);
+static void log_format_audit(struct logEntry *entry);
+static const struct message_entry msgtab[] = {
+    { "MSG_INVALID_FACILITY", "$b%s$b is an invalid log facility." },
+    { "MSG_INVALID_SEVERITY", "$b%s$b is an invalid severity level." },
+    { NULL, NULL }
+};
+
+static struct logDestination *
+log_open(const char *name)
+{
+    struct logDest_vtable *vtbl;
+    struct logDestination *ld;
+    char *sep;
+    char type_name[32];
+    if ((ld = dict_find(log_dests, name, NULL))) {
+        ld->refcnt++;
+        return ld;
+    }
+    if ((sep = strchr(name, ':'))) {
+        memcpy(type_name, name, sep-name);
+        type_name[sep-name] = 0;
+    } else {
+        strcpy(type_name, name);
+    }
+    if (!(vtbl = dict_find(log_dest_types, type_name, NULL))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Invalid log type for log '%s'.", name);
+        return 0;
+    }
+    if (!(ld = vtbl->open(sep ? sep+1 : 0))) {
+        return 0;
+    }
+    ld->name = strdup(name);
+    dict_insert(log_dests, ld->name, ld);
+    ld->refcnt = 1;
+    return ld;
+}
+
+static void
+logList_open(struct logList *ll, struct record_data *rd)
+{
+    struct logDestination *ld;
+    unsigned int ii;
+
+    if (!ll->size) {
+        logList_init(ll);
+    }
+    switch (rd->type) {
+    case RECDB_QSTRING:
+        if ((ld = log_open(rd->d.qstring))) {
+            logList_append(ll, ld);
+        }
+        break;
+    case RECDB_STRING_LIST:
+        for (ii=0; ii<rd->d.slist->used; ++ii) {
+            if ((ld = log_open(rd->d.slist->list[ii]))) {
+                logList_append(ll, ld);
+            }
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+logList_join(struct logList *target, const struct logList *source)
+{
+    unsigned int ii, jj, kk;
+
+    if (!source->used) {
+        return;
+    }
+    jj = target->used;
+    target->used += source->used;
+    target->size += source->used;
+    target->list = realloc(target->list, target->size * sizeof(target->list[0]));
+    for (ii = 0; ii < source->used; ++ii, ++jj) {
+        int dup;
+        for (dup = 0, kk = 0; kk < jj; kk++) {
+            if (target->list[kk] == source->list[ii]) {
+                dup = 1;
+                break;
+            }
+        }
+        if (dup) {
+            jj--;
+            target->used--;
+            continue;
+        }
+        target->list[jj] = source->list[ii];
+        target->list[jj]->refcnt++;
+    }
+}
+
+static void
+logList_close(struct logList *ll)
+{
+    unsigned int ii;
+    for (ii=0; ii<ll->used; ++ii) {
+        if (!--ll->list[ii]->refcnt) {
+            struct logDestination *ld = ll->list[ii];
+            ld->vtbl->close(ld);
+        }
+    }
+    logList_clean(ll);
+}
+
+static void
+close_logs(void)
+{
+    dict_iterator_t it;
+    struct log_type *lt;
+    enum log_severity ls;
+
+    for (it = dict_first(log_types); it; it = iter_next(it)) {
+        lt = iter_data(it);
+        for (ls = 0; ls < LOG_NUM_SEVERITIES; ls++) {
+            logList_close(&lt->logs[ls]);
+
+            lt->logs[ls].size = 0;
+            lt->logs[ls].used = 0;
+            lt->logs[ls].list = 0;
+        }
+    }
+}
+
+static void
+log_type_free(void *ptr)
+{
+    struct log_type *lt = ptr;
+    struct logEntry *le, *next;
+    
+    for (le = lt->log_oldest; le; le = next) {
+        next = le->next;
+        free(le->default_desc);
+        free(le);
+    }
+    free(lt);
+}
+
+static void
+cleanup_logs(void)
+{
+
+    close_logs();
+    dict_delete(log_types);
+    dict_delete(log_dests);
+    dict_delete(log_dest_types);
+}
+
+static enum log_severity
+find_severity(const char *text)
+{
+    enum log_severity ls;
+    for (ls = 0; ls < LOG_NUM_SEVERITIES; ++ls)
+        if (!ircncasecmp(text, log_severity_names[ls], strlen(log_severity_names[ls])))
+            return ls;
+    return LOG_NUM_SEVERITIES;
+}
+
+/* Log keys are based on syslog.conf syntax:
+ *   KEY := LOGSET '.' SEVSET
+ *   LOGSET := LOGLIT | LOGLIT ',' LOGSET
+ *   LOGLIT := a registered log type
+ *   SEVSET := '*' | SEVLIT | '<' SEVLIT | '<=' SEVLIT | '>' SEVLIT | '>=' SEVLIT | SEVLIG ',' SEVSET
+ *   SEVLIT := one of log_severity_names
+ * A KEY contains the Cartesian product of the logs in its LOGSET
+ * and the severities in its SEVSET.
+ */
+
+static void
+log_parse_logset(char *buffer, struct string_list *slist)
+{
+    slist->used = 0;
+    while (buffer) {
+        char *cont = strchr(buffer, ',');
+        if (cont) *cont++ = 0;
+        string_list_append(slist, strdup(buffer));
+        buffer = cont;
+    }
+}
+
+static void
+log_parse_sevset(char *buffer, char targets[LOG_NUM_SEVERITIES])
+{
+    memset(targets, 0, LOG_NUM_SEVERITIES);
+    while (buffer) {
+        char *cont;
+        enum log_severity bound;
+        int first;
+
+        cont = strchr(buffer, ',');
+        if (cont) *cont++ = 0;
+        if (buffer[0] == '*' && buffer[1] == 0) {
+            for (bound = 0; bound < LOG_NUM_SEVERITIES; bound++) {
+                /* make people explicitly specify replay targets */
+                if (bound != LOG_REPLAY)
+                    targets[bound] = 1;
+            }
+        } else if (buffer[0] == '<') {
+            if (buffer[1] == '=') {
+                bound = find_severity(buffer+2) + 1;
+            } else {
+                bound = find_severity(buffer+1);
+            }
+            for (first = 1; bound > 0; bound--) {
+                /* make people explicitly specify replay targets */
+                if (bound != LOG_REPLAY || first) {
+                    targets[bound] = 1;
+                    first = 0;
+                }
+            }
+        } else if (buffer[0] == '>') {
+            if (buffer[1] == '=') {
+                bound = find_severity(buffer+2);
+            } else {
+                bound = find_severity(buffer+1) + 1;
+            }
+            for (first = 1; bound < LOG_NUM_SEVERITIES; bound++) {
+                /* make people explicitly specify replay targets */
+                if (bound != LOG_REPLAY || first) {
+                    targets[bound] = 1;
+                    first = 0;
+                }
+            }
+        } else {
+            bound = find_severity(buffer);
+            targets[bound] = 1;
+        }
+        buffer = cont;
+    }
+}
+
+static void
+log_parse_cross(const char *buffer, struct string_list *types, char sevset[LOG_NUM_SEVERITIES])
+{
+    char *dup, *sep;
+
+    dup = strdup(buffer);
+    sep = strchr(dup, '.');
+    *sep++ = 0;
+    log_parse_logset(dup, types);
+    log_parse_sevset(sep, sevset);
+    free(dup);
+}
+
+static void
+log_parse_options(struct log_type *type, struct dict *conf)
+{
+    const char *opt;
+    opt = database_get_data(conf, "max_age", RECDB_QSTRING);
+    if (opt) type->max_age = ParseInterval(opt);
+    opt = database_get_data(conf, "max_count", RECDB_QSTRING);
+    if (opt) type->max_count = strtoul(opt, NULL, 10);
+}
+
+static void
+log_conf_read(void)
+{
+    struct record_data *rd, *rd2;
+    dict_iterator_t it;
+    const char *sep;
+    struct log_type *type;
+    enum log_severity sev;
+    unsigned int ii;
+
+    close_logs();
+    dict_delete(log_dests);
+
+    log_dests = dict_new();
+    dict_set_free_keys(log_dests, free);
+
+    rd = conf_get_node("logs");
+    if (rd && (rd->type == RECDB_OBJECT)) {
+        for (it = dict_first(rd->d.object); it; it = iter_next(it)) {
+            if ((sep = strchr(iter_key(it), '.'))) {
+                struct logList logList;
+                char sevset[LOG_NUM_SEVERITIES];
+                struct string_list *slist;
+
+                /* It looks like a <type>.<severity> record.  Try to parse it. */
+                slist = alloc_string_list(4);
+                log_parse_cross(iter_key(it), slist, sevset);
+                logList.size = 0;
+                logList_open(&logList, iter_data(it));
+                for (ii = 0; ii < slist->used; ++ii) {
+                    type = log_register_type(slist->list[ii], NULL);
+                    for (sev = 0; sev < LOG_NUM_SEVERITIES; ++sev) {
+                        if (!sevset[sev]) continue;
+                        logList_join(&type->logs[sev], &logList);
+                    }
+                }
+                logList_close(&logList);
+                free_string_list(slist);
+            } else if ((rd2 = iter_data(it))
+                       && (rd2->type == RECDB_OBJECT)
+                       && (type = log_register_type(iter_key(it), NULL))) {
+                log_parse_options(type, rd2->d.object);
+            } else {
+                log_module(MAIN_LOG, LOG_ERROR, "Unknown logs subkey '%s'.", iter_key(it));
+            }
+        }
+    }
+    if (log_debugged) {
+        log_debug();
+    }
+}
+
+void
+log_debug(void)
+{
+    enum log_severity sev;
+    struct logDestination *log_stdout;
+    struct logList target;
+
+    log_stdout = log_open("std:out");
+    logList_init(&target);
+    logList_append(&target, log_stdout);
+
+    for (sev = 0; sev < LOG_NUM_SEVERITIES; ++sev) {
+        logList_join(&log_default->logs[sev], &target);
+    }
+
+    logList_close(&target);
+    log_debugged = 1;
+}
+
+void
+log_reopen(void)
+{
+    dict_iterator_t it;
+    for (it = dict_first(log_dests); it; it = iter_next(it)) {
+        struct logDestination *ld = iter_data(it);
+        ld->vtbl->reopen(ld);
+    }
+}
+
+struct log_type *
+log_register_type(const char *name, const char *default_log)
+{
+    struct log_type *type;
+    struct logDestination *dest;
+    enum log_severity sev;
+
+    if (!(type = dict_find(log_types, name, NULL))) {
+        type = calloc(1, sizeof(*type));
+        type->name = strdup(name);
+        type->max_age = 600;
+        type->max_count = 1024;
+        dict_insert(log_types, type->name, type);
+    }
+    if (default_log && !type->default_set) {
+        /* If any severity level was unspecified in the config, use the default. */
+        dest = NULL;
+        for (sev = 0; sev < LOG_NUM_SEVERITIES; ++sev) {
+            if (sev == LOG_REPLAY) continue; /* never default LOG_REPLAY */
+            if (!type->logs[sev].size) {
+                logList_init(&type->logs[sev]);
+                if (!dest) {
+                    if (!(dest = log_open(default_log))) break;
+                    dest->refcnt--;
+                }
+                logList_append(&type->logs[sev], dest);
+                dest->refcnt++;
+            }
+        }
+        type->default_set = 1;
+    }
+    return type;
+}
+
+/* logging functions */
+
+void
+log_audit(struct log_type *type, enum log_severity sev, struct userNode *user, struct userNode *bot, const char *channel_name, unsigned int flags, const char *command)
+{
+    struct logEntry *entry;
+    unsigned int size, ii;
+    char *str_next;
+
+    /* First make sure severity is appropriate */
+    if ((sev != LOG_COMMAND) && (sev != LOG_OVERRIDE) && (sev != LOG_STAFF)) {
+        log_module(MAIN_LOG, LOG_ERROR, "Illegal audit severity %d", sev);
+        return;
+    }
+    /* Allocate and fill in the log entry */
+    size = sizeof(*entry) + strlen(user->nick) + strlen(command) + 2;
+    if (user->handle_info) {
+        size += strlen(user->handle_info->handle) + 1;
+    }
+    if (channel_name) {
+        size += strlen(channel_name) + 1;
+    }
+    if (flags & AUDIT_HOSTMASK) {
+        size += strlen(user->ident) + strlen(user->hostname) + 2;
+    }
+    entry = calloc(1, size);
+    str_next = (char*)(entry + 1);
+    entry->time = now;
+    entry->slvl = sev;
+    entry->bot = bot;
+    if (channel_name) {
+        size = strlen(channel_name) + 1;
+        entry->channel_name = memcpy(str_next, channel_name, size);
+        str_next += size;
+    }
+    if (true) {
+        size = strlen(user->nick) + 1;
+        entry->user_nick = memcpy(str_next, user->nick, size);
+        str_next += size;
+    }
+    if (user->handle_info) {
+        size = strlen(user->handle_info->handle) + 1;
+        entry->user_account = memcpy(str_next, user->handle_info->handle, size);
+        str_next += size;
+    }
+    if (flags & AUDIT_HOSTMASK) {
+        size = sprintf(str_next, "%s@%s", user->ident, user->hostname) + 1;
+        entry->user_hostmask = str_next;
+        str_next += size;
+    } else {
+        entry->user_hostmask = 0;
+    }
+    if (true) {
+        size = strlen(command) + 1;
+        entry->command = memcpy(str_next, command, size);
+        str_next += size;
+    }
+
+    /* fill in the default text for the event */
+    log_format_audit(entry);
+
+    /* insert into the linked list */
+    entry->next = 0;
+    entry->prev = type->log_newest;
+    if (type->log_newest) {
+        type->log_newest->next = entry;
+    } else {
+        type->log_oldest = entry;
+    }
+    type->log_newest = entry;
+    type->log_count++;
+
+    /* remove old elements from the linked list */
+    while (type->log_count > type->max_count) {
+        struct logEntry *next = type->log_oldest->next;
+        free(type->log_oldest->default_desc);
+        free(type->log_oldest);
+        type->log_oldest = next;
+        type->log_count--;
+    }
+    while (type->log_oldest && (type->log_oldest->time + type->max_age < (unsigned long)now)) {
+        struct logEntry *next = type->log_oldest->next;
+        free(type->log_oldest->default_desc);
+        free(type->log_oldest);
+        type->log_oldest = next;
+        type->log_count--;
+    }
+    if (type->log_oldest)
+        type->log_oldest->prev = 0;
+    else
+        type->log_newest = 0;
+
+    /* call the destination logs */
+    for (ii=0; ii<type->logs[sev].used; ++ii) {
+        struct logDestination *ld = type->logs[sev].list[ii];
+        ld->vtbl->log_audit(ld, type, entry);
+    }
+    for (ii=0; ii<log_default->logs[sev].used; ++ii) {
+        struct logDestination *ld = log_default->logs[sev].list[ii];
+        ld->vtbl->log_audit(ld, type, entry);
+    }
+}
+
+void
+log_replay(struct log_type *type, int is_write, const char *line)
+{
+    unsigned int ii;
+
+    for (ii=0; ii<type->logs[LOG_REPLAY].used; ++ii) {
+        struct logDestination *ld = type->logs[LOG_REPLAY].list[ii];
+        ld->vtbl->log_replay(ld, type, is_write, line);
+    }
+    for (ii=0; ii<log_default->logs[LOG_REPLAY].used; ++ii) {
+        struct logDestination *ld = log_default->logs[LOG_REPLAY].list[ii];
+        ld->vtbl->log_replay(ld, type, is_write, line);
+    }
+}
+
+void
+log_module(struct log_type *type, enum log_severity sev, const char *format, ...)
+{
+    char msgbuf[1024];
+    unsigned int ii;
+    va_list args;
+
+    if (sev > LOG_FATAL) {
+        log_module(MAIN_LOG, LOG_ERROR, "Illegal log_module severity %d", sev);
+        return;
+    }
+    va_start(args, format);
+    vsnprintf(msgbuf, sizeof(msgbuf), format, args);
+    va_end(args);
+    if (log_inited) {
+        for (ii=0; ii<type->logs[sev].used; ++ii) {
+            struct logDestination *ld = type->logs[sev].list[ii];
+            ld->vtbl->log_module(ld, type, sev, msgbuf);
+        }
+        for (ii=0; ii<log_default->logs[sev].used; ++ii) {
+            struct logDestination *ld = log_default->logs[sev].list[ii];
+            ld->vtbl->log_module(ld, type, sev, msgbuf);
+        }
+    } else {
+        /* Special behavior before we start full operation */
+        fprintf(stderr, "%s: %s\n", log_severity_names[sev], msgbuf);
+    }
+}
+
+/* audit log searching */
+
+struct logSearch *
+log_discrim_create(struct userNode *service, struct userNode *user, unsigned int argc, char *argv[])
+{
+    unsigned int ii;
+    struct logSearch *discrim;
+
+    /* Assume all criteria require arguments. */
+    if((argc - 1) % 2)
+    {
+       send_message(user, service, "MSG_MISSING_PARAMS", argv[0]);
+       return NULL;
+    }
+
+    discrim = malloc(sizeof(struct logSearch));
+    memset(discrim, 0, sizeof(*discrim));
+    discrim->limit = 25;
+    discrim->max_time = INT_MAX;
+    discrim->severities = ~0;
+
+    for (ii=1; ii<argc-1; ii++) {
+        if (!irccasecmp(argv[ii], "bot")) {
+            struct userNode *bot = GetUserH(argv[++ii]);
+            if (!bot) {
+                send_message(user, service, "MSG_NICK_UNKNOWN", argv[ii]);
+                goto fail;
+            } else if (!IsLocal(bot)) {
+                send_message(user, service, "MSG_NOT_A_SERVICE", argv[ii]);
+                goto fail;
+            }
+            discrim->masks.bot = bot;
+        } else if (!irccasecmp(argv[ii], "channel")) {
+            discrim->masks.channel_name = argv[++ii];
+        } else if (!irccasecmp(argv[ii], "nick")) {
+            discrim->masks.user_nick = argv[++ii];
+        } else if (!irccasecmp(argv[ii], "account")) {
+            discrim->masks.user_account = argv[++ii];
+        } else if (!irccasecmp(argv[ii], "hostmask")) {
+            discrim->masks.user_hostmask = argv[++ii];
+        } else if (!irccasecmp(argv[ii], "command")) {
+            discrim->masks.command = argv[++ii];
+        } else if (!irccasecmp(argv[ii], "age")) {
+            const char *cmp = argv[++ii];
+            if (cmp[0] == '<') {
+                if (cmp[1] == '=') {
+                    discrim->min_time = now - ParseInterval(cmp+2);
+                } else {
+                    discrim->min_time = now - (ParseInterval(cmp+1) - 1);
+                }
+            } else if (cmp[0] == '>') {
+                if (cmp[1] == '=') {
+                    discrim->max_time = now - ParseInterval(cmp+2);
+                } else {
+                    discrim->max_time = now - (ParseInterval(cmp+1) - 1);
+                }
+            } else {
+                discrim->min_time = now - ParseInterval(cmp+2);
+            }
+        } else if (!irccasecmp(argv[ii], "limit")) {
+            discrim->limit = strtoul(argv[++ii], NULL, 10);
+        } else if (!irccasecmp(argv[ii], "level")) {
+            char *severity = argv[++ii];
+            discrim->severities = 0;
+            while (1) {
+                enum log_severity sev = find_severity(severity);
+                if (sev == LOG_NUM_SEVERITIES) {
+                    send_message(user, service, "MSG_INVALID_SEVERITY", severity);
+                    goto fail;
+                }
+                discrim->severities |= 1 << sev;
+                severity = strchr(severity, ',');
+                if (!severity)
+                    break;
+                severity++;
+            }
+        } else if (!irccasecmp(argv[ii], "type")) {
+            if (!(discrim->type = dict_find(log_types, argv[++ii], NULL))) {
+                send_message(user, service, "MSG_INVALID_FACILITY", argv[ii]);
+                goto fail;
+            }
+       } else {
+           send_message(user, service, "MSG_INVALID_CRITERIA", argv[ii]);
+           goto fail;
+       }
+    }
+
+    return discrim;
+  fail:
+    free(discrim);
+    return NULL;
+}
+
+static int
+entry_match(struct logSearch *discrim, struct logEntry *entry)
+{
+    if ((entry->time < discrim->min_time)
+        || (entry->time > discrim->max_time)
+        || !(discrim->severities & (1 << entry->slvl))
+        || (discrim->masks.bot && (discrim->masks.bot != entry->bot))
+        /* don't do glob matching, so that !events #a*b does not match #acb */
+        || (discrim->masks.channel_name
+            && (!entry->channel_name
+                || irccasecmp(entry->channel_name, discrim->masks.channel_name)))
+        || (discrim->masks.user_nick
+            && !match_ircglob(entry->user_nick, discrim->masks.user_nick))
+        || (discrim->masks.user_account
+            && (!entry->user_account
+                || !match_ircglob(entry->user_account, discrim->masks.user_account)))
+        || (discrim->masks.user_hostmask
+            && entry->user_hostmask
+            && !match_ircglob(entry->user_hostmask, discrim->masks.user_hostmask))
+        || (discrim->masks.command
+            && !match_ircglob(entry->command, discrim->masks.command))) {
+       return 0;
+    }
+    return 1;
+}
+
+void
+log_report_entry(struct logEntry *match, void *extra)
+{
+    struct logReport *rpt = extra;
+    send_message_type(4, rpt->user, rpt->reporter, "%s", match->default_desc);
+}
+
+unsigned int
+log_entry_search(struct logSearch *discrim, entry_search_func esf, void *data)
+{
+    unsigned int matched = 0;
+
+    if (discrim->type) {
+        struct logEntry *entry;
+
+        for (entry = discrim->type->log_oldest; entry; entry = entry->next) {
+            if (entry_match(discrim, entry)) {
+                esf(entry, data);
+                if (++matched >= discrim->limit)
+                    break;
+            }
+        }
+    } else {
+        dict_iterator_t it;
+
+        for (it = dict_first(log_types); it; it = iter_next(it)) {
+            discrim->type = iter_data(it);
+            matched += log_entry_search(discrim, esf, data);
+        }
+    }
+
+    return matched;
+}
+
+/* generic helper functions */
+
+static void
+log_format_timestamp(time_t when, struct string_buffer *sbuf)
+{
+    struct tm local;
+    localtime_r(&when, &local);
+    if (sbuf->size < 24) {
+        sbuf->size = 24;
+        free(sbuf->list);
+        sbuf->list = calloc(1, 24);
+    }
+    sbuf->used = sprintf(sbuf->list, "[%02d:%02d:%02d %02d/%02d/%04d]", local.tm_hour, local.tm_min, local.tm_sec, local.tm_mon+1, local.tm_mday, local.tm_year+1900);
+}
+
+static void
+log_format_audit(struct logEntry *entry)
+{
+    struct string_buffer sbuf;
+    memset(&sbuf, 0, sizeof(sbuf));
+    log_format_timestamp(entry->time, &sbuf);
+    string_buffer_append_string(&sbuf, " (");
+    string_buffer_append_string(&sbuf, entry->bot->nick);
+    if (entry->channel_name) {
+        string_buffer_append(&sbuf, ':');
+        string_buffer_append_string(&sbuf, entry->channel_name);
+    }
+    string_buffer_append_string(&sbuf, ") [");
+    string_buffer_append_string(&sbuf, entry->user_nick);
+    if (entry->user_hostmask) {
+        string_buffer_append(&sbuf, '!');
+        string_buffer_append_string(&sbuf, entry->user_hostmask);
+    }
+    if (entry->user_account) {
+        string_buffer_append(&sbuf, ':');
+        string_buffer_append_string(&sbuf, entry->user_account);
+    }
+    string_buffer_append_string(&sbuf, "]: ");
+    string_buffer_append_string(&sbuf, entry->command);
+    entry->default_desc = strdup(sbuf.list);
+    free(sbuf.list);
+}
+
+/* shared stub log operations act as a noop */
+
+static void
+ldNop_reopen(UNUSED_ARG(struct logDestination *self_)) {
+    /* no operation necessary */
+}
+
+static void
+ldNop_replay(UNUSED_ARG(struct logDestination *self_), UNUSED_ARG(struct log_type *type), UNUSED_ARG(int is_write), UNUSED_ARG(const char *line)) {
+    /* no operation necessary */
+}
+
+/* file: log type */
+
+struct logDest_file {
+    struct logDestination base;
+    char *fname;
+    FILE *output;
+};
+static struct logDest_vtable ldFile_vtbl;
+
+static struct logDestination *
+ldFile_open(const char *args) {
+    struct logDest_file *ld;
+    ld = calloc(1, sizeof(*ld));
+    ld->base.vtbl = &ldFile_vtbl;
+    ld->fname = strdup(args);
+    ld->output = fopen(ld->fname, "a");
+    return &ld->base;
+}
+
+static void
+ldFile_reopen(struct logDestination *self_) {
+    struct logDest_file *self = (struct logDest_file*)self_;
+    fclose(self->output);
+    self->output = fopen(self->fname, "a");
+}
+
+static void
+ldFile_close(struct logDestination *self_) {
+    struct logDest_file *self = (struct logDest_file*)self_;
+    fclose(self->output);
+    free(self->fname);
+    free(self);
+}
+
+static void
+ldFile_audit(struct logDestination *self_, UNUSED_ARG(struct log_type *type), struct logEntry *entry) {
+    struct logDest_file *self = (struct logDest_file*)self_;
+    fputs(entry->default_desc, self->output);
+    fputc('\n', self->output);
+    fflush(self->output);
+}
+
+static void
+ldFile_replay(struct logDestination *self_, UNUSED_ARG(struct log_type *type), int is_write, const char *line) {
+    struct logDest_file *self = (struct logDest_file*)self_;
+    struct string_buffer sbuf;
+    memset(&sbuf, 0, sizeof(sbuf));
+    log_format_timestamp(now, &sbuf);
+    string_buffer_append_string(&sbuf, is_write ? "W: " : "   ");
+    string_buffer_append_string(&sbuf, line);
+    fputs(sbuf.list, self->output);
+    fputc('\n', self->output);
+    free(sbuf.list);
+    fflush(self->output);
+}
+
+static void
+ldFile_module(struct logDestination *self_, struct log_type *type, enum log_severity sev, const char *message) {
+    struct logDest_file *self = (struct logDest_file*)self_;
+    struct string_buffer sbuf;
+    memset(&sbuf, 0, sizeof(sbuf));
+    log_format_timestamp(now, &sbuf);
+    fprintf(self->output, "%s (%s:%s) %s\n", sbuf.list, type->name, log_severity_names[sev], message);
+    free(sbuf.list);
+    fflush(self->output);
+}
+
+static struct logDest_vtable ldFile_vtbl = {
+    "file",
+    ldFile_open,
+    ldFile_reopen,
+    ldFile_close,
+    ldFile_audit,
+    ldFile_replay,
+    ldFile_module
+};
+
+/* std: log type */
+
+static struct logDest_vtable ldStd_vtbl;
+
+static struct logDestination *
+ldStd_open(const char *args) {
+    struct logDest_file *ld;
+    ld = calloc(1, sizeof(*ld));
+    ld->base.vtbl = &ldStd_vtbl;
+    ld->fname = strdup(args);
+
+    /* Print to stderr if given "err" and default to stdout otherwise. */
+    if (atoi(args)) {
+        ld->output = fdopen(atoi(args), "a");
+    } else if (!strcasecmp(args, "err")) {
+        ld->output = stdout;
+    } else {
+        ld->output = stderr;
+    }
+
+    return &ld->base;
+}
+
+static void
+ldStd_close(struct logDestination *self_) {
+    struct logDest_file *self = (struct logDest_file*)self_;
+    free(self->fname);
+    free(self);
+}
+
+static void
+ldStd_replay(struct logDestination *self_, UNUSED_ARG(struct log_type *type), int is_write, const char *line) {
+    struct logDest_file *self = (struct logDest_file*)self_;
+    fprintf(self->output, "%s%s\n", is_write ? "W: " : "   ", line);
+}
+
+static void
+ldStd_module(struct logDestination *self_, UNUSED_ARG(struct log_type *type), enum log_severity sev, const char *message) {
+    struct logDest_file *self = (struct logDest_file*)self_;
+    fprintf(self->output, "%s: %s\n", log_severity_names[sev], message);
+}
+
+static struct logDest_vtable ldStd_vtbl = {
+    "std",
+    ldStd_open,
+    ldNop_reopen,
+    ldStd_close,
+    ldFile_audit,
+    ldStd_replay,
+    ldStd_module
+};
+
+/* irc: log type */
+
+struct logDest_irc {
+    struct logDestination base;
+    char *target;
+};
+static struct logDest_vtable ldIrc_vtbl;
+
+static struct logDestination *
+ldIrc_open(const char *args) {
+    struct logDest_irc *ld;
+    ld = calloc(1, sizeof(*ld));
+    ld->base.vtbl = &ldIrc_vtbl;
+    ld->target = strdup(args);
+    return &ld->base;
+}
+
+static void
+ldIrc_close(struct logDestination *self_) {
+    struct logDest_irc *self = (struct logDest_irc*)self_;
+    free(self->target);
+    free(self);
+}
+
+static void
+ldIrc_audit(struct logDestination *self_, UNUSED_ARG(struct log_type *type), struct logEntry *entry) {
+    struct logDest_irc *self = (struct logDest_irc*)self_;
+
+    if (entry->channel_name) {
+        send_target_message(4, self->target, entry->bot, "(%s", strchr(strchr(entry->default_desc, ' '), ':')+1);
+    } else {
+        send_target_message(4, self->target, entry->bot, "%s", strchr(entry->default_desc, ')')+2);
+    }
+}
+
+static void
+ldIrc_module(struct logDestination *self_, struct log_type *type, enum log_severity sev, const char *message) {
+    struct logDest_irc *self = (struct logDest_irc*)self_;
+    extern struct userNode *opserv;
+
+    send_target_message(4, self->target, opserv, "%s %s: %s\n", type->name, log_severity_names[sev], message);
+}
+
+static struct logDest_vtable ldIrc_vtbl = {
+    "irc",
+    ldIrc_open,
+    ldNop_reopen,
+    ldIrc_close,
+    ldIrc_audit,
+    ldNop_replay, /* totally ignore this - it would be a recipe for disaster */
+    ldIrc_module
+};
+
+void
+log_init(void)
+{
+    log_types = dict_new();
+    dict_set_free_keys(log_types, free);
+    dict_set_free_data(log_types, log_type_free);
+    log_dest_types = dict_new();
+    /* register log types */
+    dict_insert(log_dest_types, ldFile_vtbl.type_name, &ldFile_vtbl);
+    dict_insert(log_dest_types, ldStd_vtbl.type_name, &ldStd_vtbl);
+    dict_insert(log_dest_types, ldIrc_vtbl.type_name, &ldIrc_vtbl);
+    conf_register_reload(log_conf_read);
+    log_default = log_register_type("*", NULL);
+    reg_exit_func(cleanup_logs);
+    message_register_table(msgtab);
+    log_inited = 1;
+}
diff --git a/src/log.h b/src/log.h
new file mode 100644 (file)
index 0000000..938b145
--- /dev/null
+++ b/src/log.h
@@ -0,0 +1,93 @@
+/* log.h - Diagnostic and error logging
+ * Copyright 2000-2003 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef LOG_H
+#define LOG_H
+
+#include "common.h"
+
+enum log_severity {
+    LOG_REPLAY,   /* 0 */
+    LOG_DEBUG,
+    LOG_COMMAND,
+    LOG_INFO,
+    LOG_OVERRIDE,
+    LOG_STAFF,    /* 5 */
+    LOG_WARNING,
+    LOG_ERROR,
+    LOG_FATAL,
+    LOG_NUM_SEVERITIES
+};
+
+struct log_type;
+
+void log_init(void);
+void log_reopen(void);
+void log_debug(void);
+
+/* bitmap values in flags parameter to log_audit */
+#define AUDIT_HOSTMASK  0x01
+
+struct log_type *log_register_type(const char *name, const char *default_log);
+/* constraint for log_audit: sev one of LOG_COMMAND, LOG_OVERRIDE, LOG_STAFF */
+void log_audit(struct log_type *type, enum log_severity sev, struct userNode *user, struct userNode *bot, const char *channel_name, unsigned int flags, const char *command);
+/* constraint for log_module: sev < LOG_COMMAND */
+void log_module(struct log_type *type, enum log_severity sev, const char *format, ...) PRINTF_LIKE(3, 4);
+void log_replay(struct log_type *type, int is_write, const char *line);
+
+/* Log searching functions - ONLY searches log_audit'ed data */
+
+struct logEntry
+{
+                                      /* field nullable in real entries? */
+    time_t            time;
+    enum log_severity slvl;
+    struct userNode   *bot;           /* no */
+    char              *channel_name;  /* yes */
+    char              *user_nick;     /* no */
+    char              *user_account;  /* yes */
+    char              *user_hostmask; /* yes */
+    char              *command;       /* no */
+    char              *default_desc;
+    struct logEntry   *next;
+    struct logEntry   *prev;
+};
+
+struct logSearch
+{
+    struct logEntry  masks;
+    struct log_type  *type;
+    time_t           min_time;
+    time_t           max_time;
+    unsigned int     limit;
+    unsigned int     severities;
+};
+
+struct logReport
+{
+    struct userNode  *reporter;
+    struct userNode  *user;
+};
+
+typedef void (*entry_search_func)(struct logEntry *match, void *extra);
+void log_report_entry(struct logEntry *match, void *extra);
+struct logSearch* log_discrim_create(struct userNode *service, struct userNode *user, unsigned int argc, char *argv[]);
+unsigned int log_entry_search(struct logSearch *discrim, entry_search_func esf, void *data);
+void report_entry(struct userNode *service, struct userNode *user, struct logEntry *entry);
+
+#endif
diff --git a/src/main.c b/src/main.c
new file mode 100644 (file)
index 0000000..5bc704e
--- /dev/null
@@ -0,0 +1,855 @@
+/* main.c - srvx
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#define PID_FILE "srvx.pid"
+
+#include "conf.h"
+#include "gline.h"
+#include "ioset.h"
+#include "modcmd.h"
+#include "saxdb.h"
+#include "sendmail.h"
+#include "timeq.h"
+
+#include "chanserv.h"
+#include "global.h"
+#include "modules.h"
+#include "opserv.h"
+
+#ifdef HAVE_GETOPT_H
+#include <getopt.h>
+#else
+#include "getopt.h"
+#endif
+#ifdef HAVE_SYS_RESOURCE_H
+#include <sys/resource.h>
+#endif
+#ifdef HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#endif
+#ifdef HAVE_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif
+#ifdef HAVE_SYS_WAIT_H
+#include <sys/wait.h>
+#endif
+
+#ifndef SIGCHLD
+#define SIGCHLD SIGCLD
+#endif
+
+extern FILE *replay_file;
+
+time_t boot_time, burst_begin, now;
+unsigned long burst_length;
+struct log_type *MAIN_LOG;
+
+int quit_services, max_cycles;
+
+char *services_config = "srvx.conf";
+
+char **services_argv;
+int services_argc;
+
+struct cManagerNode cManager;
+
+struct policer_params *oper_policer_params, *luser_policer_params, *god_policer_params;
+
+static const struct message_entry msgtab[] = {
+    { "MSG_NONE", "None" },
+    { "MSG_ON", "On" },
+    { "MSG_OFF", "Off" },
+    { "MSG_NEVER", "Never" },
+    { "MSG_SERVICE_IMMUNE", "$b%s$b may not be kicked, killed, banned, or deopped." },
+    { "MSG_SERVICE_PRIVILEGED", "$b%s$b is a privileged service." },
+    { "MSG_NOT_A_SERVICE", "$b%s$b is not a service bot." },
+    { "MSG_COMMAND_UNKNOWN", "$b%s$b is an unknown command." },
+    { "MSG_COMMAND_PRIVILEGED", "$b%s$b is a privileged command." },
+    { "MSG_COMMAND_DISABLED", "$b%s$b is a disabled command." },
+    { "MSG_SETTING_PRIVILEGED", "$b%s$b is a privileged setting." },
+    { "MSG_REGISTER_HANDLE", "You must first register a account with $b$N$b." },
+    { "MSG_AUTHENTICATE", "You must first authenticate with $b$N$b." },
+    { "MSG_USER_AUTHENTICATE", "%s must first authenticate with $b$N$b." },
+    { "MSG_SET_EMAIL_ADDR", "You must first set your account's email address.  (Contact network staff if you cannot auth to your account.)" },
+    { "MSG_HANDLE_UNKNOWN", "Account $b%s$b has not been registered." },
+    { "MSG_NICK_UNKNOWN", "User with nick $b%s$b does not exist." },
+    { "MSG_CHANNEL_UNKNOWN", "Channel with name $b%s$b does not exist." },
+    { "MSG_SERVER_UNKNOWN", "Server with name $b%s$b does not exist or is not linked." },
+    { "MSG_MODULE_UNKNOWN", "No module has been registered with name $b%s$b." },
+    { "MSG_INVALID_MODES", "$b%s$b is an invalid set of channel modes." },
+    { "MSG_INVALID_GLINE", "Invalid G-line '%s'." },
+    { "MSG_INVALID_DURATION", "Invalid time span '%s'." },
+    { "MSG_NOT_CHANNEL_NAME", "You must provide a valid channel name." },
+    { "MSG_INVALID_CHANNEL", "You must provide the name of a channel that exists." },
+    { "MSG_CHANNEL_ABSENT", "You aren't currently in $b%s$b." },
+    { "MSG_CHANNEL_USER_ABSENT", "$b%s$b isn't currently in $b%s$b." },
+    { "MSG_MISSING_PARAMS", "$b%s$b requires more parameters." },
+    { "MSG_DEPRECATED_COMMAND", "The $b%s$b command has been deprecated, and will be removed in the future; please use $b%s$b instead." },
+    { "MSG_OPER_SUSPENDED", "Your $b$O$b access has been suspended." },
+    { "MSG_USER_OUTRANKED", "$b%s$b outranks you (command has no effect)." },
+    { "MSG_STUPID_ACCESS_CHANGE", "Please ask someone $belse$b to demote you." },
+    { "MSG_NO_SEARCH_ACCESS", "You do not have enough access to search based on $b%s$b." },
+    { "MSG_INVALID_CRITERIA", "$b%s$b is an invalid search criteria." },
+    { "MSG_MATCH_COUNT", "Found $b%u$b matches." },
+    { "MSG_NO_MATCHES", "Nothing matched the criteria of your search." },
+    { "MSG_TOPIC_UNKNOWN", "No help on that topic." },
+    { "MSG_INVALID_BINARY", "$b%s$b is an invalid binary value." },
+    { "MSG_INTERNAL_FAILURE", "Your command could not be processed due to an internal failure." },
+    { "MSG_DB_UNKNOWN", "I do not know of a database named %s." },
+    { "MSG_DB_IS_MONDO", "Database %s is in the \"mondo\" database and cannot be written separately." },
+    { "MSG_DB_WRITE_ERROR", "Error while writing database %s." },
+    { "MSG_DB_WROTE_DB", "Wrote database %s (in "FMT_TIME_T".%06lu seconds)." },
+    { "MSG_DB_WROTE_ALL", "Wrote all databases (in "FMT_TIME_T".%06lu seconds)." },
+    { NULL, NULL }
+};
+
+void uplink_select(char *name);
+
+static int
+uplink_insert(const char *key, void *data, UNUSED_ARG(void *extra))
+{
+    struct uplinkNode *uplink = malloc(sizeof(struct uplinkNode));
+    struct record_data *rd = data;
+    int enabled = 1;
+    char *str;
+    struct sockaddr_in *sin;
+    unsigned long addr;
+
+    if(!uplink)
+    {
+       return 0;
+    }
+
+    uplink->name = (char *)key;
+    uplink->host = database_get_data(rd->d.object, "address", RECDB_QSTRING);
+
+    str = database_get_data(rd->d.object, "port", RECDB_QSTRING);
+    uplink->port = str ? atoi(str) : 6667;
+    uplink->password = database_get_data(rd->d.object, "password", RECDB_QSTRING);
+    uplink->their_password = database_get_data(rd->d.object, "uplink_password", RECDB_QSTRING);
+
+    str = database_get_data(rd->d.object, "enabled", RECDB_QSTRING);
+    if(str)
+    {
+       enabled = atoi(str) ? 1 : 0;
+    }
+
+    cManager.enabled += enabled;
+
+    str = database_get_data(rd->d.object, "max_tries", RECDB_QSTRING);
+    uplink->max_tries = str ? atoi(str) : 3;
+    uplink->flags = enabled ? 0 : UPLINK_UNAVAILABLE;
+    uplink->state = DISCONNECTED;
+    uplink->tries = 0;
+
+    str = database_get_data(rd->d.object, "bind_address", RECDB_QSTRING);
+    uplink->bind_addr_len = sizeof(*sin);
+    if (str && getipbyname(str, &addr)) 
+    {
+       sin = malloc(uplink->bind_addr_len);
+        sin->sin_port = 0;
+       sin->sin_family = AF_INET;
+       sin->sin_addr.s_addr = addr;
+#ifdef HAVE_SIN_LEN
+       sin->sin_len = 0;
+#endif
+       memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
+       uplink->bind_addr = sin;
+    } 
+    else 
+    {
+       uplink->bind_addr = NULL;
+       uplink->bind_addr_len = 0;
+    }
+
+    uplink->next = cManager.uplinks;
+    uplink->prev = NULL;
+
+    if(cManager.uplinks)
+    {
+       cManager.uplinks->prev = uplink;
+    }
+
+    cManager.uplinks = uplink;
+
+    /* If the configuration is being reloaded, set the current uplink
+       to the reloaded equivalent, if possible. */
+    if(cManager.uplink
+       && enabled
+       && !irccasecmp(uplink->host, cManager.uplink->host)
+       && uplink->port == cManager.uplink->port)
+    {
+       uplink->state = cManager.uplink->state;
+       uplink->tries = cManager.uplink->tries;
+       cManager.uplink = uplink;
+    }
+
+    return 0;
+}
+
+void
+uplink_compile(void)
+{
+    const char *cycles;
+    dict_t conf_node;
+    struct uplinkNode *oldUplinks = NULL, *oldUplink = NULL;
+
+    /* Save the old uplinks, we'll remove them later. */
+    oldUplink = cManager.uplink;
+    oldUplinks = cManager.uplinks;
+
+    cycles = conf_get_data("server/max_cycles", RECDB_QSTRING);
+    max_cycles = cycles ? atoi(cycles) : 30;
+    if(!(conf_node = conf_get_data("uplinks", RECDB_OBJECT)))
+    {
+        log_module(MAIN_LOG, LOG_FATAL, "No uplinks configured; giving up.");
+       exit(1);
+    }
+
+    cManager.enabled = 0;
+    dict_foreach(conf_node, uplink_insert, NULL);
+
+    /* Remove the old uplinks, if any. It doesn't matter if oldUplink (below)
+       is a reference to one of these, because it won't get dereferenced. */
+    if(oldUplinks)
+    {
+       struct uplinkNode *uplink, *next;
+
+       oldUplinks->prev->next = NULL;
+
+       for(uplink = oldUplinks; uplink; uplink = next)
+       {
+           next = uplink->next;
+           free(uplink);
+       }
+    }
+
+    /* If the uplink hasn't changed, it's either NULL or pointing at
+       an uplink that was just deleted, select a new one. */
+    if(cManager.uplink == oldUplink)
+    {
+       if(oldUplink)
+       {
+           irc_squit(self, "Uplinks updated; selecting new uplink.", NULL);
+       }
+
+       cManager.uplink = NULL;
+       uplink_select(NULL);
+    }
+}
+
+struct uplinkNode *
+uplink_find(char *name)
+{
+    struct uplinkNode *uplink;
+
+    if(!cManager.enabled || !cManager.uplinks)
+    {
+       return NULL;
+    }
+
+    for(uplink = cManager.uplinks; uplink; uplink = uplink->next)
+    {
+       if(!strcasecmp(uplink->name, name))
+       {
+           return uplink;
+       }
+    }
+
+    return NULL;
+}
+
+void
+uplink_select(char *name)
+{
+    struct uplinkNode *start, *uplink, *next;
+    int stop;
+
+    if(!cManager.enabled || !cManager.uplinks)
+    {
+       log_module(MAIN_LOG, LOG_FATAL, "No uplinks enabled; giving up.");
+       exit(1);
+    }
+
+    if(!cManager.uplink)
+    {
+       start = cManager.uplinks;
+    }
+    else
+    {
+       start = cManager.uplink->next;
+       if(!start)
+       {
+           start = cManager.uplinks;
+       }
+    }
+
+    stop = 0;
+    for(uplink = start; uplink; uplink = next)
+    {
+       next = uplink->next ? uplink->next : cManager.uplinks;
+
+       if(stop)
+       {
+           uplink = NULL;
+           break;
+       }
+
+       /* We've wrapped around the list. */
+       if(next == start)
+       {
+           sleep((cManager.cycles >> 1) * 5);
+           cManager.cycles++;
+
+           if(max_cycles && (cManager.cycles >= max_cycles))
+           {
+               log_module(MAIN_LOG, LOG_FATAL, "Maximum uplink list cycles exceeded; giving up.");
+               exit(1);
+           }
+
+           /* Give the uplink currently in 'uplink' consideration,
+              and if not selected, break on the next iteration. */
+           stop = 1;
+       }
+
+       /* Skip bad uplinks. */
+       if(uplink->flags & UPLINK_UNAVAILABLE)
+       {
+           continue;
+       }
+
+       if(name && irccasecmp(uplink->name, name))
+       {
+           /* If we were told to connect to a specific uplink, don't stop
+              until we find it.
+           */
+           continue;
+       }
+
+       /* It would be possible to track uplink health through a variety
+          of statistics and only break on the best uplink. For now, break
+          on the first available one.
+       */
+
+       break;
+    }
+
+    if(!uplink)
+    {
+       /* We are shit outta luck if every single uplink has been passed
+          over. Use the current uplink if possible. */
+       if(!cManager.uplink || cManager.uplink->flags & UPLINK_UNAVAILABLE)
+       {
+           log_module(MAIN_LOG, LOG_FATAL, "All available uplinks exhausted; giving up.");
+           exit(1);
+       }
+
+       return;
+    }
+
+    cManager.uplink = uplink;
+}
+
+int
+uplink_connect(void)
+{
+    struct uplinkNode *uplink = cManager.uplink;
+
+    if(uplink->state != DISCONNECTED)
+    {
+       return 0;
+    }
+
+    if(uplink->flags & UPLINK_UNAVAILABLE)
+    {
+       uplink_select(NULL);
+       uplink = cManager.uplink;
+    }
+
+    if(uplink->tries)
+    {
+       /* This delay could scale with the number of tries. */
+       sleep(2);
+    }
+
+    if(!create_socket_client(uplink))
+    {
+       if(uplink->max_tries && (uplink->tries >= uplink->max_tries))
+       {
+           /* This is a bad uplink, move on. */
+           uplink->flags |= UPLINK_UNAVAILABLE;
+           uplink_select(NULL);
+       }
+
+       return 0;
+    }
+    else
+    {
+       uplink->state = AUTHENTICATING;
+       irc_introduce(uplink->password);
+    }
+
+    return 1;
+}
+
+void
+received_ping(void)
+{
+    /* This function is called when a ping is received. Take it as
+       a sign of link health and reset the connection manager
+       information. */
+
+    cManager.cycles = 0;
+}
+
+void sigaction_writedb(int x)
+{
+#ifndef HAVE_STRSIGNAL
+    log_module(MAIN_LOG, LOG_INFO, "Signal %d -- writing databases.", x);
+#else
+    log_module(MAIN_LOG, LOG_INFO, "%s -- writing databases.", strsignal(x));
+#endif
+    do_write_dbs = 1;
+}
+
+void sigaction_exit(int x)
+{
+#ifndef HAVE_STRSIGNAL
+    log_module(MAIN_LOG, LOG_INFO, "Signal %d -- exiting.", x);
+#else
+    log_module(MAIN_LOG, LOG_INFO, "%s -- exiting.", strsignal(x));
+#endif
+    irc_squit(self, "Exiting on signal from console.", NULL);
+    quit_services = 1;
+}
+
+void sigaction_wait(UNUSED_ARG(int x))
+{
+    int code;
+    wait4(-1, &code, WNOHANG, NULL);
+}
+
+void sigaction_rehash(int x)
+{
+#ifndef HAVE_STRSIGNAL
+    log_module(MAIN_LOG, LOG_INFO, "Signal %d -- rehashing.", x);
+#else
+    log_module(MAIN_LOG, LOG_INFO, "%s -- rehashing.", strsignal(x));
+#endif
+    do_reopen = 1;
+}
+
+static exit_func_t *ef_list;
+static unsigned int ef_size = 0, ef_used = 0;
+
+void reg_exit_func(exit_func_t handler)
+{
+    if (ef_used == ef_size) {
+       if (ef_size) {
+           ef_size <<= 1;
+           ef_list = realloc(ef_list, ef_size*sizeof(exit_func_t));
+       } else {
+           ef_size = 8;
+           ef_list = malloc(ef_size*sizeof(exit_func_t));
+       }
+    }
+    ef_list[ef_used++] = handler;
+}
+
+void call_exit_funcs(void)
+{
+    unsigned int n = ef_used;
+
+    /* Call them in reverse order because we initialize logs, then
+     * nickserv, then chanserv, etc., and they register their exit
+     * funcs in that order, and there are some dependencies (for
+     * example, ChanServ requires NickServ to not have cleaned up).
+     */
+
+    while (n > 0) {
+       ef_list[--n]();
+    }
+    free(ef_list);
+    ef_used = ef_size = 0;
+}
+
+int
+set_policer_param(const char *param, void *data, void *extra)
+{
+    struct record_data *rd = data;
+    const char *str = GET_RECORD_QSTRING(rd);
+    if (str) {
+        policer_params_set(extra, param, str);
+    }
+    return 0;
+}
+
+static void
+conf_globals(void)
+{
+    const char *info;
+    dict_t dict;
+
+    info = conf_get_data("services/global/nick", RECDB_QSTRING);
+    if (info && (info[0] != '.'))
+        init_global(info);
+
+    info = conf_get_data("services/nickserv/nick", RECDB_QSTRING);
+    if (info && (info[0] != '.'))
+        init_nickserv(info);
+
+    info = conf_get_data("services/chanserv/nick", RECDB_QSTRING);
+    if (info && (info[0] != '.'))
+        init_chanserv(info);
+
+    god_policer_params = policer_params_new();
+    if ((dict = conf_get_data("policers/commands-god", RECDB_OBJECT))) {
+        dict_foreach(dict, set_policer_param, god_policer_params);
+    } else {
+        policer_params_set(god_policer_params, "size", "30");
+        policer_params_set(god_policer_params, "drain-rate", "1");
+    }
+    oper_policer_params = policer_params_new();
+    if ((dict = conf_get_data("policers/commands-oper", RECDB_OBJECT))) {
+        dict_foreach(dict, set_policer_param, oper_policer_params);
+    } else {
+        policer_params_set(oper_policer_params, "size", "10");
+        policer_params_set(oper_policer_params, "drain-rate", "1");
+    }
+    luser_policer_params = policer_params_new();
+    if ((dict = conf_get_data("policers/commands-luser", RECDB_OBJECT))) {
+        dict_foreach(dict, set_policer_param, luser_policer_params);
+    } else {
+        policer_params_set(luser_policer_params, "size", "5");
+        policer_params_set(luser_policer_params, "drain-rate", "0.50");
+    }
+
+    info = conf_get_data("services/opserv/nick", RECDB_QSTRING);
+    if (info)
+        init_opserv(info);
+}
+
+#ifdef HAVE_SYS_RESOURCE_H
+
+static int
+set_item_rlimit(const char *name, void *data, void *extra)
+{
+    int rsrc, found;
+    struct record_data *rd = data;
+    struct rlimit rlim;
+    const char *str;
+
+    rsrc = (int)dict_find(extra, name, &found);
+    if (!found) {
+        log_module(MAIN_LOG, LOG_ERROR, "Invalid rlimit \"%s\" in rlimits section.", name);
+        return 0;
+    }
+    if (!(str = GET_RECORD_QSTRING(rd))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Missing or invalid parameter type for rlimit \"%s\".", name);
+        return 0;
+    }
+    if (getrlimit(rsrc, &rlim) < 0) {
+        log_module(MAIN_LOG, LOG_ERROR, "Couldn't get rlimit \"%s\": errno %d: %s", name, errno, strerror(errno));
+        return 0;
+    }
+    rlim.rlim_cur = ParseVolume(str);
+    if (setrlimit(rsrc, &rlim) < 0) {
+        log_module(MAIN_LOG, LOG_ERROR, "Couldn't set rlimit \"%s\": errno %d: %s", name, errno, strerror(errno));
+    }
+    return 0;
+}
+
+static void
+conf_rlimits(void)
+{
+    dict_t dict, values;
+
+    values = dict_new();
+    dict_insert(values, "data", (void*)RLIMIT_DATA);
+    dict_insert(values, "stack", (void*)RLIMIT_STACK);
+#ifdef RLIMIT_VMEM
+    dict_insert(values, "vmem", (void*)RLIMIT_VMEM);
+#else
+#ifdef RLIMIT_AS
+    dict_insert(values, "vmem", (void*)RLIMIT_AS);
+#endif
+#endif
+    if ((dict = conf_get_data("rlimits", RECDB_OBJECT))) {
+        dict_foreach(dict, set_item_rlimit, values);
+    }
+    dict_delete(values);
+}
+
+#else
+
+static void
+conf_rlimits(void)
+{
+}
+
+#endif
+
+void main_shutdown(void)
+{
+    struct uplinkNode *ul, *ul_next;
+    ioset_cleanup();
+    for (ul = cManager.uplinks; ul; ul = ul_next) {
+        ul_next = ul->next;
+        if (ul->bind_addr) free(ul->bind_addr);
+        free(ul);
+    }
+    tools_cleanup();
+    conf_close();
+    remove(PID_FILE);
+    policer_params_delete(god_policer_params);
+    policer_params_delete(oper_policer_params);
+    policer_params_delete(luser_policer_params);
+    if (replay_file)
+        fclose(replay_file);
+}
+
+void usage(char *self) {
+    /* We can assume we have getopt_long(). */
+    printf("Usage: %s [-c config] [-r log] [-d] [-f] [-v|-h]\n"
+           "-c, --config                    selects a different configuration file.\n"
+           "-d, --debug                     enables debug mode.\n"
+           "-f, --foreground                run srvx in the foreground.\n"
+           "-h, --help                      prints this usage message.\n"
+           "-k, --check                     checks the configuration file's syntax.\n"
+           "-r, --replay                    replay a log file (for debugging)\n"
+           "-v, --version                   prints this program's version.\n"
+           , self);
+}
+
+void version() {
+    printf("    --------------------------------------------------\n"
+           "    - "PACKAGE_STRING" ("CODENAME"), Built: " __DATE__ ", " __TIME__".\n"
+           "    - Copyright (C) 2000 - 2003, srvx Development Team\n"
+           "    --------------------------------------------------\n");
+}
+
+void license() {
+    printf("\n"
+           "This program is free software; you can redistribute it and/or modify\n"
+           "it under the terms of the GNU General Public License as published by\n"
+           "the Free Software Foundation; either version 2 of the License, or\n"
+           "(at your option) any later version.\n"
+           "\n"
+           "This program is distributed in the hope that it will be useful,\n"
+           "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
+           "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
+           "GNU General Public License for more details.\n"
+           "\n"
+           "You should have received a copy of the GNU General Public License\n"
+           "along with this program; if not, write to the Free Software\n"
+           "Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.\n\n");
+}
+
+#if WITH_MALLOC_BOEHM_GC
+void
+gc_warn_proc(char *msg, GC_word arg)
+{
+    log_module(MAIN_LOG, LOG_ERROR, "GC(%p): %s", (void*)arg, msg);
+}
+#endif
+
+int main(int argc, char *argv[])
+{
+    int daemon, debug;
+    pid_t pid = 0;
+    FILE *file_out;
+    struct sigaction sv;
+
+    daemon = 1;
+    debug = 0;
+    tools_init();
+
+    /* set up some signal handlers */
+    memset(&sv, 0, sizeof(sv));
+    sigemptyset(&sv.sa_mask);
+    sv.sa_handler = SIG_IGN;
+    sigaction(SIGPIPE, &sv, NULL);
+    sv.sa_handler = sigaction_rehash;
+    sigaction(SIGHUP, &sv, NULL);
+    sv.sa_handler = sigaction_writedb;
+    sigaction(SIGINT, &sv, NULL);
+    sv.sa_handler = sigaction_exit;
+    sigaction(SIGQUIT, &sv, NULL);
+    sv.sa_handler = sigaction_wait;
+    sigaction(SIGCHLD, &sv, NULL);
+
+    if (argc > 1) { /* parse command line, if any */
+       int c;
+       struct option options[] =
+       {
+           {"config", 1, 0, 'c'},
+            {"debug", 0, 0, 'd'},
+           {"foreground", 0, 0, 'f'},
+           {"help", 0, 0, 'h'},
+           {"check", 0, 0, 'k'},
+            {"replay", 1, 0, 'r'},
+           {"version", 0, 0, 'v'},
+           {"verbose", 0, 0, 'V'},
+           {0, 0, 0, 0}
+       };
+
+       while ((c = getopt_long(argc, argv, "c:kr:dfvVh", options, NULL)) != -1) {
+           switch(c) {
+           case 'c':
+               services_config = optarg;
+               break;
+           case 'k':
+               if (conf_read(services_config)) {
+                   printf("%s appears to be a valid configuration file.\n", services_config);
+               } else {
+                   printf("%s is an invalid configuration file.\n", services_config);
+               }
+               exit(0);
+            case 'r':
+                replay_file = fopen(optarg, "r");
+                if (!replay_file) {
+                    fprintf(stderr, "Could not open %s for reading: %s (%d)\n",
+                            optarg, strerror(errno), errno);
+                    exit(0);
+                }
+                break;
+            case 'd':
+                debug = 1;
+                break;
+           case 'f':
+               daemon = 0;
+               break;
+           case 'v':
+               version();
+               license();
+               exit(0);
+           case 'h':
+           default:
+               usage(argv[0]);
+               exit(0);
+           }
+       }
+    }
+
+    version();
+
+#ifdef __CYGWIN__
+    if(daemon)
+    {
+       fprintf(stderr, "Daemon mode not supported under Cygwin.\n");
+       daemon = 0;
+    }
+#endif
+
+    if (replay_file) {
+        /* We read a line here to "prime" the replay file parser, but
+         * mostly to get the right value of "now" for when we do the
+         * irc_introduce. */
+        replay_read_line();
+        boot_time = now;
+    } else {
+        boot_time = time(&now);
+    }
+
+    log_module(MAIN_LOG, LOG_INFO, "Initializing daemon...");
+    if (!conf_read(services_config)) {
+       log_module(MAIN_LOG, LOG_FATAL, "Unable to read %s.", services_config);
+       exit(0);
+    }
+
+    conf_register_reload(uplink_compile);
+
+    if (daemon) {
+       /* Attempt to fork into the background if daemon mode is on. */
+       pid = fork();
+       if (pid < 0) {
+           log_module(MAIN_LOG, LOG_FATAL, "Unable to fork: %s", strerror(errno));
+        } else if (pid > 0) {
+           log_module(MAIN_LOG, LOG_INFO, "Forking into the background (pid: %i)...", pid);
+           exit(0);
+       }
+       setsid();
+        /* Close these since we should not use them from now on. */
+        fclose(stdin);
+        fclose(stdout);
+        fclose(stderr);
+    }
+
+    if ((file_out = fopen(PID_FILE, "w")) == NULL) {
+       /* Create the main process' pid file */
+       log_module(MAIN_LOG, LOG_ERROR, "Unable to create PID file: %s", strerror(errno));
+    } else {
+       fprintf(file_out, "%i\n", (int)getpid());
+       fclose(file_out);
+    }
+
+    services_argc = argc;
+    services_argv = argv;
+
+    atexit(call_exit_funcs);
+    reg_exit_func(main_shutdown);
+
+    log_init();
+    MAIN_LOG = log_register_type("srvx", "file:main.log");
+    if (debug)
+        log_debug();
+#if WITH_MALLOC_BOEHM_GC
+    GC_set_warn_proc(gc_warn_proc);
+    GC_enable_incremental();
+#endif
+    timeq_init();
+    init_structs();
+    init_parse();
+    modcmd_init();
+    saxdb_init();
+    gline_init();
+    sendmail_init();
+    conf_globals(); /* initializes the core services */
+    conf_rlimits();
+    modules_init();
+    message_register_table(msgtab);
+    modcmd_finalize();
+    helpfile_finalize();
+    saxdb_finalize();
+    modules_finalize();
+
+    /* The first exit func to be called *should* be saxdb_write_all(). */
+    reg_exit_func(saxdb_write_all);
+    if (replay_file) {
+        char *msg;
+        log_module(MAIN_LOG, LOG_INFO, "Beginning replay...");
+        srand(now);
+        replay_event_loop();
+        if ((msg = dict_sanity_check(clients))) {
+            log_module(MAIN_LOG, LOG_ERROR, "Clients insanity: %s", msg);
+            free(msg);
+        }
+        if ((msg = dict_sanity_check(channels))) {
+            log_module(MAIN_LOG, LOG_ERROR, "Channels insanity: %s", msg);
+            free(msg);
+        }
+        if ((msg = dict_sanity_check(servers))) {
+            log_module(MAIN_LOG, LOG_ERROR, "Servers insanity: %s", msg);
+            free(msg);
+        }
+    } else {
+        srand(time(&now));
+        ioset_run();
+    }
+    return 0;
+}
diff --git a/src/md5.c b/src/md5.c
new file mode 100644 (file)
index 0000000..189d45d
--- /dev/null
+++ b/src/md5.c
@@ -0,0 +1,373 @@
+/* MD5C.C - RSA Data Security, Inc., MD5 message-digest algorithm */
+
+/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
+rights reserved.
+
+License to copy and use this software is granted provided that it
+is identified as the "RSA Data Security, Inc. MD5 Message-Digest
+Algorithm" in all material mentioning or referencing this software
+or this function.
+
+License is also granted to make and use derivative works provided
+that such works are identified as "derived from the RSA Data
+Security, Inc. MD5 Message-Digest Algorithm" in all material
+mentioning or referencing the derived work.
+
+RSA Data Security, Inc. makes no representations concerning either
+the merchantability of this software or the suitability of this
+software for any particular purpose. It is provided "as is"
+without express or implied warranty of any kind.
+
+These notices must be retained in any copies of any part of this
+documentation and/or software.
+ */
+
+#include "common.h"
+#include "md5.h"
+
+/* Constants for MD5Transform routine.
+ */
+#define S11 7
+#define S12 12
+#define S13 17
+#define S14 22
+#define S21 5
+#define S22 9
+#define S23 14
+#define S24 20
+#define S31 4
+#define S32 11
+#define S33 16
+#define S34 23
+#define S41 6
+#define S42 10
+#define S43 15
+#define S44 21
+
+static void MD5Transform PROTO_LIST ((UINT4 [4], unsigned char [64]));
+static void Encode PROTO_LIST
+  ((unsigned char *, UINT4 *, unsigned int));
+static void Decode PROTO_LIST
+  ((UINT4 *, unsigned char *, unsigned int));
+
+static unsigned char PADDING[64] = {
+  0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+};
+
+/* F, G, H and I are basic MD5 functions.
+ */
+#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
+#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define I(x, y, z) ((y) ^ ((x) | (~z)))
+
+/* ROTATE_LEFT rotates x left n bits. */
+#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n))))
+
+/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4.
+Rotation is separate from addition to prevent recomputation.
+ */
+#define FF(a, b, c, d, x, s, ac) { \
+ (a) += F ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+  }
+#define GG(a, b, c, d, x, s, ac) { \
+ (a) += G ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+  }
+#define HH(a, b, c, d, x, s, ac) { \
+ (a) += H ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+  }
+#define II(a, b, c, d, x, s, ac) { \
+ (a) += I ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+  }
+
+/* MD5 initialization. Begins an MD5 operation, writing a new context. */
+void MD5Init (context)
+MD5_CTX *context;                                        /* context */
+{
+  context->count[0] = context->count[1] = 0;
+  /* Load magic initialization constants. */
+  context->state[0] = 0x67452301;
+  context->state[1] = 0xefcdab89;
+  context->state[2] = 0x98badcfe;
+  context->state[3] = 0x10325476;
+}
+
+/* MD5 block update operation. Continues an MD5 message-digest
+  operation, processing another message block, and updating the
+  context.
+ */
+void MD5Update (context, input, inputLen)
+MD5_CTX *context;                                        /* context */
+unsigned char *input;                                /* input block */
+unsigned int inputLen;                     /* length of input block */
+{
+  unsigned int i, index, partLen;
+
+  /* Compute number of bytes mod 64 */
+  index = (unsigned int)((context->count[0] >> 3) & 0x3F);
+
+  /* Update number of bits */
+  if ((context->count[0] += ((UINT4)inputLen << 3))
+      < ((UINT4)inputLen << 3))
+      context->count[1]++;
+  context->count[1] += ((UINT4)inputLen >> 29);
+
+  partLen = 64 - index;
+
+  /* Transform as many times as possible. */
+  if (inputLen >= partLen) {
+      memcpy((POINTER)&context->buffer[index], (POINTER)input, partLen);
+      MD5Transform (context->state, context->buffer);
+
+      for (i = partLen; i + 63 < inputLen; i += 64)
+         MD5Transform (context->state, &input[i]);
+
+      index = 0;
+  }
+  else
+    i = 0;
+
+  /* Buffer remaining input */
+  memcpy((POINTER)&context->buffer[index], (POINTER)&input[i],
+  inputLen-i);
+}
+
+/* MD5 finalization. Ends an MD5 message-digest operation, writing the
+  the message digest and zeroizing the context.
+ */
+void MD5Final (digest, context)
+unsigned char digest[16];                         /* message digest */
+MD5_CTX *context;                                       /* context */
+{
+  unsigned char bits[8];
+  unsigned int index, padLen;
+
+  /* Save number of bits */
+  Encode (bits, context->count, 8);
+
+  /* Pad out to 56 mod 64. */
+  index = (unsigned int)((context->count[0] >> 3) & 0x3f);
+  padLen = (index < 56) ? (56 - index) : (120 - index);
+  MD5Update (context, PADDING, padLen);
+
+  /* Append length (before padding) */
+  MD5Update (context, bits, 8);
+
+  /* Store state in digest */
+  Encode (digest, context->state, 16);
+
+  /* Zeroize sensitive information. */
+  memset ((POINTER)context, 0, sizeof (*context));
+}
+
+/* MD5 basic transformation. Transforms state based on block. */
+static void MD5Transform (state, block)
+UINT4 state[4];
+unsigned char block[64];
+{
+  UINT4 a = state[0], b = state[1], c = state[2], d = state[3], x[16];
+
+  Decode (x, block, 64);
+
+  /* Round 1 */
+  FF (a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */
+  FF (d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */
+  FF (c, d, a, b, x[ 2], S13, 0x242070db); /* 3 */
+  FF (b, c, d, a, x[ 3], S14, 0xc1bdceee); /* 4 */
+  FF (a, b, c, d, x[ 4], S11, 0xf57c0faf); /* 5 */
+  FF (d, a, b, c, x[ 5], S12, 0x4787c62a); /* 6 */
+  FF (c, d, a, b, x[ 6], S13, 0xa8304613); /* 7 */
+  FF (b, c, d, a, x[ 7], S14, 0xfd469501); /* 8 */
+  FF (a, b, c, d, x[ 8], S11, 0x698098d8); /* 9 */
+  FF (d, a, b, c, x[ 9], S12, 0x8b44f7af); /* 10 */
+  FF (c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */
+  FF (b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */
+  FF (a, b, c, d, x[12], S11, 0x6b901122); /* 13 */
+  FF (d, a, b, c, x[13], S12, 0xfd987193); /* 14 */
+  FF (c, d, a, b, x[14], S13, 0xa679438e); /* 15 */
+  FF (b, c, d, a, x[15], S14, 0x49b40821); /* 16 */
+
+ /* Round 2 */
+  GG (a, b, c, d, x[ 1], S21, 0xf61e2562); /* 17 */
+  GG (d, a, b, c, x[ 6], S22, 0xc040b340); /* 18 */
+  GG (c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */
+  GG (b, c, d, a, x[ 0], S24, 0xe9b6c7aa); /* 20 */
+  GG (a, b, c, d, x[ 5], S21, 0xd62f105d); /* 21 */
+  GG (d, a, b, c, x[10], S22,  0x2441453); /* 22 */
+  GG (c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */
+  GG (b, c, d, a, x[ 4], S24, 0xe7d3fbc8); /* 24 */
+  GG (a, b, c, d, x[ 9], S21, 0x21e1cde6); /* 25 */
+  GG (d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */
+  GG (c, d, a, b, x[ 3], S23, 0xf4d50d87); /* 27 */
+  GG (b, c, d, a, x[ 8], S24, 0x455a14ed); /* 28 */
+  GG (a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */
+  GG (d, a, b, c, x[ 2], S22, 0xfcefa3f8); /* 30 */
+  GG (c, d, a, b, x[ 7], S23, 0x676f02d9); /* 31 */
+  GG (b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */
+
+  /* Round 3 */
+  HH (a, b, c, d, x[ 5], S31, 0xfffa3942); /* 33 */
+  HH (d, a, b, c, x[ 8], S32, 0x8771f681); /* 34 */
+  HH (c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */
+  HH (b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */
+  HH (a, b, c, d, x[ 1], S31, 0xa4beea44); /* 37 */
+  HH (d, a, b, c, x[ 4], S32, 0x4bdecfa9); /* 38 */
+  HH (c, d, a, b, x[ 7], S33, 0xf6bb4b60); /* 39 */
+  HH (b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */
+  HH (a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */
+  HH (d, a, b, c, x[ 0], S32, 0xeaa127fa); /* 42 */
+  HH (c, d, a, b, x[ 3], S33, 0xd4ef3085); /* 43 */
+  HH (b, c, d, a, x[ 6], S34,  0x4881d05); /* 44 */
+  HH (a, b, c, d, x[ 9], S31, 0xd9d4d039); /* 45 */
+  HH (d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */
+  HH (c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */
+  HH (b, c, d, a, x[ 2], S34, 0xc4ac5665); /* 48 */
+
+  /* Round 4 */
+  II (a, b, c, d, x[ 0], S41, 0xf4292244); /* 49 */
+  II (d, a, b, c, x[ 7], S42, 0x432aff97); /* 50 */
+  II (c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */
+  II (b, c, d, a, x[ 5], S44, 0xfc93a039); /* 52 */
+  II (a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */
+  II (d, a, b, c, x[ 3], S42, 0x8f0ccc92); /* 54 */
+  II (c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */
+  II (b, c, d, a, x[ 1], S44, 0x85845dd1); /* 56 */
+  II (a, b, c, d, x[ 8], S41, 0x6fa87e4f); /* 57 */
+  II (d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */
+  II (c, d, a, b, x[ 6], S43, 0xa3014314); /* 59 */
+  II (b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */
+  II (a, b, c, d, x[ 4], S41, 0xf7537e82); /* 61 */
+  II (d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */
+  II (c, d, a, b, x[ 2], S43, 0x2ad7d2bb); /* 63 */
+  II (b, c, d, a, x[ 9], S44, 0xeb86d391); /* 64 */
+
+  state[0] += a;
+  state[1] += b;
+  state[2] += c;
+  state[3] += d;
+
+  /* Zeroize sensitive information. */
+  memset ((POINTER)x, 0, sizeof (x));
+}
+
+/* Encodes input (UINT4) into output (unsigned char). Assumes len is
+  a multiple of 4.
+ */
+static void Encode (output, input, len)
+unsigned char *output;
+UINT4 *input;
+unsigned int len;
+{
+  unsigned int i, j;
+
+  for (i = 0, j = 0; j < len; i++, j += 4) {
+    output[j] = (unsigned char)(input[i] & 0xff);
+    output[j+1] = (unsigned char)((input[i] >> 8) & 0xff);
+    output[j+2] = (unsigned char)((input[i] >> 16) & 0xff);
+    output[j+3] = (unsigned char)((input[i] >> 24) & 0xff);
+  }
+}
+
+/* Decodes input (unsigned char) into output (UINT4). Assumes len is
+  a multiple of 4.
+ */
+static void Decode (output, input, len)
+UINT4 *output;
+unsigned char *input;
+unsigned int len;
+{
+  unsigned int i, j;
+
+  for (i = 0, j = 0; j < len; i++, j += 4)
+    output[i] = ((UINT4)input[j]) | (((UINT4)input[j+1]) << 8) |
+      (((UINT4)input[j+2]) << 16) | (((UINT4)input[j+3]) << 24);
+}
+
+static const char *
+cryptpass_real(const char *pass, char *buffer, int seed)
+{
+    static const char hex_digits[] = "0123456789ABCDEF";
+    MD5_CTX ctx;
+    unsigned int len, i, j;
+    unsigned char temp[64], buff[16];
+
+    /* first: generate seed */
+    if (seed) {
+        for (i=j=0; j<4; j++) {
+            temp[i++] = hex_digits[(seed >> (28 - 8*j)) & 15];
+            temp[i++] = hex_digits[(seed >> (24 - 8*j)) & 15];
+        }
+        /* fill temp buffer with password, 1 and then 0's */
+        j = strlen(pass);
+        if ((sizeof(temp) - i) < j) j = sizeof(temp) - i;
+        memcpy(temp+i, pass, j);
+        i += j;
+        if (i < sizeof(temp)) temp[i++] = '1';
+        while (i < sizeof(temp)) temp[i++] = '0';
+    } else {
+        i = 0;
+        /* next: duplicate password to fill temp buffer */
+        len = strlen(pass);
+        for (j=0; i<sizeof(temp); i++) {
+            temp[i] = pass[j];
+            if (++j == len) j = 0;
+        }
+    }
+    /* next: compute MD5 digest of repeated password */
+    MD5Init(&ctx);
+    MD5Update(&ctx, temp, sizeof(temp));
+    MD5Final(buff, &ctx);
+    /* finally: write digest to output buffer */
+    if (seed) {
+        buffer[0] = '$';
+        for (i=0,j=1; i<4; i++) {
+            buffer[j++] = hex_digits[(seed >> (28 - 8*i)) & 15];
+            buffer[j++] = hex_digits[(seed >> (24 - 8*i)) & 15];
+        }
+    } else {
+        j = 0;
+    }
+    for (i=0; i<sizeof(buff); i++) {
+       buffer[j++] = hex_digits[buff[i] >> 4];
+       buffer[j++] = hex_digits[buff[i] & 15];
+    }
+    buffer[j] = 0;
+    return buffer;
+}
+
+const char *
+cryptpass(const char *pass, char *buffer)
+{
+    int seed;
+    do { seed = rand(); } while (!seed);
+    return cryptpass_real(pass, buffer, seed);
+}
+
+int
+checkpass(const char *pass, const char *crypt)
+{
+    char new_crypt[MD5_CRYPT_LENGTH], hseed[9];
+    int seed;
+
+    if (crypt[0] == '$') {
+        /* new-style crypt, use "seed" after '$' */
+        strncpy(hseed, crypt+1, 8);
+        hseed[8] = 0;
+        seed = strtoul(hseed, NULL, 16);
+    } else {
+        /* old-style crypt, use "seed" of 0 */
+        seed = 0;
+    }
+    cryptpass_real(pass, new_crypt, seed);
+    return !strcmp(crypt, new_crypt);
+}
diff --git a/src/md5.h b/src/md5.h
new file mode 100644 (file)
index 0000000..4febb69
--- /dev/null
+++ b/src/md5.h
@@ -0,0 +1,69 @@
+/* GLOBAL.H - RSAREF types and constants */
+
+/* PROTOTYPES should be set to one if and only if the compiler supports
+  function argument prototyping.
+  The following makes PROTOTYPES default to 0 if it has not already
+  been defined with C compiler flags.
+ */
+#ifndef PROTOTYPES
+#define PROTOTYPES 1
+#endif
+
+/* POINTER defines a generic pointer type */
+typedef unsigned char *POINTER;
+
+/* UINT2 defines a two byte word */
+typedef unsigned short int UINT2;
+
+/* UINT4 defines a four byte word */
+#if defined(__alpha)
+typedef unsigned int UINT4;
+#else
+typedef unsigned long int UINT4;
+#endif
+
+/* PROTO_LIST is defined depending on how PROTOTYPES is defined above.
+   If using PROTOTYPES, then PROTO_LIST returns the list, otherwise it
+  returns an empty list.
+ */
+#if PROTOTYPES
+#define PROTO_LIST(list) list
+#else
+#define PROTO_LIST(list) ()
+#endif
+
+/* MD5.H - header file for MD5C.C */
+
+/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
+rights reserved.
+
+License to copy and use this software is granted provided that it
+is identified as the "RSA Data Security, Inc. MD5 Message-Digest
+Algorithm" in all material mentioning or referencing this software
+or this function.
+
+License is also granted to make and use derivative works provided
+that such works are identified as "derived from the RSA Data
+Security, Inc. MD5 Message-Digest Algorithm" in all material
+mentioning or referencing the derived work.
+
+RSA Data Security, Inc. makes no representations concerning either
+the merchantability of this software or the suitability of this
+software for any particular purpose. It is provided "as is"
+without express or implied warranty of any kind.
+
+These notices must be retained in any copies of any part of this
+documentation and/or software.
+ */
+
+/* MD5 context. */
+typedef struct {
+  UINT4 state[4];                                   /* state (ABCD) */
+  UINT4 count[2];        /* number of bits, modulo 2^64 (lsb first) */
+  unsigned char buffer[64];                         /* input buffer */
+} MD5_CTX;
+
+void MD5Init PROTO_LIST ((MD5_CTX *));
+void MD5Update PROTO_LIST ((MD5_CTX *, unsigned char *, unsigned int));
+void MD5Final PROTO_LIST ((unsigned char [16], MD5_CTX *));
+
diff --git a/src/mod-helpserv.c b/src/mod-helpserv.c
new file mode 100644 (file)
index 0000000..145acd2
--- /dev/null
@@ -0,0 +1,4580 @@
+/* helpserv.c - Support Helper assistant service
+ * Copyright 2002-2003 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+/* Wishlist for helpserv.c
+ * - Make HelpServ send unassigned request count to helpers as they join the
+ *   channel
+ * - FAQ responses
+ * - Get cmd_statsreport to sort by time and show requests closed
+ * - .. then make statsreport show weighted combination of time and requests closed :)
+ * - Something for users to use a subset of commands... like closing their
+ *   own request(s), viewing what they've sent so far, and finding their
+ *   helper's nick.
+ * - It would be nice to have some variable expansions for the custom
+ *   request open/close/etc messages. ${request/id}, etc
+ * - Allow manipulation of the persist types on a per-request basis... so
+ *   if it's normally set to close requests after a user quits, but there
+ *   is a long-term issue, then that single request can remain
+ * - Bot suspension
+ */
+
+#include "conf.h"
+#include "global.h"
+#include "modcmd.h"
+#include "nickserv.h"
+#include "opserv.h"
+#include "saxdb.h"
+#include "timeq.h"
+
+#define HELPSERV_CONF_NAME "modules/helpserv"
+#define HELPSERV_HELPFILE_NAME "mod-helpserv.help"
+const char *helpserv_module_deps[] = { NULL };
+
+/* db entries */
+#define KEY_BOTS "bots"
+#define KEY_LAST_STATS_UPDATE "last_stats_update"
+#define KEY_NICK "nick"
+#define KEY_DB_BADCHANS "badchans"
+#define KEY_HELP_CHANNEL "help_channel"
+#define KEY_PAGE_DEST "page_dest"
+#define KEY_CMDWORD "cmdword"
+#define KEY_PERSIST_LENGTH "persist_length"
+#define KEY_REGISTERED "registered"
+#define KEY_REGISTRAR "registrar"
+#define KEY_IDWRAP "id_wrap"
+#define KEY_REQ_MAXLEN "req_maxlen"
+#define KEY_LAST_REQUESTID "last_requestid"
+#define KEY_HELPERS "users"
+#define KEY_HELPER_LEVEL "level"
+#define KEY_HELPER_HELPMODE "helpmode"
+#define KEY_HELPER_WEEKSTART "weekstart"
+#define KEY_HELPER_STATS "stats"
+#define KEY_HELPER_STATS_TIME "time"
+#define KEY_HELPER_STATS_PICKUP "picked_up"
+#define KEY_HELPER_STATS_CLOSE "closed"
+#define KEY_HELPER_STATS_REASSIGNFROM "reassign_from"
+#define KEY_HELPER_STATS_REASSIGNTO "reassign_to"
+#define KEY_REQUESTS "requests"
+#define KEY_REQUEST_HELPER "helper"
+#define KEY_REQUEST_ASSIGNED "assigned"
+#define KEY_REQUEST_HANDLE "handle"
+#define KEY_REQUEST_TEXT "text"
+#define KEY_REQUEST_OPENED "opened"
+#define KEY_REQUEST_NICK "nick"
+#define KEY_REQUEST_USERHOST "userhost"
+#define KEY_REQUEST_CLOSED "closed"
+#define KEY_REQUEST_CLOSEREASON "closereason"
+#define KEY_NOTIFICATION "notification"
+#define KEY_PRIVMSG_ONLY "privmsg_only"
+#define KEY_REQ_ON_JOIN "req_on_join"
+#define KEY_AUTO_VOICE "auto_voice"
+#define KEY_AUTO_DEVOICE "auto_devoice"
+#define KEY_LAST_ACTIVE "last_active"
+
+/* General */
+#define HSFMT_TIME               "%a, %d %b %Y %H:%M:%S %Z"
+static const struct message_entry msgtab[] = {
+    { "HSMSG_READHELP_SUCCESS", "Read HelpServ help database in "FMT_TIME_T".%03ld seconds." },
+    { "HSMSG_INVALID_BOT", "This command requires a valid HelpServ bot name." },
+    { "HSMSG_ILLEGAL_CHANNEL", "$b%s$b is an illegal channel; cannot use it." },
+    { "HSMSG_INTERNAL_COMMAND", "$b%s$b appears to be an internal HelpServ command, sorry." },
+    { "HSMSG_NOT_IN_USERLIST", "%s lacks access to $b%s$b." },
+    { "HSMSG_LEVEL_TOO_LOW", "You lack access to this command." },
+    { "HSMSG_OPER_CMD", "This command can only be executed via $O." },
+    { "HSMSG_WTF_WHO_ARE_YOU", "$bUnable to find you on the %s userlist.$b This is a bug. Please report it." },
+    { "HSMSG_NO_USE_OPSERV", "This command cannot be used via $O. If you really need to use it, add yourself to the userlist." },
+    { "HSMSG_OPSERV_NEED_USER", "To use this command via $O, you must supply a user to target." },
+    { "HSMSG_PAGE_REQUEST", "Page from $b%s$b: $b%s$b" },
+    { "HSMSG_BAD_REQ_TYPE", "I don't know how to list $b%s$b requests." },
+
+/* Greetings */
+    { "HSMSG_GREET_EXISTING_REQ", "Welcome back to %s. You have already opened request ID#%lu. Any messages you send to $S will be appended to that request." },
+    { "HSMSG_GREET_PRIVMSG_EXISTREQ", "Hello again. Your account has a previously opened request ID#%lu. This message and any further messages you send to $S will be appended to that request." },
+
+/* User Management */
+    { "HSMSG_CANNOT_ADD", "You do not have permission to add users." },
+    { "HSMSG_CANNOT_DEL", "You do not have permission to delete users." },
+    { "HSMSG_CANNOT_CLVL", "You do not have permission to modify users' access." },
+    { "HSMSG_NO_SELF_CLVL", "You cannot change your own access." },
+    { "HSMSG_NO_BUMP_ACCESS", "You cannot give users the same or more access than yourself." },
+    { "HSMSG_NO_TRANSFER_SELF", "You cannot give ownership to your own account." },
+    { "HSMSG_ADDED_USER", "Added new $b%s$b %s to the user list." },
+    { "HSMSG_DELETED_USER", "Deleted $b%s$b %s from the user list." },
+    { "HSMSG_USER_EXISTS", "$b%s$b is already on the user list." },
+    { "HSMSG_INVALID_ACCESS", "$b%s$b is an invalid access level." },
+    { "HSMSG_CHANGED_ACCESS", "%s now has $b%s$b access." },
+    { "HSMSG_EXPIRATION_DONE", "%d eligible HelpServ bots have retired." },
+    { "HSMSG_BAD_WEEKDAY", "I do not know which day of the week $b%s$b is." },
+    { "HSMSG_WEEK_STARTS", "$b%s$b's weeks start on $b%s$b." },
+
+/* Registration */
+    { "HSMSG_ILLEGAL_NICK", "$b%s$b is an illegal nick; cannot use it." },
+    { "HSMSG_NICK_EXISTS", "The nick %s is in use by someone else." },
+    { "HSMSG_REG_SUCCESS", "%s now has ownership of bot %s." },
+    { "HSMSG_NEED_UNREG_CONFIRM", "To unregister this bot, you must /msg $S unregister CONFIRM" },
+    { "HSMSG_ERROR_ADDING_SERVICE", "Error creating new user $b%s$b." },
+
+/* Rename */
+    { "HSMSG_RENAMED", "%s has been renamed to $b%s$b." },
+    { "HSMSG_MOVE_SAME_CHANNEL", "You cannot move %s to the same channel it is on." },
+    { "HSMSG_INVALID_MOVE", "$b%s$b is not a valid nick or channel name." },
+    { "HSMSG_NEED_GIVEOWNERSHIP_CONFIRM", "To transfer ownership of this bot, you must /msg $S giveownership newowner CONFIRM" },
+    { "HSMSG_MULTIPLE_OWNERS", "There is more than one owner of %s; please use other commands to change ownership." },
+    { "HSMSG_NO_TRANSFER_SELF", "You cannot give ownership to your own account." },
+    { "HSMSG_OWNERSHIP_GIVEN", "Ownership of $b%s$b has been transferred to account $b%s$b." },
+
+/* Queue settings */
+    { "HSMSG_INVALID_OPTION", "$b%s$b is not a valid option." },
+    { "HSMSG_QUEUE_OPTIONS", "HelpServ Queue Options:" },
+    { "HSMSG_SET_COMMAND_TYPE",   "$bPageType        $b %s" },
+    { "HSMSG_SET_ALERT_TYPE",     "$bAlertPageType   $b %s" },
+    { "HSMSG_SET_STATUS_TYPE",    "$bStatusPageType  $b %s" },
+    { "HSMSG_SET_COMMAND_TARGET", "$bPageTarget      $b %s" },
+    { "HSMSG_SET_ALERT_TARGET",   "$bAlertPageTarget $b %s" },
+    { "HSMSG_SET_STATUS_TARGET",  "$bStatusPageTarget$b %s" },
+    { "HSMSG_SET_GREETING",       "$bGreeting        $b %s" },
+    { "HSMSG_SET_REQOPENED",      "$bReqOpened       $b %s" },
+    { "HSMSG_SET_REQASSIGNED",    "$bReqAssigned     $b %s" },
+    { "HSMSG_SET_REQCLOSED",      "$bReqClosed       $b %s" },
+    { "HSMSG_SET_IDLEDELAY",      "$bIdleDelay       $b %s" },
+    { "HSMSG_SET_WHINEDELAY",     "$bWhineDelay      $b %s" },
+    { "HSMSG_SET_WHINEINTERVAL",  "$bWhineInterval   $b %s" },
+    { "HSMSG_SET_EMPTYINTERVAL",  "$bEmptyInterval   $b %s" },
+    { "HSMSG_SET_STALEDELAY",     "$bStaleDelay      $b %s" },
+    { "HSMSG_SET_REQPERSIST",     "$bReqPersist      $b %s" },
+    { "HSMSG_SET_HELPERPERSIST",  "$bHelperPersist   $b %s" },
+    { "HSMSG_SET_NOTIFICATION",   "$bNotification    $b %s" },
+    { "HSMSG_SET_IDWRAP",         "$bIDWrap          $b %d" },
+    { "HSMSG_SET_REQMAXLEN",      "$bReqMaxLen       $b %d" },
+    { "HSMSG_SET_PRIVMSGONLY",    "$bPrivmsgOnly     $b %s" },
+    { "HSMSG_SET_REQONJOIN",      "$bReqOnJoin       $b %s" },
+    { "HSMSG_SET_AUTOVOICE",      "$bAutoVoice       $b %s" },
+    { "HSMSG_SET_AUTODEVOICE",    "$bAutoDevoice     $b %s" },
+    { "HSMSG_PAGE_NOTICE", "notice" },
+    { "HSMSG_PAGE_PRIVMSG", "privmsg" },
+    { "HSMSG_PAGE_ONOTICE", "onotice" },
+    { "HSMSG_LENGTH_PART", "part" },
+    { "HSMSG_LENGTH_QUIT", "quit" },
+    { "HSMSG_LENGTH_CLOSE", "close" },
+    { "HSMSG_NOTIFY_DROP", "ReqDrop" },
+    { "HSMSG_NOTIFY_USERCHANGES", "UserChanges" },
+    { "HSMSG_NOTIFY_ACCOUNTCHANGES", "AccountChanges" },
+    { "HSMSG_INVALID_INTERVAL", "Sorry, %s must be at least %s." },
+    { "HSMSG_0_DISABLED", "0 (Disabled)" },
+    { "HSMSG_NEED_MANAGER", "Only managers or higher can do this." },
+    { "HSMSG_SET_NEED_OPER", "This option can only be set by an oper." },
+
+/* Requests */
+    { "HSMSG_REQ_INVALID", "$b%s$b is not a valid request ID, or there are no requests for that nick or account." },
+    { "HSMSG_REQ_NOT_YOURS_ASSIGNED_TO", "Request $b%lu$b is not assigned to you; it is currently assigned to %s." },
+    { "HSMSG_REQ_NOT_YOURS_UNASSIGNED", "Request $b%lu$b is not assigned to you; it is currently unassigned." },
+    { "HSMSG_REQ_FOUNDMANY", "The user you entered had multiple requests. The oldest one is being used." },
+    { "HSMSG_REQ_CLOSED", "Request $b%lu$b has been closed." },
+    { "HSMSG_REQ_NO_UNASSIGNED", "There are no unassigned requests." },
+    { "HSMSG_USERCMD_NO_REQUEST", "You must have an open request to use a user command." },
+    { "HSMSG_USERCMD_UNKNOWN", "I do not know the user command $b%s$b." },
+    { "HSMSG_REQ_YOU_NOT_IN_HELPCHAN_OPEN", "You cannot open this request as you are not in %s." },
+    { "HSMSG_REQ_YOU_NOT_IN_HELPCHAN", "You cannot be assigned this request as you are not in %s." },
+    { "HSMSG_REQ_HIM_NOT_IN_HELPCHAN", "%s cannot be assigned this request as they are not in %s." },
+    { "HSMSG_REQ_SELF_NOT_IN_OVERRIDE", "You are being assigned this request even though you are not in %s, because the restriction was overridden (you are a manager or owner). If you join and then part, this request will be marked unassigned." },
+    { "HSMSG_REQ_YOU_NOT_IN_OVERRIDE", "Note: You are being assigned this request even though you are not in %s, because the restriction was overridden by a manager or owner. If you join and then part, this request will be marked unassigned." },
+    { "HSMSG_REQ_HIM_NOT_IN_OVERRIDE", "Note: %s is being assigned this request even though they are not in %s, because the restriction was overridden (you are a manager or owner). If they join and then part, this request will be marked unassigned." },
+    { "HSMSG_REQ_ASSIGNED_YOU", "You have been assigned request ID#%lu:" },
+    { "HSMSG_REQ_REASSIGNED", "You have been assigned request ID#%lu (reassigned from %s):" },
+    { "HSMSG_REQ_INFO_1", "Request ID#%lu:" },
+    { "HSMSG_REQ_INFO_2a", " - Nick %s / Account %s" },
+    { "HSMSG_REQ_INFO_2b", " - Nick %s / Not authed" },
+    { "HSMSG_REQ_INFO_2c", " - Online somewhere / Account %s" },
+    { "HSMSG_REQ_INFO_2d", " - Not online / Account %s" },
+    { "HSMSG_REQ_INFO_2e", " - Not online / No account" },
+    { "HSMSG_REQ_INFO_3", " - Opened at %s (%s ago)" },
+    { "HSMSG_REQ_INFO_4", " - Message:" },
+    { "HSMSG_REQ_INFO_MESSAGE", "   %s" },
+    { "HSMSG_REQ_ASSIGNED", "Your helper for request ID#%lu is %s (Current nick: %s)" },
+    { "HSMSG_REQ_ASSIGNED_AGAIN", "Your helper for request ID#%lu has been changed to %s (Current nick: %s)" },
+    { "HSMSG_REQ_UNASSIGNED", "Your helper for request ID#%lu has %s, so your request is no longer being handled by them. It has been placed near the front of the unhandled request queue, based on how long ago your request was opened." },
+    { "HSMSG_REQ_NEW", "Your message has been recorded and assigned request ID#%lu. A helper should contact you shortly." },
+    { "HSMSG_REQ_NEWONJOIN", "Welcome to %s. You have been assigned request ID#%lu. A helper should contact you shortly." },
+    { "HSMSG_REQ_UNHANDLED_TIME", "The oldest unhandled request has been waiting for %s." },
+    { "HSMSG_REQ_NO_UNHANDLED", "There are no other unhandled requests." },
+    { "HSMSG_REQ_PERSIST_QUIT", "Everything you tell me until you are helped (or you quit) will be recorded. If you disconnect, your request will be lost." },
+    { "HSMSG_REQ_PERSIST_PART", "Everything you tell me until you are helped (or you leave %s) will be recorded. If you part %s, your request will be lost." },
+    { "HSMSG_REQ_PERSIST_HANDLE", "Everything you tell me until you are helped will be recorded." },
+    { "HSMSG_REQ_MAXLEN", "Sorry, but your request has reached the maximum number of lines. Please wait to be assigned to a helper and continue explaining your request to them." },
+    { "HSMSG_REQ_FOUND_ANOTHER", "Request ID#%lu has been closed. $S detected that you also have request ID#%lu open. If you send $S a message, it will be associated with that request." },
+
+/* Messages that are inserted into request text */
+    { "HSMSG_REQMSG_NOTE_ADDED", "Your note for request ID#%lu has been recorded." },
+
+/* Automatically generated page messages */
+    { "HSMSG_PAGE_NEW_REQUEST_AUTHED", "New request (ID#%lu) from $b%s$b (Account %s)" },
+    { "HSMSG_PAGE_NEW_REQUEST_UNAUTHED", "New request (ID#%lu) from $b%s$b (Not logged in)" },
+    { "HSMSG_PAGE_UPD_REQUEST_AUTHED", "Request ID#%lu has been updated by $b%s$b (Account %s). Request was initially opened at %s, and was last updated %s ago." },
+    { "HSMSG_PAGE_UPD_REQUEST_NOT_AUTHED", "Request ID#%lu has been updated by $b%s$b (not authed). Request was initially opened at %s, and was last updated %s ago." },
+    { "HSMSG_PAGE_CLOSE_REQUEST_1", "Request ID#%lu from $b%s$b (Account %s) has been closed by %s." },
+    { "HSMSG_PAGE_CLOSE_REQUEST_2", "Request ID#%lu from $b%s$b (Not authed) has been closed by %s." },
+    { "HSMSG_PAGE_CLOSE_REQUEST_3", "Request ID#%lu from an offline user (Account %s) has been closed by %s." },
+    { "HSMSG_PAGE_CLOSE_REQUEST_4", "Request ID#%lu from an offline user (no account) has been closed by %s." },
+    { "HSMSG_PAGE_ASSIGN_REQUEST_1", "Request ID#%lu from $b%s$b (Account %s) has been assigned to %s." },
+    { "HSMSG_PAGE_ASSIGN_REQUEST_2", "Request ID#%lu from $b%s$b (Not authed) has been assigned to %s." },
+    { "HSMSG_PAGE_ASSIGN_REQUEST_3", "Request ID#%lu from an offline user (Account %s) has been assigned to %s." },
+    { "HSMSG_PAGE_ASSIGN_REQUEST_4", "Request ID#%lu from an offline user (no account) has been assigned to %s." },
+    /* The last %s is still an I18N lose.  Blame whoever decided to overload it so much. */
+    { "HSMSG_PAGE_HELPER_GONE_1", "Request ID#%lu from $b%s$b (Account %s) $bhas been unassigned$b, as its helper, %s has %s." },
+    { "HSMSG_PAGE_HELPER_GONE_2", "Request ID#%lu from $b%s$b (Not authed) $bhas been unassigned$b, as its helper, %s has %s." },
+    { "HSMSG_PAGE_HELPER_GONE_3", "Request ID#%lu from an offline user (Account %s) $bhas been unassigned$b, as its helper, %s has %s." },
+    { "HSMSG_PAGE_HELPER_GONE_4", "Request ID#%lu from an offline user (No account) $bhas been unassigned$b, as its helper, %s has %s." },
+    { "HSMSG_PAGE_WHINE_HEADER", "$b%u unhandled request(s)$b waiting at least $b%s$b (%u total)" },
+    { "HSMSG_PAGE_IDLE_HEADER", "$b%u users$b in %s $bidle at least %s$b:" },
+    { "HSMSG_PAGE_EMPTYALERT", "$b%s has no helpers present (%u unhandled request(s))$b" },
+    { "HSMSG_PAGE_ONLYTRIALALERT", "$b%s has no full helpers present (%d trial(s) present; %u unhandled request(s))$b" },
+    { "HSMSG_PAGE_FIRSTEMPTYALERT", "$b%s has no helpers present because %s has left (%u unhandled request(s))$b" },
+    { "HSMSG_PAGE_FIRSTONLYTRIALALERT", "$b%s has no full helpers present because %s has left (%d trial(s) present; %u unhandled request(s))$b" },
+    { "HSMSG_PAGE_EMPTYNOMORE", "%s has joined %s; cancelling the \"no helpers present\" alert" },
+
+/* Notification messages */
+    { "HSMSG_NOTIFY_USER_QUIT", "The user for request ID#%lu, $b%s$b, has disconnected." },
+    { "HSMSG_NOTIFY_USER_MOVE", "The account for request ID#%lu, $b%s$b has been unregistered. It has been associated with user $b%s$b." },
+    { "HSMSG_NOTIFY_USER_NICK", "The user for request ID#%lu has changed their nick from $b%s$b to $b%s$b." },
+    { "HSMSG_NOTIFY_USER_FOUND", "The user for request ID#%lu is now online using nick $b%s$b." },
+    { "HSMSG_NOTIFY_HAND_RENAME", "The account for request ID#%lu has been renamed from $b%s$b to $b%s$b." },
+    { "HSMSG_NOTIFY_HAND_MOVE", "The account for request ID#%lu has been changed to $b%s$b from $b%s$b." },
+    { "HSMSG_NOTIFY_HAND_STUCK", "The user for request ID#%lu, $b%s$b, has re-authenticated to account $b%s$b from $b%s$b, and the request remained associated with the old handle." },
+    { "HSMSG_NOTIFY_HAND_AUTH", "The user for request ID#%lu, $b%s$b, has authenticated to account $b%s$b." },
+    { "HSMSG_NOTIFY_HAND_UNREG", "The account for request ID#%lu, $b%s$b, has been unregistered by $b%s$b." },
+    { "HSMSG_NOTIFY_HAND_MERGE", "The account for request ID#%lu, $b%s$b, has been merged with $b%s$b by %s." },
+    { "HSMSG_NOTIFY_ALLOWAUTH", "The user for request ID#%lu, $b%s$b, has been permitted by %s to authenticate to account $b%s$b without hostmask checking." },
+    { "HSMSG_NOTIFY_UNALLOWAUTH", "The user for request ID#%lu, $b%s$b, has had their ability to authenticate without hostmask checking revoked by %s." },
+    { "HSMSG_NOTIFY_FAILPW", "The user for request ID#%lu, $b%s$b, has attempted to authenticate to account $b%s$b, but used an incorrect password." },
+    { "HSMSG_NOTIFY_REQ_DROP_PART", "Request ID#%lu has been $bdropped$b because %s left the help channel." },
+    { "HSMSG_NOTIFY_REQ_DROP_QUIT", "Request ID#%lu has been $bdropped$b because %s quit IRC." },
+    { "HSMSG_NOTIFY_REQ_DROP_UNREGGED", "Request ID#%lu (account %s) has been $bdropped$b because the account was unregistered." },
+
+/* Presence and request-related messages */
+    { "HSMSG_REQ_DROPPED_PART", "You have left $b%s$b. Your help request (ID#%lu) has been deleted." },
+    { "HSMSG_REQ_WARN_UNREG", "The account you were authenticated to ($b%s$b) has been unregistered. Therefore unless you register a new handle, or authenticate to another one, if you disconnect, your HelpServ $S (%s) request ID#%lu will be lost." },
+    { "HSMSG_REQ_DROPPED_UNREG", "By unregistering the account $b%s$b, HelpServ $S (%s) request ID#%lu was dropped, as there was nobody online to associate the request with." },
+    { "HSMSG_REQ_ASSIGNED_UNREG", "As the account $b%s$b was unregistered, HelpServ $S (%s) request ID#%lu was associated with you, as you were authenticated to that account. If you disconnect, then this request will be lost forever." },
+    { "HSMSG_REQ_AUTH_STUCK", "By authenticating to account $b%s$b, HelpServ $S (%s) request ID#%lu remained with the previous account $b%s$b (because the request will remain until closed). This means that if you send $S a message, it $uwill not$u be associated with this request." },
+    { "HSMSG_REQ_AUTH_MOVED", "By authenticating to account $b%s$b, HelpServ $S (%s) request ID#%lu ceased to be associated with your previously authenticated account ($b%s$b), and was transferred to your new account (because the request will only remain until you %s). This means that if you send $S a message, it $uwill$u be associated with this request." },
+
+/* Lists */
+    { "HSMSG_BOTLIST_HEADER", "$bCurrent HelpServ bots:$b" },
+    { "HSMSG_USERLIST_HEADER", "$b%s users:$b" },
+    { "HSMSG_USERLIST_ZOOT_LVL", "%s $b%ss$b:" },
+    { "HSMSG_REQLIST_AUTH", "You are currently assigned these requests:" },
+    { "HSMSG_REQ_LIST_TOP_UNASSIGNED", "Listing $ball unassigned$b requests (%d in list)." },
+    { "HSMSG_REQ_LIST_TOP_ASSIGNED", "Listing $ball assigned$b requests (%d in list)." },
+    { "HSMSG_REQ_LIST_TOP_YOUR", "Listing $byour$b requests (%d in list)." },
+    { "HSMSG_REQ_LIST_TOP_ALL", "Listing $ball$b requests (%d in list)." },
+    { "HSMSG_REQ_LIST_NONE", "There are no matching requests." },
+    { "HSMSG_STATS_TOP", "Stats for %s user $b%s$b (week starts %s):" },
+    { "HSMSG_STATS_TIME", "$uTime spent helping in %s:$u" },
+    { "HSMSG_STATS_REQS", "$uRequest activity statistics:$u" },
+
+/* Responses to user commands */
+    { "HSMSG_YOU_BEING_HELPED", "You are already being helped." },
+    { "HSMSG_YOU_BEING_HELPED_BY", "You are already being helped by $b%s$b." },
+    { "HSMSG_WAIT_STATUS", "You are %d of %d in line; the first person has waited %s." },
+    { NULL, NULL }
+};
+
+enum helpserv_level {
+    HlNone,
+    HlTrial,
+    HlHelper,
+    HlManager,
+    HlOwner,
+    HlOper,
+    HlCount
+};
+
+static const char *helpserv_level_names[] = {
+    "None",
+    "Trial",
+    "Helper",
+    "Manager",
+    "Owner",
+    "Oper",
+    NULL
+};
+
+enum page_type {
+    PAGE_NONE,
+    PAGE_NOTICE,
+    PAGE_PRIVMSG,
+    PAGE_ONOTICE,
+    PAGE_COUNT
+};
+
+static const struct {
+    char *db_name;
+    char *print_name;
+    irc_send_func func;
+} page_types[] = {
+    { "none", "MSG_NONE", NULL },
+    { "notice", "HSMSG_PAGE_NOTICE", irc_notice },
+    { "privmsg", "HSMSG_PAGE_PRIVMSG", irc_privmsg },
+    { "onotice", "HSMSG_PAGE_ONOTICE", irc_wallchops },
+    { NULL, NULL, NULL }
+};
+
+enum page_source {
+    PGSRC_COMMAND,
+    PGSRC_ALERT,
+    PGSRC_STATUS,
+    PGSRC_COUNT
+};
+
+static const struct {
+    char *db_name;
+    char *print_target;
+    char *print_type;
+} page_sources[] = {
+    { "command", "HSMSG_SET_COMMAND_TARGET", "HSMSG_SET_COMMAND_TYPE" },
+    { "alert", "HSMSG_SET_ALERT_TARGET", "HSMSG_SET_ALERT_TYPE" },
+    { "status", "HSMSG_SET_STATUS_TARGET", "HSMSG_SET_STATUS_TYPE" },
+    { NULL, NULL, NULL }
+};
+
+enum message_type {
+    MSGTYPE_GREETING,
+    MSGTYPE_REQ_OPENED,
+    MSGTYPE_REQ_ASSIGNED,
+    MSGTYPE_REQ_CLOSED,
+    MSGTYPE_REQ_DROPPED,
+    MSGTYPE_COUNT
+};
+
+static const struct {
+    char *db_name;
+    char *print_name;
+} message_types[] = {
+    { "greeting", "HSMSG_SET_GREETING" },
+    { "reqopened", "HSMSG_SET_REQOPENED" },
+    { "reqassigned", "HSMSG_SET_REQASSIGNED" },
+    { "reqclosed", "HSMSG_SET_REQCLOSED" },
+    { "reqdropped", "HSMSG_SET_REQDROPPED" },
+    { NULL, NULL }
+};
+
+enum interval_type {
+    INTERVAL_IDLE_DELAY,
+    INTERVAL_WHINE_DELAY,
+    INTERVAL_WHINE_INTERVAL,
+    INTERVAL_EMPTY_INTERVAL,
+    INTERVAL_STALE_DELAY,
+    INTERVAL_COUNT
+};
+
+static const struct {
+    char *db_name;
+    char *print_name;
+} interval_types[] = {
+    { "idledelay", "HSMSG_SET_IDLEDELAY" },
+    { "whinedelay", "HSMSG_SET_WHINEDELAY" },
+    { "whineinterval", "HSMSG_SET_WHINEINTERVAL" },
+    { "emptyinterval", "HSMSG_SET_EMPTYINTERVAL" },
+    { "staledelay", "HSMSG_SET_STALEDELAY" },
+    { NULL, NULL }
+};
+
+enum persistence_type {
+    PERSIST_T_REQUEST,
+    PERSIST_T_HELPER,
+    PERSIST_T_COUNT
+};
+
+static const struct {
+    char *db_name;
+    char *print_name;
+} persistence_types[] = {
+    { "reqpersist", "HSMSG_SET_REQPERSIST" },
+    { "helperpersist", "HSMSG_SET_HELPERPERSIST" },
+    { NULL, NULL }
+};
+
+enum persistence_length {
+    PERSIST_PART,
+    PERSIST_QUIT,
+    PERSIST_CLOSE,
+    PERSIST_COUNT
+};
+
+static const struct {
+    char *db_name;
+    char *print_name;
+} persistence_lengths[] = {
+    { "part", "HSMSG_LENGTH_PART" },
+    { "quit", "HSMSG_LENGTH_QUIT" },
+    { "close", "HSMSG_LENGTH_CLOSE" },
+    { NULL, NULL }
+};
+
+enum notification_type {
+    NOTIFY_NONE,
+    NOTIFY_DROP,
+    NOTIFY_USER,
+    NOTIFY_HANDLE,
+    NOTIFY_COUNT
+};
+
+static const struct {
+    char *db_name;
+    char *print_name;
+} notification_types[] = {
+    { "none", "MSG_NONE" },
+    { "reqdrop", "HSMSG_NOTIFY_DROP" },
+    { "userchanges", "HSMSG_NOTIFY_USERCHANGES" },
+    { "accountchanges", "HSMSG_NOTIFY_ACCOUNTCHANGES" },
+    { NULL, NULL }
+};
+
+static const char *weekday_names[] = {
+    "Sunday",
+    "Monday",
+    "Tuesday",
+    "Wednesday",
+    "Thursday",
+    "Friday",
+    "Saturday",
+    NULL
+};
+
+static const char *statsreport_week[] = {
+    "Stats report for current week",
+    "Stats report for one week ago",
+    "Stats report for two weeks ago",
+    "Stats report for three weeks ago"
+};
+
+static struct {
+    const char *description;
+    const char *reqlogfile;
+    unsigned long db_backup_frequency;
+    unsigned int expire_age;
+    char user_escape;
+} helpserv_conf;
+
+static time_t last_stats_update;
+static int shutting_down;
+static FILE *reqlog_f;
+static struct saxdb_context *reqlog_ctx;
+static struct log_type *HS_LOG;
+
+#define CMD_NEED_BOT            0x001
+#define CMD_NOT_OVERRIDE        0x002
+#define CMD_FROM_OPSERV_ONLY    0x004
+#define CMD_IGNORE_EVENT        0x008
+#define CMD_NEVER_FROM_OPSERV   0x010
+
+struct helpserv_bot {
+    struct userNode *helpserv;
+
+    struct chanNode *helpchan;
+
+    struct chanNode *page_targets[PGSRC_COUNT];
+    enum page_type page_types[PGSRC_COUNT];
+    char *messages[MSGTYPE_COUNT];
+    unsigned long intervals[INTERVAL_COUNT];
+    enum notification_type notify;
+
+    /* This is a default; it can be changed on a per-request basis */
+    enum persistence_type persist_types[PERSIST_T_COUNT];
+
+    dict_t users; /* indexed by handle */
+
+    struct helpserv_request *unhandled; /* linked list of unhandled requests */
+    dict_t requests; /* indexed by request id */
+    unsigned long last_requestid;
+    unsigned long id_wrap;
+    unsigned long req_maxlen; /* Maxmimum request length in lines */
+
+    unsigned int privmsg_only : 1;
+    unsigned int req_on_join : 1;
+    unsigned int auto_voice : 1;
+    unsigned int auto_devoice : 1;
+
+    unsigned int helpchan_empty : 1;
+
+    time_t registered;
+    time_t last_active;
+    char *registrar;
+};
+
+struct helpserv_user {
+    struct handle_info *handle;
+    struct helpserv_bot *hs;
+    unsigned int help_mode : 1;
+    unsigned int week_start : 3;
+    enum helpserv_level level;
+    /* statistics */
+    time_t join_time; /* when they joined, or 0 if not in channel */
+    /* [0] through [3] are n weeks ago, [4] is the total of everything before that */
+    unsigned int time_per_week[5]; /* how long they've were in the channel the past 4 weeks */
+    unsigned int picked_up[5]; /* how many requests they have picked up */
+    unsigned int closed[5]; /* how many requests they have closed */
+    unsigned int reassigned_from[5]; /* how many requests reassigned from them to others */
+    unsigned int reassigned_to[5]; /* how many requests reassigned from others to them */
+};
+
+struct helpserv_request {
+    struct helpserv_user *helper;
+    struct helpserv_bot *hs;
+    struct string_list *text;
+    struct helpserv_request *next_unhandled;
+
+    struct helpserv_reqlist *parent_nick_list;
+    struct helpserv_reqlist *parent_hand_list;
+
+    /* One, but not both, of "user" and "handle" may be NULL,
+     * depending on what we know about the user.
+     *
+     * If persist == PERSIST_CLOSE when the user quits, then it
+     * switches to handle instead of user... and stays that way (it's
+     * possible to have >1 nick per handle, so you can't really decide
+     * to reassign a userNode to it unless they send another message
+     * to HelpServ).
+     */
+    struct userNode *user;
+    struct handle_info *handle;
+
+    unsigned long id;
+    time_t opened;
+    time_t assigned;
+    time_t updated;
+};
+
+#define DEFINE_LIST_ALLOC(STRUCTNAME) \
+struct STRUCTNAME * STRUCTNAME##_alloc() {\
+    struct STRUCTNAME *newlist; \
+    newlist = malloc(sizeof(struct STRUCTNAME)); \
+    STRUCTNAME##_init(newlist); \
+    return newlist; \
+}\
+void STRUCTNAME##_free(void *data) {\
+    struct STRUCTNAME *list = data; /* void * to let dict_set_free_data use it */ \
+    STRUCTNAME##_clean(list); \
+    free(list); \
+}
+
+DECLARE_LIST(helpserv_botlist, struct helpserv_bot *);
+DEFINE_LIST(helpserv_botlist, struct helpserv_bot *);
+DEFINE_LIST_ALLOC(helpserv_botlist);
+
+DECLARE_LIST(helpserv_reqlist, struct helpserv_request *);
+DEFINE_LIST(helpserv_reqlist, struct helpserv_request *);
+DEFINE_LIST_ALLOC(helpserv_reqlist);
+
+DECLARE_LIST(helpserv_userlist, struct helpserv_user *);
+DEFINE_LIST(helpserv_userlist, struct helpserv_user *);
+DEFINE_LIST_ALLOC(helpserv_userlist);
+
+struct helpfile *helpserv_helpfile;
+static struct module *helpserv_module;
+static dict_t helpserv_func_dict;
+static dict_t helpserv_usercmd_dict; /* contains helpserv_usercmd_t */
+static dict_t helpserv_option_dict;
+static dict_t helpserv_bots_dict; /* indexed by nick */
+static dict_t helpserv_bots_bychan_dict; /* indexed by chan, holds a struct helpserv_botlist */
+/* QUESTION: why are these outside of any helpserv_bot struct? */
+static dict_t helpserv_reqs_bynick_dict; /* indexed by nick, holds a struct helpserv_reqlist */
+static dict_t helpserv_reqs_byhand_dict; /* indexed by handle, holds a struct helpserv_reqlist */
+static dict_t helpserv_users_byhand_dict; /* indexed by handle, holds a struct helpserv_userlist */
+
+/* This is so that overrides can "speak" from opserv */
+extern struct userNode *opserv;
+
+#define HELPSERV_SYNTAX() helpserv_help(hs, from_opserv, user, argv[0])
+#define HELPSERV_FUNC(NAME) int NAME(struct userNode *user, UNUSED_ARG(struct helpserv_bot *hs), int from_opserv, UNUSED_ARG(unsigned int argc), UNUSED_ARG(char *argv[]))
+typedef HELPSERV_FUNC(helpserv_func_t);
+#define HELPSERV_USERCMD(NAME) void NAME(struct helpserv_request *req, UNUSED_ARG(struct userNode *likely_helper), UNUSED_ARG(char *args))
+typedef HELPSERV_USERCMD(helpserv_usercmd_t);
+#define HELPSERV_OPTION(NAME) HELPSERV_FUNC(NAME)
+typedef HELPSERV_OPTION(helpserv_option_func_t);
+
+static HELPSERV_FUNC(cmd_help);
+
+#define REQUIRE_PARMS(N) if (argc < N) { \
+        helpserv_notice(user, "MSG_MISSING_PARAMS", argv[0]); \
+        HELPSERV_SYNTAX(); \
+        return 0; }
+
+/* For messages going to users being helped */
+#define helpserv_msguser(target, format...) send_message_type((from_opserv ? 0 : hs->privmsg_only), (target), (from_opserv ? opserv : hs->helpserv) , ## format)
+#define helpserv_user_reply(format...) send_message_type(req->hs->privmsg_only, req->user, req->hs->helpserv , ## format)
+/* For messages going to helpers */
+#define helpserv_notice(target, format...) send_message((target), (from_opserv ? opserv : hs->helpserv) , ## format)
+#define helpserv_notify(helper, format...) do { struct userNode *_target; for (_target = (helper)->handle->users; _target; _target = _target->next_authed) { \
+        send_message(_target, (helper)->hs->helpserv, ## format); \
+    } } while (0)
+#define helpserv_message(hs, target, id) do { if ((hs)->messages[id]) { \
+    if (from_opserv) \
+        send_message_type(4, (target), opserv, "%s", (hs)->messages[id]); \
+    else \
+        send_message_type(4 | hs->privmsg_only, (target), hs->helpserv, "%s", (hs)->messages[id]); \
+    } } while (0)
+#define helpserv_page(TYPE, FORMAT...) do { \
+    struct chanNode *target=NULL; int msg_type=0; \
+    target = hs->page_targets[TYPE]; \
+    switch (hs->page_types[TYPE]) { \
+        case PAGE_NOTICE: msg_type = 0; break; \
+        case PAGE_PRIVMSG: msg_type = 1; break; \
+        case PAGE_ONOTICE: msg_type = 2; break; \
+        default: log_module(HS_LOG, LOG_ERROR, "helpserv_page() called but %s has an invalid page type %d.", hs->helpserv->nick, TYPE); \
+        case PAGE_NONE: target = NULL; break; \
+    } \
+    if (target) send_target_message(msg_type, target->name, hs->helpserv, ## FORMAT); \
+    } while (0)
+#define helpserv_get_handle_info(user, text) smart_get_handle_info((from_opserv ? opserv : hs->helpserv) , (user), (text))
+
+struct helpserv_cmd {
+    enum helpserv_level access;
+    helpserv_func_t *func;
+    double weight;
+    long flags;
+};
+
+static void run_empty_interval(void *data);
+
+static void helpserv_interval(char *output, time_t interval) {
+    int num_hours = interval / 3600;
+    int num_minutes = (interval % 3600) / 60;
+    sprintf(output, "%u hour%s, %u minute%s", num_hours, num_hours == 1 ? "" : "s", num_minutes, num_minutes == 1 ? "" : "s");
+}
+
+static const char * helpserv_level2str(enum helpserv_level level) {
+    if (level <= HlOper) {
+        return helpserv_level_names[level];
+    } else {
+        log_module(HS_LOG, LOG_ERROR, "helpserv_level2str receieved invalid level %d.", level);
+        return "Error";
+    }
+}
+
+static enum helpserv_level helpserv_str2level(const char *msg) {
+    enum helpserv_level nn;
+    for (nn=HlNone; nn<=HlOper; nn++) {
+        if (!irccasecmp(msg, helpserv_level_names[nn]))
+            return nn;
+    }
+    log_module(HS_LOG, LOG_ERROR, "helpserv_str2level received invalid level %s.", msg);
+    return HlNone; /* Error */
+}
+
+static struct helpserv_user *GetHSUser(struct helpserv_bot *hs, struct handle_info *hi) {
+    return dict_find(hs->users, hi->handle, NULL);
+}
+
+static void helpserv_log_request(struct helpserv_request *req, const char *reason) {
+    char key[27+NICKLEN];
+    char userhost[USERLEN+HOSTLEN+2];
+
+    if (!reqlog_ctx || !req)
+        return;
+    if (!reason)
+        reason = "";
+
+    sprintf(key, "%s-" FMT_TIME_T "-%lu", req->hs->helpserv->nick, req->opened, req->id);
+    saxdb_start_record(reqlog_ctx, key, 1);
+    if (req->helper) {
+        saxdb_write_string(reqlog_ctx, KEY_REQUEST_HELPER, req->helper->handle->handle);
+        saxdb_write_int(reqlog_ctx, KEY_REQUEST_ASSIGNED, req->assigned);
+    }
+    if (req->handle) {
+        saxdb_write_string(reqlog_ctx, KEY_REQUEST_HANDLE, req->handle->handle);
+    }
+    if (req->user) {
+        saxdb_write_string(reqlog_ctx, KEY_REQUEST_NICK, req->user->nick);
+        sprintf(userhost, "%s@%s", req->user->ident, req->user->hostname);
+        saxdb_write_string(reqlog_ctx, KEY_REQUEST_USERHOST, userhost);
+    }
+    saxdb_write_int(reqlog_ctx, KEY_REQUEST_OPENED, req->opened);
+    saxdb_write_int(reqlog_ctx, KEY_REQUEST_CLOSED, now);
+    saxdb_write_string(reqlog_ctx, KEY_REQUEST_CLOSEREASON, reason);
+    saxdb_write_string_list(reqlog_ctx, KEY_REQUEST_TEXT, req->text);
+    saxdb_end_record(reqlog_ctx);
+
+    fflush(reqlog_f);
+}
+
+/* Searches for a request by number, nick, or account (num|nick|*account).
+ * As there can potentially be >1 match, it takes a reqlist. The return
+ * value is the "best" request found (explained in the comment block below).
+ *
+ * If num_results is not NULL, it is set to the number of potentially matching
+ * requests.
+ * If hs_user is not NULL, requests assigned to this helper will be given
+ * preference (oldest assigned, falling back to oldest if there are none).
+ */
+static struct helpserv_request * smart_get_request(struct helpserv_bot *hs, struct helpserv_user *hs_user, const char *needle, int *num_results) {
+    struct helpserv_reqlist *reqlist, resultlist;
+    struct helpserv_request *req, *oldest=NULL, *oldest_assigned=NULL;
+    struct userNode *user;
+    unsigned int i;
+
+    if (num_results)
+        *num_results = 0;
+
+    if (*needle == '*') {
+        /* This test (handle) requires the least processing, so it's first */
+        if (!(reqlist = dict_find(helpserv_reqs_byhand_dict, needle+1, NULL)))
+            return NULL;
+        helpserv_reqlist_init(&resultlist);
+        for (i=0; i < reqlist->used; i++) {
+            req = reqlist->list[i];
+            if (req->hs == hs) {
+                helpserv_reqlist_append(&resultlist, req);
+                if (num_results)
+                    (*num_results)++;
+            }
+        }
+    } else if (!needle[strspn(needle, "0123456789")]) {
+        /* The string is 100% numeric - a request id */
+        if (!(req = dict_find(hs->requests, needle, NULL)))
+            return NULL;
+        if (num_results)
+            *num_results = 1;
+        return req;
+    } else if ((user = GetUserH(needle))) {
+        /* And finally, search by nick */
+        if (!(reqlist = dict_find(helpserv_reqs_bynick_dict, needle, NULL)))
+            return NULL;
+        helpserv_reqlist_init(&resultlist);
+
+        for (i=0; i < reqlist->used; i++) {
+            req = reqlist->list[i];
+            if (req->hs == hs) {
+                helpserv_reqlist_append(&resultlist, req);
+                if (num_results)
+                    (*num_results)++;
+            }
+        }
+        /* If the nick didn't have anything, try their handle */
+        if (!resultlist.used && user->handle_info) {
+            char star_handle[NICKSERV_HANDLE_LEN+2];
+
+            helpserv_reqlist_clean(&resultlist);
+            sprintf(star_handle, "*%s", user->handle_info->handle);
+
+            return smart_get_request(hs, hs_user, star_handle, num_results);
+        }
+    } else {
+        return NULL;
+    }
+
+    if (resultlist.used == 0) {
+        helpserv_reqlist_clean(&resultlist);
+        return NULL;
+    } else if (resultlist.used == 1) {
+        req = resultlist.list[0];
+        helpserv_reqlist_clean(&resultlist);
+        return req;
+    }
+
+    /* In case there is >1 request returned, use the oldest one assigned to
+     * the helper executing the command. Otherwise, use the oldest request.
+     * This may not be the intended result for cmd_pickup (first unhandled
+     * request may be better), or cmd_reassign (first handled request), but
+     * it's close enough, and there really aren't supposed to be multiple
+     * requests per person anyway; they're just side effects of authing. */
+
+    for (i=0; i < resultlist.used; i++) {
+        req = resultlist.list[i];
+        if (!oldest || req->opened < oldest->opened)
+            oldest = req;
+        if (hs_user && (!oldest_assigned || (req->helper == hs_user && req->opened < oldest_assigned->opened)))
+            oldest_assigned = req;
+    }
+
+    helpserv_reqlist_clean(&resultlist);
+
+    return oldest_assigned ? oldest_assigned : oldest;
+}
+
+static struct helpserv_request * create_request(struct userNode *user, struct helpserv_bot *hs, int from_join) {
+    struct helpserv_request *req = calloc(1, sizeof(struct helpserv_request));
+    char lbuf[3][MAX_LINE_SIZE], unh[INTERVALLEN];
+    struct helpserv_reqlist *reqlist, *hand_reqlist;
+    const unsigned int from_opserv = 0;
+    const char *fmt;
+
+    assert(req);
+
+    req->id = ++hs->last_requestid;
+    sprintf(unh, "%lu", req->id);
+    dict_insert(hs->requests, strdup(unh), req);
+
+    if (hs->id_wrap) {
+        unsigned long i;
+        char buf[12];
+        if (hs->last_requestid < hs->id_wrap) {
+            for (i=hs->last_requestid; i < hs->id_wrap; i++) {
+                sprintf(buf, "%lu", i);
+                if (!dict_find(hs->requests, buf, NULL)) {
+                    hs->last_requestid = i-1;
+                    break;
+                }
+            }
+        }
+        if (hs->last_requestid >= hs->id_wrap) {
+            for (i=1; i < hs->id_wrap; i++) {
+                sprintf(buf, "%lu", i);
+                if (!dict_find(hs->requests, buf, NULL)) {
+                    hs->last_requestid = i-1;
+                    break;
+                }
+            }
+            if (i >= hs->id_wrap) {
+                log_module(HS_LOG, LOG_INFO, "%s has more requests than its id_wrap.", hs->helpserv->nick);
+            }
+        }
+    }
+
+    req->hs = hs;
+    req->helper = NULL;
+    req->text = alloc_string_list(4);
+    req->user = user;
+    req->handle = user->handle_info;
+    if (from_join && self->burst) {
+        extern time_t burst_begin;
+        /* We need to keep all the requests during a burst join together,
+         * even if the burst takes more than 1 second. ircu seems to burst
+         * in reverse-join order. */
+        req->opened = burst_begin;
+    } else {
+        req->opened = now;
+    }
+    req->updated = now;
+
+    if (!hs->unhandled) {
+        hs->unhandled = req;
+        req->next_unhandled = NULL;
+    } else if (self->burst && hs->unhandled->opened >= req->opened) {
+        req->next_unhandled = hs->unhandled;
+        hs->unhandled = req;
+    } else if (self->burst) {
+        struct helpserv_request *unh;
+        /* Add the request to the beginning of the set of requests with
+         * req->opened having the same value. This makes reqonjoin create
+         * requests in the correct order while bursting. Note that this
+         * does not correct request ids, so they will be in reverse order
+         * though "/msg botname next" will work properly. */
+        for (unh = hs->unhandled; unh->next_unhandled && unh->next_unhandled->opened < req->opened; unh = unh->next_unhandled) ;
+        req->next_unhandled = unh->next_unhandled;
+        unh->next_unhandled = req;
+    } else {
+        struct helpserv_request *unh;
+        /* Add to the end */
+        for (unh = hs->unhandled; unh->next_unhandled; unh = unh->next_unhandled) ;
+        req->next_unhandled = NULL;
+        unh->next_unhandled = req;
+    }
+
+    if (!(reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
+        reqlist = helpserv_reqlist_alloc();
+        dict_insert(helpserv_reqs_bynick_dict, user->nick, reqlist);
+    }
+    req->parent_nick_list = reqlist;
+    helpserv_reqlist_append(reqlist, req);
+
+    if (user->handle_info) {
+        if (!(hand_reqlist = dict_find(helpserv_reqs_byhand_dict, user->handle_info->handle, NULL))) {
+            hand_reqlist = helpserv_reqlist_alloc();
+            dict_insert(helpserv_reqs_byhand_dict, user->handle_info->handle, hand_reqlist);
+        }
+        req->parent_hand_list = hand_reqlist;
+        helpserv_reqlist_append(hand_reqlist, req);
+    } else {
+        req->parent_hand_list = NULL;
+    }
+
+    if (from_join) {
+        fmt = user_find_message(user, "HSMSG_REQ_NEWONJOIN");
+        sprintf(lbuf[0], fmt, hs->helpchan->name, req->id);
+    } else {
+        fmt = user_find_message(user, "HSMSG_REQ_NEW");
+        sprintf(lbuf[0], fmt, req->id);
+    }
+    if (req != hs->unhandled) {
+        intervalString(unh, now - hs->unhandled->opened);
+        fmt = user_find_message(user, "HSMSG_REQ_UNHANDLED_TIME");
+        sprintf(lbuf[1], fmt, unh);
+    } else {
+        fmt = user_find_message(user, "HSMSG_REQ_NO_UNHANDLED");
+        sprintf(lbuf[1], fmt);
+    }
+    switch (hs->persist_types[PERSIST_T_REQUEST]) {
+        case PERSIST_PART:
+            fmt = user_find_message(user, "HSMSG_REQ_PERSIST_PART");
+            sprintf(lbuf[2], fmt, hs->helpchan->name, hs->helpchan->name);
+            break;
+        case PERSIST_QUIT:
+            fmt = user_find_message(user, "HSMSG_REQ_PERSIST_QUIT");
+            sprintf(lbuf[2], fmt);
+            break;
+        default:
+            log_module(HS_LOG, LOG_ERROR, "%s has an invalid req_persist.", hs->helpserv->nick);
+        case PERSIST_CLOSE:
+            if (user->handle_info) {
+                fmt = user_find_message(user, "HSMSG_REQ_PERSIST_HANDLE");
+                sprintf(lbuf[2], fmt);
+            } else {
+                fmt = user_find_message(user, "HSMSG_REQ_PERSIST_QUIT");
+                sprintf(lbuf[2], fmt);
+            }
+            break;
+    }
+    helpserv_message(hs, user, MSGTYPE_REQ_OPENED);
+    if (from_opserv)
+        send_message_type(4, user, opserv, "%s %s %s", lbuf[0], lbuf[1], lbuf[2]);
+    else
+        send_message_type(4, user, hs->helpserv, "%s %s %s", lbuf[0], lbuf[1], lbuf[2]);
+
+    if (hs->req_on_join && req == hs->unhandled && hs->helpchan_empty) {
+        timeq_del(0, run_empty_interval, hs, TIMEQ_IGNORE_WHEN);
+        run_empty_interval(hs);
+    }
+
+    return req;
+}
+
+/* Handle a message from a user to a HelpServ bot. */
+static void helpserv_usermsg(struct userNode *user, struct helpserv_bot *hs, char *text) {
+    const int from_opserv = 0; /* for helpserv_notice */
+    struct helpserv_request *req=NULL, *newest=NULL;
+    struct helpserv_reqlist *reqlist, *hand_reqlist;
+    unsigned int i;
+
+    if ((reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
+        for (i=0; i < reqlist->used; i++) {
+            req = reqlist->list[i];
+            if (req->hs != hs)
+                continue;
+            if (!newest || (newest->opened < req->opened))
+                newest = req;
+        }
+
+        /* If nothing was found, this will set req to NULL */
+        req = newest;
+    }
+
+    if (user->handle_info) {
+        hand_reqlist = dict_find(helpserv_reqs_byhand_dict, user->handle_info->handle, NULL);
+        if (!req && hand_reqlist) {
+            /* Most recent first again */
+            for (i=0; i < hand_reqlist->used; i++) {
+                req = hand_reqlist->list[i];
+                if ((req->hs != hs) || req->user)
+                    continue;
+                if (!newest || (newest->opened < req->opened))
+                    newest = req;
+            }
+            req = newest;
+
+            if (req) {
+                req->user = user;
+                if (!reqlist) {
+                    reqlist = helpserv_reqlist_alloc();
+                    dict_insert(helpserv_reqs_bynick_dict, user->nick, reqlist);
+                }
+                req->parent_nick_list = reqlist;
+                helpserv_reqlist_append(reqlist, req);
+
+                if (req->helper && (hs->notify >= NOTIFY_USER))
+                    helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_FOUND", req->id, user->nick);
+
+                helpserv_msguser(user, "HSMSG_GREET_PRIVMSG_EXISTREQ", req->id);
+            }
+        }
+    } else {
+        hand_reqlist = NULL;
+    }
+
+    if (!req) {
+        if (text[0] == helpserv_conf.user_escape) {
+            helpserv_msguser(user, "HSMSG_USERCMD_NO_REQUEST");
+            return;
+        }
+        if ((hs->persist_types[PERSIST_T_REQUEST] == PERSIST_PART) && !GetUserMode(hs->helpchan, user)) {
+            helpserv_msguser(user, "HSMSG_REQ_YOU_NOT_IN_HELPCHAN_OPEN", hs->helpchan->name);
+            return;
+        }
+
+        req = create_request(user, hs, 0);
+        if (user->handle_info)
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_NEW_REQUEST_AUTHED", req->id, user->nick, user->handle_info->handle);
+        else
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_NEW_REQUEST_UNAUTHED", req->id, user->nick);
+    } else if (text[0] == helpserv_conf.user_escape) {
+        char cmdname[MAXLEN], *space;
+        helpserv_usercmd_t *usercmd;
+        struct userNode *likely_helper;
+
+        /* Find somebody likely to be the helper */
+        if (!req->helper)
+            likely_helper = NULL;
+        else if ((likely_helper = req->helper->handle->users) && !likely_helper->next_authed) {
+            /* only one user it could be :> */
+        } else for (likely_helper = req->helper->handle->users; likely_helper; likely_helper = likely_helper->next_authed)
+            if (GetUserMode(hs->helpchan, likely_helper))
+                break;
+
+        /* Parse out command name */
+        space = strchr(text+1, ' ');
+        if (space)
+            strncpy(cmdname, text+1, space-text-1);
+        else
+            strcpy(cmdname, text+1);
+
+        /* Call the user command function */
+        usercmd = dict_find(helpserv_usercmd_dict, cmdname, NULL);
+        if (usercmd)
+            usercmd(req, likely_helper, space+1);
+        else
+            helpserv_msguser(user, "HSMSG_USERCMD_UNKNOWN", cmdname);
+        return;
+    } else if (hs->intervals[INTERVAL_STALE_DELAY]
+               && (req->updated < (time_t)(now - hs->intervals[INTERVAL_STALE_DELAY]))
+               && (!hs->req_maxlen || req->text->used < hs->req_maxlen)) {
+        char buf[MAX_LINE_SIZE], updatestr[INTERVALLEN], timestr[MAX_LINE_SIZE];
+
+        strftime(timestr, MAX_LINE_SIZE, HSFMT_TIME, localtime(&req->opened));
+        intervalString(updatestr, now - req->updated);
+        if (req->helper && (hs->notify >= NOTIFY_USER))
+            if (user->handle_info)
+                helpserv_notify(req->helper, "HSMSG_PAGE_UPD_REQUEST_AUTHED", req->id, user->nick, user->handle_info->handle, timestr, updatestr);
+            else
+                helpserv_notify(req->helper, "HSMSG_PAGE_UPD_REQUESTNOT_AUTHED", req->id, user->nick, timestr, updatestr);
+        else
+            if (user->handle_info)
+                helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_UPD_REQUEST_AUTHED", req->id, user->nick, user->handle_info->handle, timestr, updatestr);
+            else
+                helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_UPD_REQUEST_NOT_AUTHED", req->id, user->nick, timestr, updatestr);
+        strftime(timestr, MAX_LINE_SIZE, HSFMT_TIME, localtime(&now));
+        snprintf(buf, MAX_LINE_SIZE, "[Stale request updated at %s]", timestr);
+        string_list_append(req->text, strdup(buf));
+    }
+
+    req->updated = now;
+    if (!hs->req_maxlen || req->text->used < hs->req_maxlen)
+        string_list_append(req->text, strdup(text));
+    else
+        helpserv_msguser(user, "HSMSG_REQ_MAXLEN");
+}
+
+/* Handle messages direct to a HelpServ bot. */
+static void helpserv_botmsg(struct userNode *user, struct userNode *target, char *text, UNUSED_ARG(int server_qualified)) {
+    struct helpserv_bot *hs;
+    struct helpserv_cmd *cmd;
+    struct helpserv_user *hs_user;
+    char *argv[MAXNUMPARAMS];
+    int argc, argv_shift;
+    const int from_opserv = 0; /* for helpserv_notice */
+
+    /* Ignore things consisting of empty lines or from ourselves */
+    if (!*text || IsLocal(user))
+        return;
+
+    hs = dict_find(helpserv_bots_dict, target->nick, NULL);
+
+    /* See if we should listen to their message as a command (helper)
+     * or a help request (user) */
+    if (!user->handle_info || !(hs_user = dict_find(hs->users, user->handle_info->handle, NULL))) {
+        helpserv_usermsg(user, hs, text);
+        return;
+    }
+
+    argv_shift = 1;
+    argc = split_line(text, false, ArrayLength(argv)-argv_shift, argv+argv_shift);
+    if (!argc)
+        return;
+
+    cmd = dict_find(helpserv_func_dict, argv[argv_shift], NULL);
+    if (!cmd) {
+        helpserv_notice(user, "MSG_COMMAND_UNKNOWN", argv[argv_shift]);
+        return;
+    }
+    if (cmd->flags & CMD_FROM_OPSERV_ONLY) {
+        helpserv_notice(user, "HSMSG_OPER_CMD");
+        return;
+    }
+    if (cmd->access > hs_user->level) {
+        helpserv_notice(user, "HSMSG_LEVEL_TOO_LOW");
+        return;
+    }
+    if (!cmd->func) {
+        helpserv_notice(user, "HSMSG_INTERNAL_COMMAND", argv[argv_shift]);
+    } else if (cmd->func(user, hs, 0, argc, argv+argv_shift)) {
+        unsplit_string(argv+argv_shift, argc, text);
+        log_audit(HS_LOG, LOG_COMMAND, user, hs->helpserv, hs->helpchan->name, 0, text);
+    }
+}
+
+/* Handle a control command from an IRC operator */
+static MODCMD_FUNC(cmd_helpserv) {
+    struct helpserv_bot *hs = NULL;
+    struct helpserv_cmd *subcmd;
+    const int from_opserv = 1; /* for helpserv_notice */
+    char botnick[NICKLEN+1]; /* in case command is unregister */
+    int retval;
+
+    if (argc < 2) {
+        send_help(user, opserv, helpserv_helpfile, NULL);
+        return 0;
+    }
+
+    if (!(subcmd = dict_find(helpserv_func_dict, argv[1], NULL))) {
+        helpserv_notice(user, "MSG_COMMAND_UNKNOWN", argv[1]);
+        return 0;
+    }
+
+    if (!subcmd->func) {
+        helpserv_notice(user, "HSMSG_INTERNAL_COMMAND", argv[1]);
+        return 0;
+    }
+
+    if ((subcmd->flags & CMD_NEED_BOT) && ((argc < 3) || !(hs = dict_find(helpserv_bots_dict, argv[2], NULL)))) {
+        helpserv_notice(user, "HSMSG_INVALID_BOT");
+        return 0;
+    }
+
+    if (subcmd->flags & CMD_NEVER_FROM_OPSERV) {
+        helpserv_notice(user, "HSMSG_NO_USE_OPSERV");
+        return 0;
+    }
+
+    if (hs) {
+        argv[2] = argv[1];
+        strcpy(botnick, hs->helpserv->nick);
+        retval = subcmd->func(user, hs, 1, argc-2, argv+2);
+    } else {
+        strcpy(botnick, "No bot");
+        retval = subcmd->func(user, hs, 1, argc-1, argv+1);
+    }
+
+    return retval;
+}
+
+static void helpserv_help(struct helpserv_bot *hs, int from_opserv, struct userNode *user, const char *topic) {
+    send_help(user, (from_opserv ? opserv : hs->helpserv), helpserv_helpfile, topic);
+}
+
+static int append_entry(const char *key, UNUSED_ARG(void *data), void *extra) {
+    struct helpfile_expansion *exp = extra;
+    int row;
+
+    row = exp->value.table.length++;
+    exp->value.table.contents[row] = calloc(1, sizeof(char*));
+    exp->value.table.contents[row][0] = key;
+    return 0;
+}
+
+static struct helpfile_expansion helpserv_expand_variable(const char *variable) {
+    struct helpfile_expansion exp;
+
+    if (!irccasecmp(variable, "index")) {
+        exp.type = HF_TABLE;
+        exp.value.table.length = 1;
+        exp.value.table.width = 1;
+        exp.value.table.flags = TABLE_REPEAT_ROWS;
+        exp.value.table.contents = calloc(dict_size(helpserv_func_dict)+1, sizeof(char**));
+        exp.value.table.contents[0] = calloc(1, sizeof(char*));
+        exp.value.table.contents[0][0] = "Commands:";
+        dict_foreach(helpserv_func_dict, append_entry, &exp);
+        return exp;
+    }
+
+    exp.type = HF_STRING;
+    exp.value.str = NULL;
+    return exp;
+}
+
+static void helpserv_helpfile_read(void) {
+    helpserv_helpfile = open_helpfile(HELPSERV_HELPFILE_NAME, helpserv_expand_variable);
+}
+
+static HELPSERV_USERCMD(usercmd_wait) {
+    struct helpserv_request *other;
+    int pos, count;
+    char buf[INTERVALLEN];
+
+    if (req->helper) {
+        if (likely_helper)
+            helpserv_user_reply("HSMSG_YOU_BEING_HELPED_BY", likely_helper->nick);
+        else
+            helpserv_user_reply("HSMSG_YOU_BEING_HELPED");
+        return;
+    }
+
+    for (other = req->hs->unhandled, pos = -1, count = 0; 
+         other;
+         other = other->next_unhandled, ++count) {
+        if (other == req)
+            pos = count;
+    }
+    assert(pos >= 0);
+    intervalString(buf, now - req->hs->unhandled->opened);
+    helpserv_user_reply("HSMSG_WAIT_STATUS", pos+1, count, buf);
+}
+
+static HELPSERV_FUNC(cmd_help) {
+    const char *topic;
+
+    if (argc < 2)
+        topic = NULL;
+    else
+        topic = unsplit_string(argv+1, argc-1, NULL);
+    helpserv_help(hs, from_opserv, user, topic);
+
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_readhelp) {
+    struct timeval start, stop;
+    struct helpfile *old_helpfile = helpserv_helpfile;
+
+    gettimeofday(&start, NULL);
+    helpserv_helpfile_read();
+    if (helpserv_helpfile) {
+        close_helpfile(old_helpfile);
+    } else {
+        helpserv_helpfile = old_helpfile;
+    }
+    gettimeofday(&stop, NULL);
+    stop.tv_sec -= start.tv_sec;
+    stop.tv_usec -= start.tv_usec;
+    if (stop.tv_usec < 0) {
+        stop.tv_sec -= 1;
+        stop.tv_usec += 1000000;
+    }
+    helpserv_notice(user, "HSMSG_READHELP_SUCCESS", stop.tv_sec, stop.tv_usec/1000);
+
+    return 1;
+}
+
+static struct helpserv_user * helpserv_add_user(struct helpserv_bot *hs, struct handle_info *handle, enum helpserv_level level) {
+    struct helpserv_user *hs_user;
+    struct helpserv_userlist *userlist;
+
+    hs_user = calloc(1, sizeof(struct helpserv_user));
+    hs_user->handle = handle;
+    hs_user->hs = hs;
+    hs_user->help_mode = 0;
+    hs_user->level = level;
+    hs_user->join_time = find_handle_in_channel(hs->helpchan, handle, NULL) ? now : 0;
+    dict_insert(hs->users, handle->handle, hs_user);
+
+    if (!(userlist = dict_find(helpserv_users_byhand_dict, handle->handle, NULL))) {
+        userlist = helpserv_userlist_alloc();
+        dict_insert(helpserv_users_byhand_dict, handle->handle, userlist);
+    }
+    helpserv_userlist_append(userlist, hs_user);
+
+    return hs_user;
+}
+
+static void helpserv_del_user(struct helpserv_bot *hs, struct helpserv_user *hs_user) {
+    dict_remove(hs->users, hs_user->handle->handle);
+}
+
+static int cmd_add_user(struct helpserv_bot *hs, int from_opserv, struct userNode *user, enum helpserv_level level, int argc, char *argv[]) {
+    struct helpserv_user *actor, *new_user;
+    struct handle_info *handle;
+
+    REQUIRE_PARMS(2);
+
+    if (!from_opserv) {
+        actor = GetHSUser(hs, user->handle_info);
+        if (actor->level < HlManager) {
+            helpserv_notice(user, "HSMSG_CANNOT_ADD");
+            return 0;
+        }
+    } else {
+        actor = NULL;
+    }
+
+    if (!(handle = helpserv_get_handle_info(user, argv[1])))
+        return 0;
+
+    if (GetHSUser(hs, handle)) {
+        helpserv_notice(user, "HSMSG_USER_EXISTS", handle->handle);
+        return 0;
+    }
+
+    if (!(from_opserv) && actor && (actor->level <= level)) {
+        helpserv_notice(user, "HSMSG_NO_BUMP_ACCESS");
+        return 0;
+    }
+
+    new_user = helpserv_add_user(hs, handle, level);
+
+    helpserv_notice(user, "HSMSG_ADDED_USER", helpserv_level2str(level), handle->handle);
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_deluser) {
+    struct helpserv_user *actor=NULL, *victim;
+    struct handle_info *handle;
+    enum helpserv_level level;
+
+    REQUIRE_PARMS(2);
+
+    if (!from_opserv) {
+        actor = GetHSUser(hs, user->handle_info);
+        if (actor->level < HlManager) {
+            helpserv_notice(user, "HSMSG_CANNOT_DEL");
+            return 0;
+        }
+    }
+
+    if (!(handle = helpserv_get_handle_info(user, argv[1])))
+        return 0;
+
+    if (!(victim = GetHSUser(hs, handle))) {
+        helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", handle->handle, hs->helpserv->nick);
+        return 0;
+    }
+
+    if (!from_opserv && actor && (actor->level <= victim->level)) {
+        helpserv_notice(user, "MSG_USER_OUTRANKED", victim->handle->handle);
+        return 0;
+    }
+
+    level = victim->level;
+    helpserv_del_user(hs, victim);
+    helpserv_notice(user, "HSMSG_DELETED_USER", helpserv_level2str(level), handle->handle);
+    return 1;
+}
+
+static int
+helpserv_user_comp(const void *arg_a, const void *arg_b)
+{
+    const struct helpserv_user *a = *(struct helpserv_user**)arg_a;
+    const struct helpserv_user *b = *(struct helpserv_user**)arg_b;
+    int res;
+    if (a->level != b->level)
+        res = b->level - a->level;
+    else
+        res = irccasecmp(a->handle->handle, b->handle->handle);
+    return res;
+}
+
+static int show_helper_range(struct userNode *user, struct helpserv_bot *hs, int from_opserv, enum helpserv_level min_lvl, enum helpserv_level max_lvl) {
+    struct helpserv_userlist users;
+    struct helpfile_table tbl;
+    struct helpserv_user *hs_user;
+    dict_iterator_t it;
+    enum helpserv_level last_level;
+    unsigned int ii;
+
+    users.used = 0;
+    users.size = dict_size(hs->users);
+    users.list = alloca(users.size*sizeof(hs->users[0]));
+    helpserv_notice(user, "HSMSG_USERLIST_HEADER", hs->helpserv->nick);
+    for (it = dict_first(hs->users); it; it = iter_next(it)) {
+        hs_user = iter_data(it);
+        if (hs_user->level < min_lvl)
+            continue;
+        if (hs_user->level > max_lvl)
+            continue;
+        users.list[users.used++] = hs_user;
+    }
+    if (!users.used) {
+        helpserv_notice(user, "MSG_NONE");
+        return 1;
+    }
+    qsort(users.list, users.used, sizeof(users.list[0]), helpserv_user_comp);
+    switch (user->handle_info->userlist_style) {
+    case HI_STYLE_DEF:
+        tbl.length = users.used + 1;
+        tbl.width = 3;
+        tbl.flags = TABLE_NO_FREE;
+        tbl.contents = alloca(tbl.length * sizeof(tbl.contents[0]));
+        tbl.contents[0] = alloca(tbl.width * sizeof(tbl.contents[0][0]));
+        tbl.contents[0][0] = "Level";
+        tbl.contents[0][1] = "Handle";
+        tbl.contents[0][2] = "WeekStart";
+        for (ii = 0; ii < users.used; ) {
+            hs_user = users.list[ii++];
+            tbl.contents[ii] = alloca(tbl.width * sizeof(tbl.contents[0][0]));
+            tbl.contents[ii][0] = helpserv_level_names[hs_user->level];
+            tbl.contents[ii][1] = hs_user->handle->handle;
+            tbl.contents[ii][2] = weekday_names[hs_user->week_start];
+        }
+        table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
+        break;
+    case HI_STYLE_ZOOT: default:
+        last_level = HlNone;
+        tbl.length = 0;
+        tbl.width = 1;
+        tbl.flags = TABLE_NO_FREE | TABLE_REPEAT_ROWS | TABLE_NO_HEADERS;
+        tbl.contents = alloca(users.used * sizeof(tbl.contents[0]));
+        for (ii = 0; ii < users.used; ) {
+            hs_user = users.list[ii++];
+            if (hs_user->level != last_level) {
+                if (tbl.length) {
+                    helpserv_notice(user, "HSMSG_USERLIST_ZOOT_LVL", hs->helpserv->nick, helpserv_level_names[last_level]);
+                    table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
+                    tbl.length = 0;
+                }
+                last_level = hs_user->level;
+            }
+            tbl.contents[tbl.length] = alloca(tbl.width * sizeof(tbl.contents[0][0]));
+            tbl.contents[tbl.length++][0] = hs_user->handle->handle;
+        }
+        if (tbl.length) {
+            helpserv_notice(user, "HSMSG_USERLIST_ZOOT_LVL", hs->helpserv->nick, helpserv_level_names[last_level]);
+            table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
+        }
+    }
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_helpers) {
+    return show_helper_range(user, hs, from_opserv, HlTrial, HlOwner);
+}
+
+static HELPSERV_FUNC(cmd_wlist) {
+    return show_helper_range(user, hs, from_opserv, HlOwner, HlOwner);
+}
+
+static HELPSERV_FUNC(cmd_mlist) {
+    return show_helper_range(user, hs, from_opserv, HlManager, HlManager);
+}
+
+static HELPSERV_FUNC(cmd_hlist) {
+    return show_helper_range(user, hs, from_opserv, HlHelper, HlHelper);
+}
+
+static HELPSERV_FUNC(cmd_tlist) {
+    return show_helper_range(user, hs, from_opserv, HlTrial, HlTrial);
+}
+
+static HELPSERV_FUNC(cmd_addowner) {
+    return cmd_add_user(hs, from_opserv, user, HlOwner, argc, argv);
+}
+
+static HELPSERV_FUNC(cmd_addmanager) {
+    return cmd_add_user(hs, from_opserv, user, HlManager, argc, argv);
+}
+
+static HELPSERV_FUNC(cmd_addhelper) {
+    return cmd_add_user(hs, from_opserv, user, HlHelper, argc, argv);
+}
+
+static HELPSERV_FUNC(cmd_addtrial) {
+    return cmd_add_user(hs, from_opserv, user, HlTrial, argc, argv);
+}
+
+static HELPSERV_FUNC(cmd_clvl) {
+    struct helpserv_user *actor=NULL, *victim;
+    struct handle_info *handle;
+    enum helpserv_level level;
+
+    REQUIRE_PARMS(3);
+
+    if (!from_opserv) {
+        actor = GetHSUser(hs, user->handle_info);
+        if (actor->level < HlManager) {
+            helpserv_notice(user, "HSMSG_CANNOT_CLVL");
+            return 0;
+        }
+    }
+
+    if (!(handle = helpserv_get_handle_info(user, argv[1])))
+        return 0;
+
+    if (!(victim = GetHSUser(hs, handle))) {
+        helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", handle->handle, hs->helpserv->nick);
+        return 0;
+    }
+
+    if (((level = helpserv_str2level(argv[2])) == HlNone) || level == HlOper) {
+        helpserv_notice(user, "HSMSG_INVALID_ACCESS", argv[2]);
+        return 0;
+    }
+
+    if (!(from_opserv) && actor) {
+        if (actor == victim) {
+            helpserv_notice(user, "HSMSG_NO_SELF_CLVL");
+            return 0;
+        }
+
+        if (actor->level <= victim->level) {
+            helpserv_notice(user, "MSG_USER_OUTRANKED", victim->handle->handle);
+            return 0;
+        }
+
+        if (level >= actor->level) {
+            helpserv_notice(user, "HSMSG_NO_BUMP_ACCESS");
+            return 0;
+        }
+    }
+
+    victim->level = level;
+    helpserv_notice(user, "HSMSG_CHANGED_ACCESS", handle->handle, helpserv_level2str(level));
+
+    return 1;
+}
+
+static void free_request(void *data) {
+    struct helpserv_request *req = data;
+
+    /* Logging */
+    if (shutting_down && (req->hs->persist_types[PERSIST_T_REQUEST] != PERSIST_CLOSE || !req->handle)) {
+        helpserv_log_request(req, "srvx shutdown");
+    }
+
+    /* Clean up from the unhandled queue */
+    if (req->hs->unhandled) {
+        if (req->hs->unhandled == req) {
+            req->hs->unhandled = req->next_unhandled;
+        } else {
+            struct helpserv_request *uh;
+            for (uh=req->hs->unhandled; uh->next_unhandled && (uh->next_unhandled != req); uh = uh->next_unhandled);
+            if (uh->next_unhandled) {
+                uh->next_unhandled = req->next_unhandled;
+            }
+        }
+    }
+
+    /* Clean up the lists */
+    if (req->parent_nick_list) {
+        if (req->parent_nick_list->used == 1) {
+            dict_remove(helpserv_reqs_bynick_dict, req->user->nick);
+        } else {
+            helpserv_reqlist_remove(req->parent_nick_list, req);
+        }
+    }
+    if (req->parent_hand_list) {
+        if (req->parent_hand_list->used == 1) {
+            dict_remove(helpserv_reqs_byhand_dict, req->handle->handle);
+        } else {
+            helpserv_reqlist_remove(req->parent_hand_list, req);
+        }
+    }
+
+    free_string_list(req->text);
+    free(req);
+}
+
+static HELPSERV_FUNC(cmd_close) {
+    struct helpserv_request *req, *newest=NULL;
+    struct helpserv_reqlist *nick_list, *hand_list;
+    struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
+    struct userNode *req_user=NULL;
+    char close_reason[MAXLEN], reqnum[12];
+    unsigned long old_req;
+    unsigned int i;
+    int num_requests=0;
+
+    REQUIRE_PARMS(2);
+
+    assert(hs_user);
+
+    if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
+        helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
+        return 0;
+    }
+
+    sprintf(reqnum, "%lu", req->id);
+
+    if (num_requests > 1)
+        helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
+
+    if (hs_user->level < HlManager && req->helper != hs_user) {
+        if (req->helper)
+            helpserv_notice(user, "HSMSG_REQ_NOT_YOURS_ASSIGNED_TO", req->id, req->helper->handle->handle);
+        else
+            helpserv_notice(user, "HSMSG_REQ_NOT_YOURS_UNASSIGNED", req->id);
+        return 0;
+    }
+
+    helpserv_notice(user, "HSMSG_REQ_CLOSED", req->id);
+    if (req->user) {
+        req_user = req->user;
+        helpserv_message(hs, req->user, MSGTYPE_REQ_CLOSED);
+        if (req->handle)
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_CLOSE_REQUEST_1", req->id, req->user->nick, req->handle->handle, user->nick);
+        else
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_CLOSE_REQUEST_2", req->id, req->user->nick, user->nick);
+    } else {
+        if (req->handle)
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_CLOSE_REQUEST_3", req->id, req->handle->handle, user->nick);
+        else
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_CLOSE_REQUEST_4", req->id, user->nick);
+    }
+
+    hs_user->closed[0]++;
+    hs_user->closed[4]++;
+
+    /* Set these to keep track of the lists after the request is gone, but
+     * not if free_request() will helpserv_reqlist_free() them. */
+    nick_list = req->parent_nick_list;
+    if (nick_list && (nick_list->used == 1))
+        nick_list = NULL;
+    hand_list = req->parent_hand_list;
+    if (hand_list && (hand_list->used == 1))
+        hand_list = NULL;
+    old_req = req->id;
+
+    if (argc >= 3) {
+        snprintf(close_reason, MAXLEN, "Closed by %s: %s", user->handle_info->handle, unsplit_string(argv+2, argc-2, NULL));
+    } else {
+        sprintf(close_reason, "Closed by %s", user->handle_info->handle);
+    }
+    helpserv_log_request(req, close_reason);
+    dict_remove(hs->requests, reqnum);
+
+    /* Look for other requests associated with them */
+    if (nick_list) {
+        for (i=0; i < nick_list->used; i++) {
+            req = nick_list->list[i];
+
+            if (req->hs != hs)
+                continue;
+            if (!newest || (newest->opened < req->opened))
+                newest = req;
+        }
+
+        if (newest)
+            helpserv_msguser(newest->user, "HSMSG_REQ_FOUND_ANOTHER", old_req, newest->id);
+    }
+
+    if (req_user && hs->auto_devoice) {
+        struct modeNode *mn = GetUserMode(hs->helpchan, req_user);
+        if ((!newest || !newest->helper) && mn && (mn->modes & MODE_VOICE)) {
+            struct mod_chanmode change;
+            change.modes_set = change.modes_clear = 0;
+            change.argc = 1;
+            change.args[0].mode = MODE_REMOVE | MODE_VOICE;
+            change.args[0].member = mn;
+            mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
+        }
+    }
+
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_list) {
+    dict_iterator_t it;
+    int searchtype;
+    struct helpfile_table tbl;
+    unsigned int line, total;
+    struct helpserv_request *req;
+
+    if ((argc < 2) || !irccasecmp(argv[1], "unassigned")) {
+        for (req = hs->unhandled, total=0; req; req = req->next_unhandled, total++) ;
+        helpserv_notice(user, "HSMSG_REQ_LIST_TOP_UNASSIGNED", total);
+        searchtype = 1; /* Unassigned */
+    } else if (!irccasecmp(argv[1], "assigned")) {
+        for (req = hs->unhandled, total=dict_size(hs->requests); req; req = req->next_unhandled, total--) ;
+        helpserv_notice(user, "HSMSG_REQ_LIST_TOP_ASSIGNED", total);
+        searchtype = 2; /* Assigned */
+    } else if (!irccasecmp(argv[1], "me")) {
+        for (total = 0, it = dict_first(hs->requests); it; it = iter_next(it)) {
+            req = iter_data(it);
+            if (req->helper && (req->helper->handle == user->handle_info))
+                total++;
+        }
+        helpserv_notice(user, "HSMSG_REQ_LIST_TOP_YOUR", total);
+        searchtype = 4;
+    } else if (!irccasecmp(argv[1], "all")) {
+        total = dict_size(hs->requests);
+        helpserv_notice(user, "HSMSG_REQ_LIST_TOP_ALL", total);
+        searchtype = 3; /* All */
+    } else {
+        helpserv_notice(user, "HSMSG_BAD_REQ_TYPE", argv[1]);
+        return 0;
+    }
+
+    if (!total) {
+        helpserv_notice(user, "HSMSG_REQ_LIST_NONE");
+        return 1;
+    }
+
+    tbl.length = total+1;
+    tbl.width = 5;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
+    tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
+    tbl.contents[0][0] = "ID#";
+    tbl.contents[0][1] = "User";
+    tbl.contents[0][2] = "Helper";
+    tbl.contents[0][3] = "Time open";
+    tbl.contents[0][4] = "User status";
+
+    for (it=dict_first(hs->requests), line=0; it; it=iter_next(it)) {
+        char opentime[INTERVALLEN], reqid[12], username[NICKLEN+2];
+
+        req = iter_data(it);
+
+        switch (searchtype) {
+        case 1:
+            if (req->helper)
+                continue;
+            break;
+        case 2:
+            if (!req->helper)
+                continue;
+            break;
+        case 3:
+        default:
+            break;
+        case 4:
+            if (!req->helper || (req->helper->handle != user->handle_info))
+                continue;
+            break;
+        }
+
+        line++;
+
+        tbl.contents[line] = alloca(tbl.width * sizeof(**tbl.contents));
+        sprintf(reqid, "%lu", req->id);
+        tbl.contents[line][0] = strdup(reqid);
+        if (req->user) {
+            strcpy(username, req->user->nick);
+        } else {
+            username[0] = '*';
+            strcpy(username+1, req->handle->handle);
+        }
+        tbl.contents[line][1] = strdup(username);
+        tbl.contents[line][2] = req->helper ? req->helper->handle->handle : "(Unassigned)";
+        intervalString(opentime, now - req->opened);
+        tbl.contents[line][3] = strdup(opentime);
+        tbl.contents[line][4] = ((req->user || req->handle->users) ? "Online" : "Offline");
+    }
+
+    table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
+
+    for (; line > 0; line--) {
+        free((char *)tbl.contents[line][0]);
+        free((char *)tbl.contents[line][1]);
+        free((char *)tbl.contents[line][3]);
+    }
+
+    return 1;
+}
+
+static void helpserv_show(int from_opserv, struct helpserv_bot *hs, struct userNode *user, struct helpserv_request *req) {
+    unsigned int nn;
+    char buf[MAX_LINE_SIZE];
+    char buf2[INTERVALLEN];
+
+    if (req->user)
+        if (req->handle)
+            helpserv_notice(user, "HSMSG_REQ_INFO_2a", req->user->nick, req->handle->handle);
+        else
+            helpserv_notice(user, "HSMSG_REQ_INFO_2b", req->user->nick);
+    else if (req->handle)
+        if (req->handle->users)
+            helpserv_notice(user, "HSMSG_REQ_INFO_2c", req->handle->handle);
+        else
+            helpserv_notice(user, "HSMSG_REQ_INFO_2d", req->handle->handle);
+    else
+        helpserv_notice(user, "HSMSG_REQ_INFO_2e");
+    strftime(buf, MAX_LINE_SIZE, HSFMT_TIME, localtime(&req->opened));
+    intervalString(buf2, now - req->opened);
+    helpserv_notice(user, "HSMSG_REQ_INFO_3", buf, buf2);
+    helpserv_notice(user, "HSMSG_REQ_INFO_4");
+    for (nn=0; nn < req->text->used; nn++)
+        helpserv_notice(user, "HSMSG_REQ_INFO_MESSAGE", req->text->list[nn]);
+}
+
+/* actor is the one who executed the command... it should == user except from
+ * cmd_assign */
+static int helpserv_assign(int from_opserv, struct helpserv_bot *hs, struct userNode *user, struct userNode *actor, struct helpserv_request *req) {
+    struct helpserv_request *req2;
+    struct helpserv_user *old_helper;
+
+    if (!user->handle_info)
+        return 0;
+    if ((hs->persist_types[PERSIST_T_HELPER] == PERSIST_PART) && !GetUserMode(hs->helpchan, user)) {
+        struct helpserv_user *hsuser_actor = GetHSUser(hs, actor->handle_info);
+        if (hsuser_actor->level < HlManager) {
+            helpserv_notice(user, "HSMSG_REQ_YOU_NOT_IN_HELPCHAN", hs->helpchan->name);
+            return 0;
+        } else if (user != actor) {
+            helpserv_notice(user, "HSMSG_REQ_YOU_NOT_IN_OVERRIDE", hs->helpchan->name);
+            helpserv_notice(actor, "HSMSG_REQ_HIM_NOT_IN_OVERRIDE", user->nick, hs->helpchan->name);
+        } else
+            helpserv_notice(user, "HSMSG_REQ_SELF_NOT_IN_OVERRIDE", hs->helpchan->name);
+    }
+
+    hs->last_active = now;
+    if ((old_helper = req->helper)) {
+        /* don't need to remove from the unhandled queue */
+    } else if (hs->unhandled == req) {
+        hs->unhandled = req->next_unhandled;
+    } else for (req2 = hs->unhandled; req2; req2 = req2->next_unhandled) {
+        if (req2->next_unhandled == req) {
+            req2->next_unhandled = req->next_unhandled;
+            break;
+        }
+    }
+    req->next_unhandled = NULL;
+    req->helper = GetHSUser(hs, user->handle_info);
+    assert(req->helper);
+    req->assigned = now;
+
+    if (old_helper) {
+        helpserv_notice(user, "HSMSG_REQ_REASSIGNED", req->id, old_helper->handle->handle);
+        req->helper->reassigned_to[0]++;
+        req->helper->reassigned_to[4]++;
+        old_helper->reassigned_from[0]++;
+        old_helper->reassigned_from[4]++;
+    } else {
+        helpserv_notice(user, "HSMSG_REQ_ASSIGNED_YOU", req->id);
+        req->helper->picked_up[0]++;
+        req->helper->picked_up[4]++;
+    }
+    helpserv_show(from_opserv, hs, user, req);
+    if (req->user) {
+        helpserv_message(hs, req->user, MSGTYPE_REQ_ASSIGNED);
+        if (old_helper) {
+            helpserv_msguser(req->user, "HSMSG_REQ_ASSIGNED_AGAIN", req->id, user->handle_info->handle, user->nick);
+        } else {
+            helpserv_msguser(req->user, "HSMSG_REQ_ASSIGNED", req->id, user->handle_info->handle, user->nick);
+        }
+        if (req->handle)
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_ASSIGN_REQUEST_1", req->id, req->user->nick, req->handle->handle, user->nick);
+        else
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_ASSIGN_REQUEST_2", req->id, req->user->nick, user->nick);
+    } else {
+        if (req->handle)
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_ASSIGN_REQUEST_3", req->id, req->handle->handle, user->nick);
+        else
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_ASSIGN_REQUEST_4", req->id, user->nick);
+    }
+
+    if (req->user && hs->auto_voice) {
+        struct mod_chanmode change;
+        change.modes_set = change.modes_clear = 0;
+        change.argc = 1;
+        change.args[0].mode = MODE_VOICE;
+        if ((change.args[0].member = GetUserMode(hs->helpchan, req->user)))
+            mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
+    }
+
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_next) {
+    struct helpserv_request *req;
+
+    if (!(req = hs->unhandled)) {
+        helpserv_notice(user, "HSMSG_REQ_NO_UNASSIGNED");
+        return 0;
+    }
+    return helpserv_assign(from_opserv, hs, user, user, req);
+}
+
+static HELPSERV_FUNC(cmd_show) {
+    struct helpserv_request *req;
+    struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
+    int num_requests=0;
+
+    REQUIRE_PARMS(2);
+
+    assert(hs_user);
+
+    if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
+        helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
+        return 0;
+    }
+
+    if (num_requests > 1)
+        helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
+
+    helpserv_notice(user, "HSMSG_REQ_INFO_1", req->id);
+    helpserv_show(from_opserv, hs, user, req);
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_pickup) {
+    struct helpserv_request *req;
+    struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
+    int num_requests=0;
+
+    REQUIRE_PARMS(2);
+
+    assert(hs_user);
+
+    if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
+        helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
+        return 0;
+    }
+
+    if (num_requests > 1)
+        helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
+
+    return helpserv_assign(from_opserv, hs, user, user, req);
+}
+
+static HELPSERV_FUNC(cmd_reassign) {
+    struct helpserv_request *req;
+    struct userNode *targetuser;
+    struct helpserv_user *target;
+    struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
+    int num_requests=0;
+
+    REQUIRE_PARMS(3);
+
+    assert(hs_user);
+
+    if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
+        helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
+        return 0;
+    }
+
+    if (num_requests > 1)
+        helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
+
+    if (!(targetuser = GetUserH(argv[2]))) {
+        helpserv_notice(user, "MSG_NICK_UNKNOWN", argv[2]);
+        return 0;
+    }
+
+    if (!targetuser->handle_info) {
+        helpserv_notice(user, "MSG_USER_AUTHENTICATE", targetuser->nick);
+        return 0;
+    }
+
+    if (!(target = GetHSUser(hs, targetuser->handle_info))) {
+        helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", targetuser->nick, hs->helpserv->nick);
+        return 0;
+    }
+
+    if ((hs->persist_types[PERSIST_T_HELPER] == PERSIST_PART) && !GetUserMode(hs->helpchan, user) && (hs_user->level < HlManager)) {
+        helpserv_notice(user, "HSMSG_REQ_HIM_NOT_IN_HELPCHAN", targetuser->nick, hs->helpchan->name);
+        return 0;
+    }
+
+    helpserv_assign(from_opserv, hs, targetuser, user, req);
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_addnote) {
+    char text[MAX_LINE_SIZE], timestr[MAX_LINE_SIZE], *note;
+    struct helpserv_request *req;
+    struct helpserv_user *hs_user=GetHSUser(hs, user->handle_info);
+    int num_requests=0;
+
+    REQUIRE_PARMS(3);
+
+    assert(hs_user);
+
+    if (!(req = smart_get_request(hs, hs_user, argv[1], &num_requests))) {
+        helpserv_notice(user, "HSMSG_REQ_INVALID", argv[1]);
+        return 0;
+    }
+
+    if (num_requests > 1)
+        helpserv_notice(user, "HSMSG_REQ_FOUNDMANY");
+
+    note = unsplit_string(argv+2, argc-2, NULL);
+
+    strftime(timestr, MAX_LINE_SIZE, HSFMT_TIME, localtime(&now));
+    snprintf(text, MAX_LINE_SIZE, "[Helper note at %s]:", timestr);
+    string_list_append(req->text, strdup(text));
+    snprintf(text, MAX_LINE_SIZE, "  <%s> %s", user->handle_info->handle, note);
+    string_list_append(req->text, strdup(text));
+
+    helpserv_notice(user, "HSMSG_REQMSG_NOTE_ADDED", req->id);
+
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_page) {
+    REQUIRE_PARMS(2);
+
+    helpserv_page(PGSRC_COMMAND, "HSMSG_PAGE_REQUEST", user->nick, unsplit_string(argv+1, argc-1, NULL));
+
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_stats) {
+    struct helpserv_user *target, *hs_user;
+    struct handle_info *target_handle;
+    struct helpfile_table tbl;
+    int i;
+    char intervalstr[INTERVALLEN], buf[16];
+
+    hs_user = from_opserv ? NULL : GetHSUser(hs, user->handle_info);
+
+    if (argc > 1) {
+        if (!from_opserv && (hs_user->level < HlManager)) {
+            helpserv_notice(user, "HSMSG_NEED_MANAGER");
+            return 0;
+        }
+
+        if (!(target_handle = helpserv_get_handle_info(user, argv[1]))) {
+            return 0;
+        }
+
+        if (!(target = GetHSUser(hs, target_handle))) {
+            helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", target_handle->handle, hs->helpserv->nick);
+            return 0;
+        }
+    } else {
+        if (from_opserv) {
+            helpserv_notice(user, "HSMSG_OPSERV_NEED_USER");
+            return 0;
+        }
+        target = hs_user;
+    }
+
+    helpserv_notice(user, "HSMSG_STATS_TOP", hs->helpserv->nick, target->handle->handle, weekday_names[target->week_start]);
+
+    tbl.length = 6;
+    tbl.width = 2;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
+    tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
+    tbl.contents[0][0] = "";
+    tbl.contents[0][1] = "Recorded time";
+    for (i=0; i < 5; i++) {
+        unsigned int week_time = target->time_per_week[i];
+        tbl.contents[i+1] = alloca(tbl.width * sizeof(**tbl.contents));
+        if ((i == 0 || i == 4) && target->join_time)
+            week_time += now - target->join_time;
+        helpserv_interval(intervalstr, week_time);
+        tbl.contents[i+1][1] = strdup(intervalstr);
+    }
+    tbl.contents[1][0] = "This week";
+    tbl.contents[2][0] = "Last week";
+    tbl.contents[3][0] = "2 weeks ago";
+    tbl.contents[4][0] = "3 weeks ago";
+    tbl.contents[5][0] = "Total";
+
+    helpserv_notice(user, "HSMSG_STATS_TIME", hs->helpchan->name);
+    table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
+
+    for (i=1; i <= 5; i++)
+        free((char *)tbl.contents[i][1]);
+
+    tbl.length = 5;
+    tbl.width = 4;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
+    tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
+    tbl.contents[1] = alloca(tbl.width * sizeof(**tbl.contents));
+    tbl.contents[2] = alloca(tbl.width * sizeof(**tbl.contents));
+    tbl.contents[3] = alloca(tbl.width * sizeof(**tbl.contents));
+    tbl.contents[4] = alloca(tbl.width * sizeof(**tbl.contents));
+    tbl.contents[0][0] = "Category";
+    tbl.contents[0][1] = "This week";
+    tbl.contents[0][2] = "Last week";
+    tbl.contents[0][3] = "Total";
+
+    tbl.contents[1][0] = "Requests picked up";
+    for (i=0; i < 3; i++) {
+        sprintf(buf, "%u", target->picked_up[(i == 2 ? 4 : i)]);
+        tbl.contents[1][i+1] = strdup(buf);
+    }
+    tbl.contents[2][0] = "Requests closed";
+    for (i=0; i < 3; i++) {
+        sprintf(buf, "%u", target->closed[(i == 2 ? 4 : i)]);
+        tbl.contents[2][i+1] = strdup(buf);
+    }
+    tbl.contents[3][0] = "Reassigned from";
+    for (i=0; i < 3; i++) {
+        sprintf(buf, "%u", target->reassigned_from[(i == 2 ? 4 : i)]);
+        tbl.contents[3][i+1] = strdup(buf);
+    }
+    tbl.contents[4][0] = "Reassigned to";
+    for (i=0; i < 3; i++) {
+        sprintf(buf, "%u", target->reassigned_to[(i == 2 ? 4 : i)]);
+        tbl.contents[4][i+1] = strdup(buf);
+    }
+
+    helpserv_notice(user, "HSMSG_STATS_REQS");
+    table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
+
+    for (i=1; i < 5; i++) {
+        free((char *)tbl.contents[i][1]);
+        free((char *)tbl.contents[i][2]);
+        free((char *)tbl.contents[i][3]);
+    }
+
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_statsreport) {
+    int use_privmsg=1;
+    struct helpfile_table tbl;
+    dict_iterator_t it;
+    unsigned int line, i;
+    struct userNode *srcbot = from_opserv ? opserv : hs->helpserv;
+
+    if ((argc > 1) && !irccasecmp(argv[1], "NOTICE"))
+        use_privmsg = 0;
+
+    tbl.length = dict_size(hs->users)+1;
+    tbl.width = 3;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
+    tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
+    tbl.contents[0][0] = "Account";
+    tbl.contents[0][1] = "Requests";
+    tbl.contents[0][2] = "Time helping";
+
+    for (it=dict_first(hs->users), line=0; it; it=iter_next(it)) {
+        struct helpserv_user *hs_user=iter_data(it);
+
+        tbl.contents[++line] = alloca(tbl.width * sizeof(**tbl.contents));
+        tbl.contents[line][0] = hs_user->handle->handle;
+        tbl.contents[line][1] = malloc(12);
+        tbl.contents[line][2] = malloc(32); /* A bit more than needed */
+    }
+
+    /* 4 to 1 instead of 3 to 0 because it's unsigned */
+    for (i=4; i > 0; i--) {
+        for (it=dict_first(hs->users), line=0; it; it=iter_next(it)) {
+            struct helpserv_user *hs_user = iter_data(it);
+            /* Time */
+            unsigned int week_time = hs_user->time_per_week[i-1];
+            if ((i==1) && hs_user->join_time)
+                week_time += now - hs_user->join_time;
+            helpserv_interval((char *)tbl.contents[++line][2], week_time);
+
+            /* Requests */
+            sprintf((char *)tbl.contents[line][1], "%u", hs_user->picked_up[i-1]+hs_user->reassigned_to[i-1]);
+        }
+        send_target_message(use_privmsg, user->nick, srcbot, statsreport_week[i-1]);
+        table_send(srcbot, user->nick, 0, (use_privmsg ? irc_privmsg : irc_notice), tbl);
+    }
+
+    for (line=1; line <= dict_size(hs->users); line++) {
+        free((char *)tbl.contents[line][1]);
+        free((char *)tbl.contents[line][2]);
+    }
+
+    return 1;
+}
+
+static int
+helpserv_in_channel(struct helpserv_bot *hs, struct chanNode *channel) {
+    enum page_source pgsrc;
+    if (channel == hs->helpchan)
+        return 1;
+    for (pgsrc=0; pgsrc<PGSRC_COUNT; pgsrc++)
+        if (channel == hs->page_targets[pgsrc])
+            return 1;
+    return 0;
+}
+
+static HELPSERV_FUNC(cmd_move) {
+    if (!hs) {
+        helpserv_notice(user, "HSMSG_INVALID_BOT");
+        return 0;
+    }
+
+    REQUIRE_PARMS(2);
+
+    if (is_valid_nick(argv[1])) {
+        char *newnick = argv[1], oldnick[NICKLEN], reason[MAXLEN];
+
+        strcpy(oldnick, hs->helpserv->nick);
+
+        if (GetUserH(newnick)) {
+            helpserv_notice(user, "HSMSG_NICK_EXISTS", newnick);
+            return 0;
+        }
+
+        dict_remove2(helpserv_bots_dict, hs->helpserv->nick, 1);
+        NickChange(hs->helpserv, newnick, 0);
+        dict_insert(helpserv_bots_dict, hs->helpserv->nick, hs);
+
+        helpserv_notice(user, "HSMSG_RENAMED", oldnick, newnick);
+
+        snprintf(reason, MAXLEN, "HelpServ bot %s (in %s) renamed to %s by %s.", oldnick, hs->helpchan->name, newnick, user->nick);
+        global_message(MESSAGE_RECIPIENT_OPERS, reason);
+
+        return 1;
+    } else if (IsChannelName(argv[1])) {
+        struct chanNode *old_helpchan = hs->helpchan;
+        char *newchan = argv[1], oldchan[CHANNELLEN], reason[MAXLEN];
+        struct helpserv_botlist *botlist;
+
+        strcpy(oldchan, hs->helpchan->name);
+
+        if (!irccasecmp(oldchan, newchan)) {
+            helpserv_notice(user, "HSMSG_MOVE_SAME_CHANNEL", hs->helpserv->nick);
+            return 0;
+        }
+
+        if (opserv_bad_channel(newchan)) {
+            helpserv_notice(user, "HSMSG_ILLEGAL_CHANNEL", newchan);
+            return 0;
+        }
+
+        botlist = dict_find(helpserv_bots_bychan_dict, hs->helpchan->name, NULL);
+        helpserv_botlist_remove(botlist, hs);
+        if (botlist->used == 0) {
+            dict_remove(helpserv_bots_bychan_dict, hs->helpchan->name);
+        }
+
+        hs->helpchan = NULL;
+        if (!helpserv_in_channel(hs, old_helpchan)) {
+            snprintf(reason, MAXLEN, "Moved to %s by %s.", newchan, user->nick);
+            DelChannelUser(hs->helpserv, old_helpchan, reason, 0);
+        }
+
+        if (!(hs->helpchan = GetChannel(newchan))) {
+            hs->helpchan = AddChannel(newchan, now, NULL, NULL);
+            AddChannelUser(hs->helpserv, hs->helpchan)->modes |= MODE_CHANOP;
+        } else if (!helpserv_in_channel(hs, old_helpchan)) {
+            struct mod_chanmode change;
+            change.modes_set = change.modes_clear = 0;
+            change.argc = 1;
+            change.args[0].mode = MODE_CHANOP;
+            change.args[0].member = AddChannelUser(hs->helpserv, hs->helpchan);
+            mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
+        }
+
+        if (!(botlist = dict_find(helpserv_bots_bychan_dict, hs->helpchan->name, NULL))) {
+            botlist = helpserv_botlist_alloc();
+            dict_insert(helpserv_bots_bychan_dict, hs->helpchan->name, botlist);
+        }
+        helpserv_botlist_append(botlist, hs);
+
+        snprintf(reason, MAXLEN, "HelpServ %s (%s) moved to %s by %s.", hs->helpserv->nick, oldchan, newchan, user->nick);
+        global_message(MESSAGE_RECIPIENT_OPERS, reason);
+
+        return 1;
+    } else {
+        helpserv_notice(user, "HSMSG_INVALID_MOVE", argv[1]);
+        return 0;
+    }
+}
+
+static HELPSERV_FUNC(cmd_bots) {
+    dict_iterator_t it;
+    struct helpfile_table tbl;
+    unsigned int i;
+
+    helpserv_notice(user, "HSMSG_BOTLIST_HEADER");
+
+    tbl.length = dict_size(helpserv_bots_dict)+1;
+    tbl.width = 4;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
+    tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
+    tbl.contents[0][0] = "Bot";
+    tbl.contents[0][1] = "Channel";
+    tbl.contents[0][2] = "Owner";
+    tbl.contents[0][3] = "Inactivity";
+
+    for (it=dict_first(helpserv_bots_dict), i=1; it; it=iter_next(it), i++) {
+        dict_iterator_t it2;
+        struct helpserv_bot *bot;
+        struct helpserv_user *owner=NULL;
+
+        bot = iter_data(it);
+        
+        for (it2=dict_first(bot->users); it2; it2=iter_next(it2)) {
+            if (((struct helpserv_user *)iter_data(it2))->level == HlOwner) {
+                owner = iter_data(it2);
+                break;
+            }
+        }
+
+        tbl.contents[i] = alloca(tbl.width * sizeof(**tbl.contents));
+        tbl.contents[i][0] = iter_key(it);
+        tbl.contents[i][1] = bot->helpchan->name;
+        tbl.contents[i][2] = owner ? owner->handle->handle : "None";
+        tbl.contents[i][3] = alloca(INTERVALLEN);
+        intervalString((char*)tbl.contents[i][3], now - bot->last_active);
+    }
+
+    table_send((from_opserv ? opserv : hs->helpserv), user->nick, 0, NULL, tbl);
+
+    return 1;
+}
+
+static void helpserv_page_helper_gone(struct helpserv_bot *hs, struct helpserv_request *req, const char *reason) {
+    const int from_opserv = 0;
+
+    if (!req->helper)
+        return;
+
+    /* Let the user know that their request is now unhandled */
+    if (req->user) {
+        struct modeNode *mn = GetUserMode(hs->helpchan, req->user);
+        helpserv_msguser(req->user, "HSMSG_REQ_UNASSIGNED", req->id, reason);
+        if (hs->auto_devoice && mn && (mn->modes & MODE_VOICE)) {
+            struct mod_chanmode change;
+            change.modes_set = change.modes_clear = 0;
+            change.argc = 1;
+            change.args[0].mode = MODE_REMOVE | MODE_VOICE;
+            change.args[0].member = mn;
+            mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
+        }
+        if(req->handle)
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_HELPER_GONE_1", req->id, req->user->nick, req->handle->handle, req->helper->handle->handle, reason);
+        else
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_HELPER_GONE_2", req->id, req->user->nick, req->helper->handle->handle, reason);
+    } else {
+        if(req->handle)
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_HELPER_GONE_3", req->id, req->handle->handle, req->helper->handle->handle, reason);
+        else
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_HELPER_GONE_2", req->id, req->helper->handle->handle, reason);
+    }
+
+    /* Now put it back in the queue */
+    if (hs->unhandled == NULL) {
+        /* Nothing there, put it at the front */
+        hs->unhandled = req;
+        req->next_unhandled = NULL;
+    } else {
+        /* Should it be at the front? */
+        if (hs->unhandled->opened >= req->opened) {
+            req->next_unhandled = hs->unhandled;
+            hs->unhandled = req;
+        } else {
+            struct helpserv_request *unhandled;
+            /* Find the request that this should be inserted AFTER */
+            for (unhandled=hs->unhandled; unhandled->next_unhandled && (unhandled->next_unhandled->opened < req->opened); unhandled = unhandled->next_unhandled);
+            req->next_unhandled = unhandled->next_unhandled;
+            unhandled->next_unhandled = req;
+        }
+    }
+
+    req->helper = NULL;
+}
+
+/* This takes care of WHINE_DELAY and IDLE_DELAY */
+static void run_whine_interval(void *data) {
+    struct helpserv_bot *hs=data;
+    struct helpfile_table tbl;
+    unsigned int i;
+
+    /* First, run the WHINE_DELAY */
+    if (hs->intervals[INTERVAL_WHINE_DELAY]
+        && (hs->page_types[PGSRC_ALERT] != PAGE_NONE)
+        && (hs->page_targets[PGSRC_ALERT] != NULL)
+        && (!hs->intervals[INTERVAL_EMPTY_INTERVAL] || !hs->helpchan_empty)) {
+        struct helpserv_request *unh;
+        struct helpserv_reqlist reqlist;
+        unsigned int queuesize=0;
+
+        helpserv_reqlist_init(&reqlist);
+
+        for (unh = hs->unhandled; unh; unh = unh->next_unhandled) {
+            queuesize++;
+            if ((now - unh->opened) >= (time_t)hs->intervals[INTERVAL_WHINE_DELAY]) {
+                helpserv_reqlist_append(&reqlist, unh);
+            }
+        }
+
+        if (reqlist.used) {
+            char strwhinedelay[INTERVALLEN];
+
+            intervalString(strwhinedelay, (time_t)hs->intervals[INTERVAL_WHINE_DELAY]);
+#if ANNOYING_ALERT_PAGES
+            tbl.length = reqlist.used + 1;
+            tbl.width = 4;
+            tbl.flags = TABLE_NO_FREE;
+            tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
+            tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
+            tbl.contents[0][0] = "ID#";
+            tbl.contents[0][1] = "Nick";
+            tbl.contents[0][2] = "Account";
+            tbl.contents[0][3] = "Waiting time";
+
+            for (i=1; i <= reqlist.used; i++) {
+                char reqid[12], unh_time[INTERVALLEN];
+                unh = reqlist.list[i-1];
+
+                tbl.contents[i] = alloca(tbl.width * sizeof(**tbl.contents));
+                sprintf(reqid, "%lu", unh->id);
+                tbl.contents[i][0] = strdup(reqid);
+                tbl.contents[i][1] = unh->user ? unh->user->nick : "Not online";
+                tbl.contents[i][2] = unh->handle ? unh->handle->handle : "Not authed";
+                intervalString(unh_time, now - unh->opened);
+                tbl.contents[i][3] = strdup(unh_time);
+            }
+
+            helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_WHINE_HEADER", reqlist.used, strwhinedelay, queuesize);
+            table_send(hs->helpserv, hs->page_targets[PGSRC_ALERT]->name, 0, page_type_funcs[hs->page_types[PGSRC_ALERT]], tbl);
+
+            for (i=1; i <= reqlist.used; i++) {
+                free((char *)tbl.contents[i][0]);
+                free((char *)tbl.contents[i][3]);
+            }
+#else
+            helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_WHINE_HEADER", reqlist.used, strwhinedelay, queuesize);
+#endif
+        }
+
+        helpserv_reqlist_clean(&reqlist);
+    }
+
+    /* Next run IDLE_DELAY */
+    if (hs->intervals[INTERVAL_IDLE_DELAY]
+        && (hs->page_types[PGSRC_STATUS] != PAGE_NONE)
+        && (hs->page_targets[PGSRC_STATUS] != NULL)) {
+        struct modeList mode_list;
+
+        modeList_init(&mode_list);
+
+        for (i=0; i < hs->helpchan->members.used; i++) {
+            struct modeNode *mn = hs->helpchan->members.list[i];
+            /* Ignore ops. Perhaps this should be a set option? */
+            if (mn->modes & MODE_CHANOP)
+                continue;
+            /* Check if they've been idle long enough */
+            if ((unsigned)(now - mn->idle_since) < hs->intervals[INTERVAL_IDLE_DELAY])
+                continue;
+            /* Add them to the list of idle people.. */
+            modeList_append(&mode_list, mn);
+        }
+
+        if (mode_list.used) {
+            char stridledelay[INTERVALLEN];
+
+            tbl.length = mode_list.used + 1;
+            tbl.width = 4;
+            tbl.flags = TABLE_NO_FREE;
+            tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
+            tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
+            tbl.contents[0][0] = "Nick";
+            tbl.contents[0][1] = "Account";
+            tbl.contents[0][2] = "ID#";
+            tbl.contents[0][3] = "Idle time";
+
+            for (i=1; i <= mode_list.used; i++) {
+                char reqid[12], idle_time[INTERVALLEN];
+                struct helpserv_reqlist *reqlist;
+                struct modeNode *mn = mode_list.list[i-1];
+
+                tbl.contents[i] = alloca(tbl.width * sizeof(**tbl.contents));
+                tbl.contents[i][0] = mn->user->nick;
+                tbl.contents[i][1] = mn->user->handle_info ? mn->user->handle_info->handle : "Not authed";
+
+                if ((reqlist = dict_find(helpserv_reqs_bynick_dict, mn->user->nick, NULL))) {
+                    int j;
+
+                    for (j = reqlist->used-1; j >= 0; j--) {
+                        struct helpserv_request *req = reqlist->list[j];
+
+                        if (req->hs == hs) {
+                            sprintf(reqid, "%lu", req->id);
+                            break;
+                        }
+                    }
+
+                    if (j < 0)
+                        strcpy(reqid, "None");
+                } else {
+                    strcpy(reqid, "None");
+                }
+                tbl.contents[i][2] = strdup(reqid);
+
+                intervalString(idle_time, now - mn->idle_since);
+                tbl.contents[i][3] = strdup(idle_time);
+            }
+
+            intervalString(stridledelay, (time_t)hs->intervals[INTERVAL_IDLE_DELAY]);
+            helpserv_page(PGSRC_STATUS, "HSMSG_PAGE_IDLE_HEADER", mode_list.used, hs->helpchan->name, stridledelay);
+            table_send(hs->helpserv, hs->page_targets[PGSRC_STATUS]->name, 0, page_types[hs->page_types[PGSRC_STATUS]].func, tbl);
+
+            for (i=1; i <= mode_list.used; i++) {
+                free((char *)tbl.contents[i][2]);
+                free((char *)tbl.contents[i][3]);
+            }
+        }
+
+        modeList_clean(&mode_list);
+    }
+
+    if (hs->intervals[INTERVAL_WHINE_INTERVAL]) {
+        timeq_add(now + hs->intervals[INTERVAL_WHINE_INTERVAL], run_whine_interval, hs);
+    }
+}
+
+/* Returns -1 if there's any helpers,
+ * 0 if there are no helpers
+ * >1 if there are trials (number of trials)
+ */
+static int find_helpchan_helpers(struct helpserv_bot *hs) {
+    int num_trials=0;
+    dict_iterator_t it;
+
+    for (it=dict_first(hs->users); it; it=iter_next(it)) {
+        struct helpserv_user *hs_user=iter_data(it);
+
+        if (find_handle_in_channel(hs->helpchan, hs_user->handle, NULL)) {
+            if (hs_user->level >= HlHelper) {
+                hs->helpchan_empty = 0;
+                return -1;
+            }
+            num_trials++;
+        }
+    }
+
+    hs->helpchan_empty = 1;
+    return num_trials;
+}
+
+
+static void run_empty_interval(void *data) {
+    struct helpserv_bot *hs=data;
+    int num_trials=find_helpchan_helpers(hs);
+    unsigned int num_unh;
+    struct helpserv_request *unh;
+
+    if (num_trials == -1)
+        return;
+    if (hs->req_on_join && !hs->unhandled)
+        return;
+
+    for (num_unh=0, unh=hs->unhandled; unh; num_unh++)
+        unh = unh->next_unhandled;
+
+    if (num_trials)
+        helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_ONLYTRIALALERT", hs->helpchan->name, num_trials, num_unh);
+    else
+        helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_EMPTYALERT", hs->helpchan->name, num_unh);
+
+    if (hs->intervals[INTERVAL_EMPTY_INTERVAL])
+        timeq_add(now + hs->intervals[INTERVAL_EMPTY_INTERVAL], run_empty_interval, hs);
+}
+
+static void free_user(void *data) {
+    struct helpserv_user *hs_user = data;
+    struct helpserv_bot *hs = hs_user->hs;
+    struct helpserv_userlist *userlist;
+    dict_iterator_t it;
+
+    if (hs->requests) {
+        for (it=dict_first(hs->requests); it; it=iter_next(it)) {
+            struct helpserv_request *req = iter_data(it);
+
+            if (req->helper == hs_user)
+                helpserv_page_helper_gone(hs, req, "been deleted");
+        }
+    }
+
+    userlist = dict_find(helpserv_users_byhand_dict, hs_user->handle->handle, NULL);
+    if (userlist->used == 1) {
+        dict_remove(helpserv_users_byhand_dict, hs_user->handle->handle);
+    } else {
+        helpserv_userlist_remove(userlist, hs_user);
+    }
+
+    free(data);
+}
+
+static struct helpserv_bot *register_helpserv(const char *nick, const char *help_channel, const char *registrar) {
+    struct helpserv_bot *hs;
+    struct helpserv_botlist *botlist;
+
+    /* Laziness dictates calloc, since there's a lot to set to NULL or 0, and
+     * it's a harmless default */
+    hs = calloc(1, sizeof(struct helpserv_bot));
+
+    if (!(hs->helpserv = AddService(nick, helpserv_conf.description))) {
+        free(hs);
+        return NULL;
+    }
+
+    reg_privmsg_func(hs->helpserv, helpserv_botmsg);
+
+    if (!(hs->helpchan = GetChannel(help_channel))) {
+        hs->helpchan = AddChannel(help_channel, now, NULL, NULL);
+        AddChannelUser(hs->helpserv, hs->helpchan)->modes |= MODE_CHANOP;
+    } else {
+        struct mod_chanmode change;
+        change.modes_set = change.modes_clear = 0;
+        change.argc = 1;
+        change.args[0].mode = MODE_CHANOP;
+        change.args[0].member = AddChannelUser(hs->helpserv, hs->helpchan);
+        mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
+    }
+
+    if (registrar)
+        hs->registrar = strdup(registrar);
+
+    hs->users = dict_new();
+    /* Don't free keys - they use the handle_info's handle field */
+    dict_set_free_data(hs->users, free_user);
+    hs->requests = dict_new();
+    dict_set_free_keys(hs->requests, free);
+    dict_set_free_data(hs->requests, free_request);
+
+    dict_insert(helpserv_bots_dict, hs->helpserv->nick, hs);
+
+    if (!(botlist = dict_find(helpserv_bots_bychan_dict, hs->helpchan->name, NULL))) {
+        botlist = helpserv_botlist_alloc();
+        dict_insert(helpserv_bots_bychan_dict, hs->helpchan->name, botlist);
+    }
+    helpserv_botlist_append(botlist, hs);
+
+    return hs;
+}
+
+static HELPSERV_FUNC(cmd_register) {
+    char *nick, *helpchan, reason[MAXLEN];
+    struct handle_info *handle;
+
+    REQUIRE_PARMS(4);
+    nick = argv[1];
+    if (!is_valid_nick(nick)) {
+        helpserv_notice(user, "HSMSG_ILLEGAL_NICK", nick);
+        return 0;
+    }
+    if (GetUserH(nick)) {
+        helpserv_notice(user, "HSMSG_NICK_EXISTS", nick);
+        return 0;
+    }
+    helpchan = argv[2];
+    if (!IsChannelName(helpchan)) {
+        helpserv_notice(user, "HSMSG_ILLEGAL_CHANNEL", helpchan);
+        HELPSERV_SYNTAX();
+        return 0;
+    }
+    if (opserv_bad_channel(helpchan)) {
+        helpserv_notice(user, "HSMSG_ILLEGAL_CHANNEL", helpchan);
+        return 0;
+    }
+    if (!(handle = helpserv_get_handle_info(user, argv[3])))
+        return 0;
+
+    if (!(hs = register_helpserv(nick, helpchan, user->handle_info->handle))) {
+        helpserv_notice(user, "HSMSG_ERROR_ADDING_SERVICE", nick);
+        return 0;
+    }
+
+    hs->registered = now;
+    helpserv_add_user(hs, handle, HlOwner);
+
+    helpserv_notice(user, "HSMSG_REG_SUCCESS", handle->handle, nick);
+
+    snprintf(reason, MAXLEN, "HelpServ %s (%s) registered to %s by %s.", nick, hs->helpchan->name, handle->handle, user->nick);
+    /* Not sent to helpers, since they can't register HelpServ */
+    global_message(MESSAGE_RECIPIENT_OPERS, reason);
+    return 1;
+}
+
+static void unregister_helpserv(struct helpserv_bot *hs) {
+    enum message_type msgtype;
+
+    timeq_del(0, NULL, hs, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_FUNC);
+
+    /* Requests before users so that it doesn't spam mentioning now-unhandled
+     * requests because the users were deleted */
+    dict_delete(hs->requests);
+    hs->requests = NULL; /* so we don't try to look up requests in free_user() */
+    dict_delete(hs->users);
+    free(hs->registrar);
+
+    for (msgtype=0; msgtype<MSGTYPE_COUNT; msgtype++)
+        free(hs->messages[msgtype]);
+}
+
+static void helpserv_free_bot(void *data) {
+    unregister_helpserv(data);
+    free(data);
+}
+
+static void helpserv_unregister(struct helpserv_bot *bot, const char *quit_fmt, const char *global_fmt, const char *actor) {
+    char reason[MAXLEN], channame[CHANNELLEN], botname[NICKLEN];
+    struct helpserv_botlist *botlist;
+    size_t len;
+
+    botlist = dict_find(helpserv_bots_bychan_dict, bot->helpchan->name, NULL);
+    helpserv_botlist_remove(botlist, bot);
+    if (!botlist->used)
+        dict_remove(helpserv_bots_bychan_dict, bot->helpchan->name);
+    len = strlen(bot->helpserv->nick) + 1;
+    safestrncpy(botname, bot->helpserv->nick, len);
+    len = strlen(bot->helpchan->name) + 1;
+    safestrncpy(channame, bot->helpchan->name, len);
+    snprintf(reason, sizeof(reason), quit_fmt, actor);
+    DelUser(bot->helpserv, NULL, 1, reason);
+    dict_remove(helpserv_bots_dict, botname);
+    snprintf(reason, sizeof(reason), global_fmt, botname, channame, actor);
+    global_message(MESSAGE_RECIPIENT_OPERS, reason);
+}
+
+static HELPSERV_FUNC(cmd_unregister) {
+    if (!from_opserv) {
+        if (argc < 2 || strcmp(argv[1], "CONFIRM")) {
+            helpserv_notice(user, "HSMSG_NEED_UNREG_CONFIRM");
+            return 0;
+        }
+        log_audit(HS_LOG, LOG_COMMAND, user, hs->helpserv, hs->helpchan->name, 0, "unregister CONFIRM");
+    }
+
+    helpserv_unregister(hs, "Unregistered by %s", "HelpServ %s (%s) unregistered by %s.", user->nick);
+    return from_opserv;
+}
+
+static HELPSERV_FUNC(cmd_expire) {
+    struct helpserv_botlist victims;
+    struct helpserv_bot *bot;
+    dict_iterator_t it, next;
+    unsigned int count = 0;
+
+    memset(&victims, 0, sizeof(victims));
+    for (it = dict_first(helpserv_bots_dict); it; it = next) {
+        bot = iter_data(it);
+        next = iter_next(it);
+        if ((unsigned int)(now - bot->last_active) < helpserv_conf.expire_age)
+            continue;
+        helpserv_unregister(bot, "Registration expired due to inactivity", "HelpServ %s (%s) expired at request of %s.", user->nick);
+        count++;
+    }
+    helpserv_notice(user, "HSMSG_EXPIRATION_DONE", count);
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_giveownership) {
+    struct handle_info *hi;
+    struct helpserv_user *new_owner, *old_owner, *hs_user;
+    dict_iterator_t it;
+    char reason[MAXLEN];
+
+    if (!from_opserv && ((argc < 3) || strcmp(argv[2], "CONFIRM"))) {
+        helpserv_notice(user, "HSMSG_NEED_GIVEOWNERSHIP_CONFIRM");
+        return 0;
+    }
+    hi = helpserv_get_handle_info(user, argv[1]);
+    if (!hi)
+        return 0;
+    new_owner = GetHSUser(hs, hi);
+    if (!new_owner) {
+        helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", hi->handle, hs->helpserv->nick);
+        return 0;
+    }
+    if (!from_opserv)
+        old_owner = GetHSUser(hs, user->handle_info);
+    else for (it = dict_first(hs->users), old_owner = NULL; it; it = iter_next(it)) {
+        hs_user = iter_data(it);
+        if (hs_user->level != HlOwner)
+            continue;
+        if (old_owner) {
+            helpserv_notice(user, "HSMSG_MULTIPLE_OWNERS", hs->helpserv->nick);
+            return 0;
+        }
+        old_owner = hs_user;
+    }
+    if (!from_opserv && (new_owner->handle == user->handle_info)) {
+        helpserv_notice(user, "HSMSG_NO_TRANSFER_SELF");
+        return 0;
+    }
+    if (old_owner)
+        old_owner->level = HlManager;
+    new_owner->level = HlOwner;
+    helpserv_notice(user, "HSMSG_OWNERSHIP_GIVEN", hs->helpserv->nick, new_owner->handle->handle);
+    sprintf(reason, "%s (%s) ownership transferred to %s by %s.", hs->helpserv->nick, hs->helpchan->name, new_owner->handle->handle, user->handle_info->handle);
+    return 1;
+}
+
+static HELPSERV_FUNC(cmd_weekstart) {
+    struct handle_info *hi;
+    struct helpserv_user *actor, *victim;
+    int changed = 0;
+
+    REQUIRE_PARMS(2);
+    actor = from_opserv ? NULL : GetHSUser(hs, user->handle_info);
+    if (!(hi = helpserv_get_handle_info(user, argv[1])))
+        return 0;
+    if (!(victim = GetHSUser(hs, hi))) {
+        helpserv_notice(user, "HSMSG_NOT_IN_USERLIST", hi->handle, hs->helpserv->nick);
+        return 0;
+    }
+    if (actor && (actor->level <= victim->level) && (actor != victim)) {
+        helpserv_notice(user, "MSG_USER_OUTRANKED", victim->handle->handle);
+        return 0;
+    }
+    if (argc > 2 && (!actor || actor->level >= HlManager)) {
+        int new_day = 7;
+        switch (argv[2][0]) {
+        case 's': case 'S':
+            if ((argv[2][1] == 'u') || (argv[2][1] == 'U'))
+                new_day = 0;
+            else if ((argv[2][1] == 'a') || (argv[2][1] == 'A'))
+                new_day = 6;
+            break;
+        case 'm': case 'M': new_day = 1; break;
+        case 't': case 'T':
+            if ((argv[2][1] == 'u') || (argv[2][1] == 'U'))
+                new_day = 2;
+            else if ((argv[2][1] == 'h') || (argv[2][1] == 'H'))
+                new_day = 4;
+            break;
+        case 'w': case 'W': new_day = 3; break;
+        case 'f': case 'F': new_day = 5; break;
+        }
+        if (new_day == 7) {
+            helpserv_notice(user, "HSMSG_BAD_WEEKDAY", argv[2]);
+            return 0;
+        }
+        victim->week_start = new_day;
+        changed = 1;
+    }
+    helpserv_notice(user, "HSMSG_WEEK_STARTS", victim->handle->handle, weekday_names[victim->week_start]);
+    return changed;
+}
+
+static void set_page_target(struct helpserv_bot *hs, enum page_source idx, const char *target) {
+    struct chanNode *new_target, *old_target;
+
+    if (target) {
+        if (!IsChannelName(target)) {
+            log_module(HS_LOG, LOG_ERROR, "%s has an invalid page target.", hs->helpserv->nick);
+            return;
+        }
+        new_target = GetChannel(target);
+        if (!new_target) {
+            new_target = AddChannel(target, now, NULL, NULL);
+            AddChannelUser(hs->helpserv, new_target);
+        }
+    } else {
+        new_target = NULL;
+    }
+    if (new_target == hs->page_targets[idx])
+        return;
+    old_target = hs->page_targets[idx];
+    hs->page_targets[idx] = NULL;
+    if (old_target && !helpserv_in_channel(hs, old_target))
+        DelChannelUser(hs->helpserv, old_target, "Changing page target.", 0);
+    if (new_target && !helpserv_in_channel(hs, new_target)) {
+        struct mod_chanmode change;
+        change.modes_set = change.modes_clear = 0;
+        change.argc = 1;
+        change.args[0].mode = MODE_CHANOP;
+        change.args[0].member = AddChannelUser(hs->helpserv, new_target);
+        mod_chanmode_announce(hs->helpserv, new_target, &change);
+    }
+    hs->page_targets[idx] = new_target;
+}
+
+static int opt_page_target(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum page_source idx) {
+    int changed = 0;
+
+    if (argc > 0) {
+        if (!IsOper(user)) {
+            helpserv_notice(user, "HSMSG_SET_NEED_OPER");
+            return 0;
+        }
+        if (!strcmp(argv[0], "*")) {
+            set_page_target(hs, idx, NULL);
+            changed = 1;
+        } else if (!IsChannelName(argv[0])) {
+            helpserv_notice(user, "MSG_NOT_CHANNEL_NAME");
+            return 0;
+        } else {
+            set_page_target(hs, idx, argv[0]);
+            changed = 1;
+        }
+    }
+    if (hs->page_targets[idx])
+        helpserv_notice(user, page_sources[idx].print_target, hs->page_targets[idx]->name);
+    else
+        helpserv_notice(user, page_sources[idx].print_target, user_find_message(user, "MSG_NONE"));
+    return changed;
+}
+
+static HELPSERV_OPTION(opt_pagetarget_command) {
+    return opt_page_target(user, hs, from_opserv, argc, argv, PGSRC_COMMAND);
+}
+
+static HELPSERV_OPTION(opt_pagetarget_alert) {
+    return opt_page_target(user, hs, from_opserv, argc, argv, PGSRC_ALERT);
+}
+
+static HELPSERV_OPTION(opt_pagetarget_status) {
+    return opt_page_target(user, hs, from_opserv, argc, argv, PGSRC_STATUS);
+}
+
+static enum page_type page_type_from_name(const char *name) {
+    enum page_type type;
+    for (type=0; type<PAGE_COUNT; type++)
+        if (!irccasecmp(page_types[type].db_name, name))
+            return type;
+    return PAGE_COUNT;
+}
+
+static int opt_page_type(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum page_source idx) {
+    enum page_type new_type;
+    int changed=0;
+
+    if (argc > 0) {
+        new_type = page_type_from_name(argv[0]);
+        if (new_type == PAGE_COUNT) {
+            helpserv_notice(user, "HSMSG_INVALID_OPTION", argv[0]);
+            return 0;
+        }
+        hs->page_types[idx] = new_type;
+        changed = 1;
+    }
+    helpserv_notice(user, page_sources[idx].print_type,
+                    user_find_message(user, page_types[hs->page_types[idx]].print_name));
+    return changed;
+}
+
+static HELPSERV_OPTION(opt_pagetype) {
+    return opt_page_type(user, hs, from_opserv, argc, argv, PGSRC_COMMAND);
+}
+
+static HELPSERV_OPTION(opt_alert_page_type) {
+    return opt_page_type(user, hs, from_opserv, argc, argv, PGSRC_ALERT);
+}
+
+static HELPSERV_OPTION(opt_status_page_type) {
+    return opt_page_type(user, hs, from_opserv, argc, argv, PGSRC_STATUS);
+}
+
+static int opt_message(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum message_type idx) {
+    int changed=0;
+
+    if (argc > 0) {
+        char *msg = unsplit_string(argv, argc, NULL);
+        free(hs->messages[idx]);
+        hs->messages[idx] = strcmp(msg, "*") ? strdup(msg) : NULL;
+        changed = 1;
+    }
+    if (hs->messages[idx])
+        helpserv_notice(user, message_types[idx].print_name, hs->messages[idx]);
+    else
+        helpserv_notice(user, message_types[idx].print_name, user_find_message(user, "MSG_NONE"));
+    return changed;
+}
+
+static HELPSERV_OPTION(opt_greeting) {
+    return opt_message(user, hs, from_opserv, argc, argv, MSGTYPE_GREETING);
+}
+
+static HELPSERV_OPTION(opt_req_opened) {
+    return opt_message(user, hs, from_opserv, argc, argv, MSGTYPE_REQ_OPENED);
+}
+
+static HELPSERV_OPTION(opt_req_assigned) {
+    return opt_message(user, hs, from_opserv, argc, argv, MSGTYPE_REQ_ASSIGNED);
+}
+
+static HELPSERV_OPTION(opt_req_closed) {
+    return opt_message(user, hs, from_opserv, argc, argv, MSGTYPE_REQ_CLOSED);
+}
+
+static int opt_interval(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum interval_type idx, unsigned int min) {
+    char buf[INTERVALLEN];
+    int changed=0;
+
+    if (argc > 0) {
+        unsigned long new_int = ParseInterval(argv[0]);
+        if (!new_int && strcmp(argv[0], "0")) {
+            helpserv_notice(user, "MSG_INVALID_DURATION", argv[0]);
+            return 0;
+        }
+        if (new_int && new_int < min) {
+            intervalString(buf, min);
+            helpserv_notice(user, "HSMSG_INVALID_INTERVAL", user_find_message(user, interval_types[idx].print_name), buf);
+            return 0;
+        }
+        hs->intervals[idx] = new_int;
+        changed = 1;
+    }
+    if (hs->intervals[idx]) {
+        intervalString(buf, hs->intervals[idx]);
+        helpserv_notice(user, interval_types[idx].print_name, buf);
+    } else
+        helpserv_notice(user, interval_types[idx].print_name, user_find_message(user, "HSMSG_0_DISABLED"));
+    return changed;
+}
+
+static HELPSERV_OPTION(opt_idle_delay) {
+    return opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_IDLE_DELAY, 60);
+}
+
+static HELPSERV_OPTION(opt_whine_delay) {
+    return opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_WHINE_DELAY, 60);
+}
+
+static HELPSERV_OPTION(opt_whine_interval) {
+    unsigned int old_val = hs->intervals[INTERVAL_WHINE_INTERVAL];
+    int retval;
+
+    retval = opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_WHINE_INTERVAL, 60);
+
+    if (!old_val && hs->intervals[INTERVAL_WHINE_INTERVAL]) {
+        timeq_add(now + hs->intervals[INTERVAL_WHINE_INTERVAL], run_whine_interval, hs);
+    } else if (old_val && !hs->intervals[INTERVAL_WHINE_INTERVAL]) {
+        timeq_del(0, run_whine_interval, hs, TIMEQ_IGNORE_WHEN);
+    }
+
+    return retval;
+}
+
+static HELPSERV_OPTION(opt_empty_interval) {
+    unsigned int old_val = hs->intervals[INTERVAL_EMPTY_INTERVAL];
+    int retval;
+
+    retval = opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_EMPTY_INTERVAL, 60);
+
+    if (!old_val && hs->intervals[INTERVAL_EMPTY_INTERVAL]) {
+        timeq_add(now + hs->intervals[INTERVAL_EMPTY_INTERVAL], run_empty_interval, hs);
+    } else if (old_val && !hs->intervals[INTERVAL_EMPTY_INTERVAL]) {
+        timeq_del(0, run_empty_interval, hs, TIMEQ_IGNORE_WHEN);
+    }
+
+    return retval;
+}
+
+static HELPSERV_OPTION(opt_stale_delay) {
+    return opt_interval(user, hs, from_opserv, argc, argv, INTERVAL_STALE_DELAY, 60);
+}
+
+static enum persistence_length persistence_from_name(const char *name) {
+    enum persistence_length pers;
+    for (pers=0; pers<PERSIST_COUNT; pers++)
+        if (!irccasecmp(name, persistence_lengths[pers].db_name))
+            return pers;
+    return PERSIST_COUNT;
+}
+
+static int opt_persist(struct userNode *user, struct helpserv_bot *hs, int from_opserv, int argc, char *argv[], enum persistence_type idx) {
+    int changed=0;
+
+    if (argc > 0) {
+        enum persistence_length new_pers = persistence_from_name(argv[0]);
+        if (new_pers == PERSIST_COUNT) {
+            helpserv_notice(user, "HSMSG_INVALID_OPTION", argv[0]);
+            return 0;
+        }
+        hs->persist_types[idx] = new_pers;
+        changed = 1;
+    }
+    helpserv_notice(user, persistence_types[idx].print_name,
+                    user_find_message(user, persistence_lengths[hs->persist_types[idx]].print_name));
+    return changed;
+}
+
+static HELPSERV_OPTION(opt_request_persistence) {
+    return opt_persist(user, hs, from_opserv, argc, argv, PERSIST_T_REQUEST);
+}
+
+static HELPSERV_OPTION(opt_helper_persistence) {
+    return opt_persist(user, hs, from_opserv, argc, argv, PERSIST_T_HELPER);
+}
+
+static enum notification_type notification_from_name(const char *name) {
+    enum notification_type notify;
+    for (notify=0; notify<NOTIFY_COUNT; notify++)
+        if (!irccasecmp(name, notification_types[notify].db_name))
+            return notify;
+    return NOTIFY_COUNT;
+}
+
+static HELPSERV_OPTION(opt_notification) {
+    int changed=0;
+
+    if (argc > 0) {
+        enum notification_type new_notify = notification_from_name(argv[0]);
+        if (new_notify == NOTIFY_COUNT) {
+            helpserv_notice(user, "HSMSG_INVALID_OPTION", argv[0]);
+            return 0;
+        }
+        if (!from_opserv && (new_notify == NOTIFY_HANDLE)) {
+            helpserv_notice(user, "HSMSG_SET_NEED_OPER");
+            return 0;
+        }
+        hs->notify = new_notify;
+        changed = 1;
+    }
+    helpserv_notice(user, "HSMSG_SET_NOTIFICATION", user_find_message(user, notification_types[hs->notify].print_name));
+    return changed;
+}
+
+#define OPTION_UINT(var, name) do { \
+    int changed=0; \
+    if (argc > 0) { \
+        (var) = strtoul(argv[0], NULL, 0); \
+        changed = 1; \
+    } \
+    helpserv_notice(user, name, (var)); \
+    return changed; \
+} while (0);
+
+static HELPSERV_OPTION(opt_id_wrap) {
+    OPTION_UINT(hs->id_wrap, "HSMSG_SET_IDWRAP");
+}
+
+static HELPSERV_OPTION(opt_req_maxlen) {
+    OPTION_UINT(hs->req_maxlen, "HSMSG_SET_REQMAXLEN");
+}
+
+#define OPTION_BINARY(var, name) do { \
+    int changed=0; \
+    if (argc > 0) { \
+        if (enabled_string(argv[0])) { \
+            (var) = 1; \
+            changed = 1; \
+        } else if (disabled_string(argv[0])) { \
+            (var) = 0; \
+            changed = 1; \
+        } else { \
+            helpserv_notice(user, "MSG_INVALID_BINARY", argv[0]); \
+            return 0; \
+        } \
+    } \
+    helpserv_notice(user, name, user_find_message(user, (var) ? "MSG_ON" : "MSG_OFF")); \
+    return changed; \
+} while (0);
+
+static HELPSERV_OPTION(opt_privmsg_only) {
+    OPTION_BINARY(hs->privmsg_only, "HSMSG_SET_PRIVMSGONLY");
+}
+
+static HELPSERV_OPTION(opt_req_on_join) {
+    OPTION_BINARY(hs->req_on_join, "HSMSG_SET_REQONJOIN");
+}
+
+static HELPSERV_OPTION(opt_auto_voice) {
+    OPTION_BINARY(hs->auto_voice, "HSMSG_SET_AUTOVOICE");
+}
+
+static HELPSERV_OPTION(opt_auto_devoice) {
+    OPTION_BINARY(hs->auto_devoice, "HSMSG_SET_AUTODEVOICE");
+}
+
+static HELPSERV_FUNC(cmd_set) {
+    helpserv_option_func_t *opt;
+
+    if (argc < 2) {
+        unsigned int i;
+        helpserv_option_func_t *display[] = {
+            opt_pagetarget_command, opt_pagetarget_alert, opt_pagetarget_status,
+            opt_pagetype, opt_alert_page_type, opt_status_page_type,
+            opt_greeting, opt_req_opened, opt_req_assigned, opt_req_closed,
+            opt_idle_delay, opt_whine_delay, opt_whine_interval,
+            opt_empty_interval, opt_stale_delay, opt_request_persistence,
+            opt_helper_persistence, opt_notification, opt_id_wrap,
+            opt_req_maxlen, opt_privmsg_only, opt_req_on_join, opt_auto_voice,
+            opt_auto_devoice
+        };
+
+        helpserv_notice(user, "HSMSG_QUEUE_OPTIONS");
+        for (i=0; i<ArrayLength(display); i++)
+            display[i](user, hs, from_opserv, 0, argv);
+        return 1;
+    }
+
+    if (!(opt = dict_find(helpserv_option_dict, argv[1], NULL))) {
+        helpserv_notice(user, "HSMSG_INVALID_OPTION", argv[1]);
+        return 0;
+    }
+
+    if ((argc > 2) && !from_opserv) {
+        struct helpserv_user *hs_user;
+
+        if (!(hs_user = dict_find(hs->users, user->handle_info->handle, NULL))) {
+            helpserv_notice(user, "HSMSG_WTF_WHO_ARE_YOU", hs->helpserv->nick);
+            return 0;
+        }
+
+        if (hs_user->level < HlManager) {
+            helpserv_notice(user, "HSMSG_NEED_MANAGER");
+            return 0;
+        }
+    }
+    return opt(user, hs, from_opserv, argc-2, argv+2);
+}
+
+static int user_write_helper(const char *key, void *data, void *extra) {
+    struct helpserv_user *hs_user = data;
+    struct saxdb_context *ctx = extra;
+    struct string_list strlist;
+    char str[5][16], *strs[5];
+    unsigned int i;
+
+    saxdb_start_record(ctx, key, 0);
+    /* Helper identification. */
+    saxdb_write_string(ctx, KEY_HELPER_LEVEL, helpserv_level2str(hs_user->level));
+    saxdb_write_string(ctx, KEY_HELPER_HELPMODE, (hs_user->help_mode ? "1" : "0"));
+    saxdb_write_int(ctx, KEY_HELPER_WEEKSTART, hs_user->week_start);
+    /* Helper stats */
+    saxdb_start_record(ctx, KEY_HELPER_STATS, 0);
+    for (i=0; i < ArrayLength(strs); ++i)
+        strs[i] = str[i];
+    strlist.list = strs;
+    strlist.used = 5;
+    /* Time in help channel */
+    for (i=0; i < strlist.used; i++) {
+        unsigned int week_time = hs_user->time_per_week[i];
+        if ((i==0 || i==4) && hs_user->join_time)
+            week_time += now - hs_user->join_time;
+        sprintf(str[i], "%u", week_time);
+    }
+    saxdb_write_string_list(ctx, KEY_HELPER_STATS_TIME, &strlist);
+    /* Requests picked up */
+    for (i=0; i < strlist.used; i++)
+        sprintf(str[i], "%u", hs_user->picked_up[i]);
+    saxdb_write_string_list(ctx, KEY_HELPER_STATS_PICKUP, &strlist);
+    /* Requests closed */
+    for (i=0; i < strlist.used; i++)
+        sprintf(str[i], "%u", hs_user->closed[i]);
+    saxdb_write_string_list(ctx, KEY_HELPER_STATS_CLOSE, &strlist);
+    /* Requests reassigned from user */
+    for (i=0; i < strlist.used; i++)
+        sprintf(str[i], "%u", hs_user->reassigned_from[i]);
+    saxdb_write_string_list(ctx, KEY_HELPER_STATS_REASSIGNFROM, &strlist);
+    /* Requests reassigned to user */
+    for (i=0; i < strlist.used; i++)
+        sprintf(str[i], "%u", hs_user->reassigned_to[i]);
+    saxdb_write_string_list(ctx, KEY_HELPER_STATS_REASSIGNTO, &strlist);
+    /* End of stats and whole record. */
+    saxdb_end_record(ctx);
+    saxdb_end_record(ctx);
+    return 0;
+}
+
+static int user_read_helper(const char *key, void *data, void *extra) {
+    struct record_data *rd = data;
+    struct helpserv_bot *hs = extra;
+    struct helpserv_user *hs_user;
+    struct handle_info *handle;
+    dict_t stats;
+    enum helpserv_level level;
+    char *str;
+    struct string_list *strlist;
+    unsigned int i;
+
+    if (rd->type != RECDB_OBJECT || !dict_size(rd->d.object)) {
+        log_module(HS_LOG, LOG_ERROR, "Invalid user %s for %s.", key, hs->helpserv->nick);
+        return 0;
+    }
+
+    if (!(handle = get_handle_info(key))) {
+        log_module(HS_LOG, LOG_ERROR, "Nonexistant account %s for %s.", key, hs->helpserv->nick);
+        return 0;
+    }
+    str = database_get_data(rd->d.object, KEY_HELPER_LEVEL, RECDB_QSTRING);
+    if (str) {
+        level = helpserv_str2level(str);
+        if (level == HlNone) {
+            log_module(HS_LOG, LOG_ERROR, "Account %s has invalid level %s.", key, str);
+            return 0;
+        }
+    } else {
+        log_module(HS_LOG, LOG_ERROR, "Account %s has no level field for %s.", key, hs->helpserv->nick);
+        return 0;
+    }
+
+    hs_user = helpserv_add_user(hs, handle, level);
+
+    str = database_get_data(rd->d.object, KEY_HELPER_HELPMODE, RECDB_QSTRING);
+    hs_user->help_mode = (str && strtol(str, NULL, 0)) ? 1 : 0;
+    str = database_get_data(rd->d.object, KEY_HELPER_WEEKSTART, RECDB_QSTRING);
+    hs_user->week_start = str ? strtol(str, NULL, 0) : 0;
+
+    /* Stats */
+    stats = database_get_data(GET_RECORD_OBJECT(rd), KEY_HELPER_STATS, RECDB_OBJECT);
+
+    if (stats) {
+        /* The tests for strlist->used are for converting the old format to the new one */
+        strlist = database_get_data(stats, KEY_HELPER_STATS_TIME, RECDB_STRING_LIST);
+        if (strlist) {
+            for (i=0; i < 5 && i < strlist->used; i++)
+                hs_user->time_per_week[i] = strtoul(strlist->list[i], NULL, 0);
+            if (strlist->used == 4)
+                hs_user->time_per_week[4] = hs_user->time_per_week[0]+hs_user->time_per_week[1]+hs_user->time_per_week[2]+hs_user->time_per_week[3];
+        }
+        strlist = database_get_data(stats, KEY_HELPER_STATS_PICKUP, RECDB_STRING_LIST);
+        if (strlist) {
+            for (i=0; i < 5 && i < strlist->used; i++)
+                hs_user->picked_up[i] = strtoul(strlist->list[i], NULL, 0);
+            if (strlist->used == 2)
+                hs_user->picked_up[4] = hs_user->picked_up[0]+hs_user->picked_up[1];
+        }
+        strlist = database_get_data(stats, KEY_HELPER_STATS_CLOSE, RECDB_STRING_LIST);
+        if (strlist) {
+            for (i=0; i < 5 && i < strlist->used; i++)
+                hs_user->closed[i] = strtoul(strlist->list[i], NULL, 0);
+            if (strlist->used == 2)
+                hs_user->closed[4] = hs_user->closed[0]+hs_user->closed[1];
+        }
+        strlist = database_get_data(stats, KEY_HELPER_STATS_REASSIGNFROM, RECDB_STRING_LIST);
+        if (strlist) {
+            for (i=0; i < 5 && i < strlist->used; i++)
+                hs_user->reassigned_from[i] = strtoul(strlist->list[i], NULL, 0);
+            if (strlist->used == 2)
+                hs_user->reassigned_from[4] = hs_user->reassigned_from[0]+hs_user->reassigned_from[1];
+        }
+        strlist = database_get_data(stats, KEY_HELPER_STATS_REASSIGNTO, RECDB_STRING_LIST);
+        if (strlist) {
+            for (i=0; i < 5 && i < strlist->used; i++)
+                hs_user->reassigned_to[i] = strtoul(strlist->list[i], NULL, 0);
+            if (strlist->used == 2)
+                hs_user->reassigned_to[4] = hs_user->reassigned_to[0]+hs_user->reassigned_to[1];
+        }
+    }
+
+    return 0;
+}
+
+static int request_write_helper(const char *key, void *data, void *extra) {
+    struct helpserv_request *request = data;
+    struct saxdb_context *ctx = extra;
+
+    if (!request->handle)
+        return 0;
+
+    saxdb_start_record(ctx, key, 0);
+    if (request->helper) {
+        saxdb_write_string(ctx, KEY_REQUEST_HELPER, request->helper->handle->handle);
+        saxdb_write_int(ctx, KEY_REQUEST_ASSIGNED, request->assigned);
+    }
+    saxdb_write_string(ctx, KEY_REQUEST_HANDLE, request->handle->handle);
+    saxdb_write_int(ctx, KEY_REQUEST_OPENED, request->opened);
+    saxdb_write_string_list(ctx, KEY_REQUEST_TEXT, request->text);
+    saxdb_end_record(ctx);
+    return 0;
+}
+
+static int request_read_helper(const char *key, void *data, void *extra) {
+    struct record_data *rd = data;
+    struct helpserv_bot *hs = extra;
+    struct helpserv_request *request;
+    struct string_list *strlist;
+    char *str;
+
+    if (rd->type != RECDB_OBJECT || !dict_size(rd->d.object)) {
+        log_module(HS_LOG, LOG_ERROR, "Invalid request %s:%s.", hs->helpserv->nick, key);
+        return 0;
+    }
+
+    request = calloc(1, sizeof(struct helpserv_request));
+
+    request->id = strtoul(key, NULL, 0);
+    request->hs = hs;
+    request->user = NULL;
+    request->parent_nick_list = request->parent_hand_list = NULL;
+
+    str = database_get_data(rd->d.object, KEY_REQUEST_HANDLE, RECDB_QSTRING);
+    if (!str || !(request->handle = get_handle_info(str))) {
+        log_module(HS_LOG, LOG_ERROR, "Request %s:%s has an invalid or nonexistant account.", hs->helpserv->nick, key);
+        free(request);
+        return 0;
+    }
+    if (!(request->parent_hand_list = dict_find(helpserv_reqs_byhand_dict, request->handle->handle, NULL))) {
+        request->parent_hand_list = helpserv_reqlist_alloc();
+        dict_insert(helpserv_reqs_byhand_dict, request->handle->handle, request->parent_hand_list);
+    }
+    helpserv_reqlist_append(request->parent_hand_list, request);
+
+    str = database_get_data(rd->d.object, KEY_REQUEST_OPENED, RECDB_QSTRING);
+    if (!str) {
+        log_module(HS_LOG, LOG_ERROR, "Request %s:%s has a nonexistant opening time. Using time(NULL).", hs->helpserv->nick, key);
+        request->opened = time(NULL);
+    } else {
+        request->opened = (time_t)strtoul(str, NULL, 0);
+    }
+
+    str = database_get_data(rd->d.object, KEY_REQUEST_ASSIGNED, RECDB_QSTRING);
+    if (str)
+        request->assigned = (time_t)strtoul(str, NULL, 0);
+
+    str = database_get_data(rd->d.object, KEY_REQUEST_HELPER, RECDB_QSTRING);
+    if (str) {
+        if (!(request->helper = dict_find(hs->users, str, NULL))) {
+            log_module(HS_LOG, LOG_ERROR, "Request %s:%s has an invalid or nonexistant helper.", hs->helpserv->nick, key);
+            free(request);
+            return 0;
+        }
+    } else {
+        if (!hs->unhandled) {
+            request->next_unhandled = NULL;
+            hs->unhandled = request;
+        } else if (hs->unhandled->opened > request->opened) {
+            request->next_unhandled = hs->unhandled;
+            hs->unhandled = request;
+        } else {
+            struct helpserv_request *unh;
+            for (unh = hs->unhandled; unh->next_unhandled && (unh->next_unhandled->opened < request->opened); unh = unh->next_unhandled);
+            request->next_unhandled = unh->next_unhandled;
+            unh->next_unhandled = request;
+        }
+    }
+
+    strlist = database_get_data(rd->d.object, KEY_REQUEST_TEXT, RECDB_STRING_LIST);
+    if (!strlist) {
+        log_module(HS_LOG, LOG_ERROR, "Request %s:%s has no text.", hs->helpserv->nick, key);
+        free(request);
+        return 0;
+    }
+    request->text = string_list_copy(strlist);
+
+    dict_insert(hs->requests, strdup(key), request);
+
+    return 0;
+}
+
+static int
+helpserv_bot_write(const char *key, void *data, void *extra) {
+    const struct helpserv_bot *hs = data;
+    struct saxdb_context *ctx = extra;
+    enum page_source pagesrc;
+    enum message_type msgtype;
+    enum interval_type inttype;
+    enum persistence_type persisttype;
+    struct string_list *slist;
+
+    /* Entire bot */
+    saxdb_start_record(ctx, key, 1);
+
+    /* Helper list */
+    saxdb_start_record(ctx, KEY_HELPERS, 1);
+    dict_foreach(hs->users, user_write_helper, ctx);
+    saxdb_end_record(ctx);
+
+    /* Open requests */
+    if (hs->persist_types[PERSIST_T_REQUEST] == PERSIST_CLOSE) {
+        saxdb_start_record(ctx, KEY_REQUESTS, 0);
+        dict_foreach(hs->requests, request_write_helper, ctx);
+        saxdb_end_record(ctx);
+    }
+
+    /* Other settings and state */
+    saxdb_write_string(ctx, KEY_HELP_CHANNEL, hs->helpchan->name);
+    slist = alloc_string_list(PGSRC_COUNT);
+    for (pagesrc=0; pagesrc<PGSRC_COUNT; pagesrc++) {
+        struct chanNode *target = hs->page_targets[pagesrc];
+        string_list_append(slist, strdup(target ? target->name : "*"));
+    }
+    saxdb_write_string_list(ctx, KEY_PAGE_DEST, slist);
+    free_string_list(slist);
+    for (pagesrc=0; pagesrc<PGSRC_COUNT; pagesrc++) {
+        const char *src = page_types[hs->page_types[pagesrc]].db_name;
+        saxdb_write_string(ctx, page_sources[pagesrc].db_name, src);
+    }
+    for (msgtype=0; msgtype<MSGTYPE_COUNT; msgtype++) {
+        const char *msg = hs->messages[msgtype];
+        if (msg)
+            saxdb_write_string(ctx, message_types[msgtype].db_name, msg);
+    }
+    for (inttype=0; inttype<INTERVAL_COUNT; inttype++) {
+        if (!hs->intervals[inttype])
+            continue;
+        saxdb_write_int(ctx, interval_types[inttype].db_name, hs->intervals[inttype]);
+    }
+    for (persisttype=0; persisttype<PERSIST_T_COUNT; persisttype++) {
+        const char *persist = persistence_lengths[hs->persist_types[persisttype]].db_name;
+        saxdb_write_string(ctx, persistence_types[persisttype].db_name, persist);
+    }
+    saxdb_write_string(ctx, KEY_NOTIFICATION, notification_types[hs->notify].db_name);
+    saxdb_write_int(ctx, KEY_REGISTERED, hs->registered);
+    saxdb_write_int(ctx, KEY_IDWRAP, hs->id_wrap);
+    saxdb_write_int(ctx, KEY_REQ_MAXLEN, hs->req_maxlen);
+    saxdb_write_int(ctx, KEY_LAST_REQUESTID, hs->last_requestid);
+    if (hs->registrar)
+        saxdb_write_string(ctx, KEY_REGISTRAR, hs->registrar);
+    saxdb_write_int(ctx, KEY_PRIVMSG_ONLY, hs->privmsg_only);
+    saxdb_write_int(ctx, KEY_REQ_ON_JOIN, hs->req_on_join);
+    saxdb_write_int(ctx, KEY_AUTO_VOICE, hs->auto_voice);
+    saxdb_write_int(ctx, KEY_AUTO_DEVOICE, hs->auto_devoice);
+    saxdb_write_int(ctx, KEY_LAST_ACTIVE, hs->last_active);
+
+    /* End bot record */
+    saxdb_end_record(ctx);
+    return 0;
+}
+
+static int
+helpserv_saxdb_write(struct saxdb_context *ctx) {
+    saxdb_start_record(ctx, KEY_BOTS, 1);
+    dict_foreach(helpserv_bots_dict, helpserv_bot_write, ctx);
+    saxdb_end_record(ctx);
+    saxdb_write_int(ctx, KEY_LAST_STATS_UPDATE, last_stats_update);
+    return 0;
+}
+
+static int helpserv_bot_read(const char *key, void *data, UNUSED_ARG(void *extra)) {
+    struct record_data *br = data, *raw_record;
+    struct helpserv_bot *hs;
+    char *registrar, *helpchannel_name, *str;
+    dict_t users, requests;
+    enum page_source pagesrc;
+    enum message_type msgtype;
+    enum interval_type inttype;
+    enum persistence_type persisttype;
+
+    users = database_get_data(GET_RECORD_OBJECT(br), KEY_HELPERS, RECDB_OBJECT);
+    if (!users) {
+        log_module(HS_LOG, LOG_ERROR, "%s has no users.", key);
+        return 0;
+    }
+    helpchannel_name = database_get_data(GET_RECORD_OBJECT(br), KEY_HELP_CHANNEL, RECDB_QSTRING);
+    if (!helpchannel_name || !IsChannelName(helpchannel_name)) {
+        log_module(HS_LOG, LOG_ERROR, "%s has an invalid channel name.", key);
+        return 0;
+    }
+    registrar = database_get_data(GET_RECORD_OBJECT(br), KEY_REGISTRAR, RECDB_QSTRING);
+
+    hs = register_helpserv(key, helpchannel_name, registrar);
+
+    raw_record = dict_find(GET_RECORD_OBJECT(br), KEY_PAGE_DEST, NULL);
+    switch (raw_record ? raw_record->type : RECDB_INVALID) {
+    case RECDB_QSTRING:
+        set_page_target(hs, PGSRC_COMMAND, GET_RECORD_QSTRING(raw_record));
+        pagesrc = PGSRC_COMMAND + 1;
+        break;
+    case RECDB_STRING_LIST: {
+        struct string_list *slist = GET_RECORD_STRING_LIST(raw_record);
+        for (pagesrc=0; (pagesrc<slist->used) && (pagesrc<PGSRC_COUNT); pagesrc++) {
+            const char *dest = slist->list[pagesrc];
+            set_page_target(hs, pagesrc, strcmp(dest, "*") ? dest : NULL);
+        }
+        break;
+    }
+    default:
+        set_page_target(hs, PGSRC_COMMAND, NULL);
+        pagesrc = PGSRC_COMMAND + 1;
+        break;
+    }
+    while (pagesrc < PGSRC_COUNT) {
+        set_page_target(hs, pagesrc++, hs->page_targets[PGSRC_COMMAND] ? hs->page_targets[PGSRC_COMMAND]->name : NULL);
+    }
+
+    for (pagesrc=0; pagesrc<PGSRC_COUNT; pagesrc++) {
+        str = database_get_data(GET_RECORD_OBJECT(br), page_sources[pagesrc].db_name, RECDB_QSTRING);
+        hs->page_types[pagesrc] = str ? page_type_from_name(str) : PAGE_NONE;
+    }
+
+    for (msgtype=0; msgtype<MSGTYPE_COUNT; msgtype++) {
+        str = database_get_data(GET_RECORD_OBJECT(br), message_types[msgtype].db_name, RECDB_QSTRING);
+        hs->messages[msgtype] = str ? strdup(str) : NULL;
+    }
+
+    for (inttype=0; inttype<INTERVAL_COUNT; inttype++) {
+        str = database_get_data(GET_RECORD_OBJECT(br), interval_types[inttype].db_name, RECDB_QSTRING);
+        hs->intervals[inttype] = str ? ParseInterval(str) : 0;
+    }
+    if (hs->intervals[INTERVAL_WHINE_INTERVAL])
+        timeq_add(now + hs->intervals[INTERVAL_WHINE_INTERVAL], run_whine_interval, hs);
+    if (hs->intervals[INTERVAL_EMPTY_INTERVAL])
+        timeq_add(now + hs->intervals[INTERVAL_EMPTY_INTERVAL], run_empty_interval, hs);
+
+    for (persisttype=0; persisttype<PERSIST_T_COUNT; persisttype++) {
+        str = database_get_data(GET_RECORD_OBJECT(br), persistence_types[persisttype].db_name, RECDB_QSTRING);
+        hs->persist_types[persisttype] = str ? persistence_from_name(str) : PERSIST_QUIT;
+    }
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_NOTIFICATION, RECDB_QSTRING);
+    hs->notify = str ? notification_from_name(str) : NOTIFY_NONE;
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_REGISTERED, RECDB_QSTRING);
+    if (str)
+        hs->registered = (time_t)strtol(str, NULL, 0);
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_IDWRAP, RECDB_QSTRING);
+    if (str)
+        hs->id_wrap = strtoul(str, NULL, 0);
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_REQ_MAXLEN, RECDB_QSTRING);
+    if (str)
+        hs->req_maxlen = strtoul(str, NULL, 0);
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_LAST_REQUESTID, RECDB_QSTRING);
+    if (str)
+        hs->last_requestid = strtoul(str, NULL, 0);
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_PRIVMSG_ONLY, RECDB_QSTRING);
+    hs->privmsg_only = str ? enabled_string(str) : 0;
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_REQ_ON_JOIN, RECDB_QSTRING);
+    hs->req_on_join = str ? enabled_string(str) : 0;
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_AUTO_VOICE, RECDB_QSTRING);
+    hs->auto_voice = str ? enabled_string(str) : 0;
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_AUTO_DEVOICE, RECDB_QSTRING);
+    hs->auto_devoice = str ? enabled_string(str) : 0;
+    str = database_get_data(GET_RECORD_OBJECT(br), KEY_LAST_ACTIVE, RECDB_QSTRING);
+    hs->last_active = str ? atoi(str) : now;
+
+    dict_foreach(users, user_read_helper, hs);
+
+    requests = database_get_data(GET_RECORD_OBJECT(br), KEY_REQUESTS, RECDB_OBJECT);
+    if (requests)
+        dict_foreach(requests, request_read_helper, hs);
+
+    return 0;
+}
+
+static int
+helpserv_saxdb_read(struct dict *conf_db) {
+    dict_t object;
+    char *str;
+
+    if ((object = database_get_data(conf_db, KEY_BOTS, RECDB_OBJECT))) {
+        dict_foreach(object, helpserv_bot_read, NULL);
+    }
+
+    str = database_get_data(conf_db, KEY_LAST_STATS_UPDATE, RECDB_QSTRING);
+    last_stats_update = str ? (time_t)strtol(str, NULL, 0) : now;
+    return 0;
+}
+
+static void helpserv_conf_read(void) {
+    dict_t conf_node;
+    const char *str;
+
+    if (!(conf_node = conf_get_data(HELPSERV_CONF_NAME, RECDB_OBJECT))) {
+        log_module(HS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type", HELPSERV_CONF_NAME);
+        return;
+    }
+
+    str = database_get_data(conf_node, "db_backup_freq", RECDB_QSTRING);
+    helpserv_conf.db_backup_frequency = str ? ParseInterval(str) : 7200;
+
+    str = database_get_data(conf_node, "description", RECDB_QSTRING);
+    helpserv_conf.description = str;
+
+    str = database_get_data(conf_node, "reqlogfile", RECDB_QSTRING);
+    if (str && strlen(str))
+        helpserv_conf.reqlogfile = str;
+    else
+        helpserv_conf.reqlogfile = NULL;
+
+    str = database_get_data(conf_node, "expiration", RECDB_QSTRING);
+    helpserv_conf.expire_age = ParseInterval(str ? str : "60d");
+    str = database_get_data(conf_node, "user_escape", RECDB_QSTRING);
+    helpserv_conf.user_escape = str ? str[0] : '@';
+
+    if (reqlog_ctx) {
+        saxdb_close_context(reqlog_ctx);
+        reqlog_ctx = NULL;
+    }
+    if (reqlog_f) {
+        fclose(reqlog_f);
+        reqlog_f = NULL;
+    }
+    if (helpserv_conf.reqlogfile) {
+        if (!(reqlog_f = fopen(helpserv_conf.reqlogfile, "a"))) {
+            log_module(HS_LOG, LOG_ERROR, "Unable to open request logfile (%s): %s", helpserv_conf.reqlogfile, strerror(errno));
+        } else {
+            reqlog_ctx = saxdb_open_context(reqlog_f);
+        }
+    }
+}
+
+static struct helpserv_cmd *
+helpserv_define_func(const char *name, helpserv_func_t *func, enum helpserv_level access, long flags) {
+    struct helpserv_cmd *cmd = calloc(1, sizeof(struct helpserv_cmd));
+
+    cmd->access = access;
+    cmd->weight = 1.0;
+    cmd->func = func;
+    cmd->flags = flags;
+    dict_insert(helpserv_func_dict, name, cmd);
+
+    return cmd;
+}
+
+/* Drop requests that persist until part when a user leaves the chan */
+static void handle_part(struct userNode *user, struct chanNode *chan, UNUSED_ARG(const char *reason)) {
+    struct helpserv_botlist *botlist;
+    struct helpserv_userlist *userlist;
+    const int from_opserv = 0; /* for helpserv_notice */
+    unsigned int i;
+
+    if ((botlist = dict_find(helpserv_bots_bychan_dict, chan->name, NULL))) {
+        for (i=0; i < botlist->used; i++) {
+            struct helpserv_bot *hs;
+            dict_iterator_t it;
+
+            hs = botlist->list[i];
+            if (!hs->helpserv)
+                continue;
+            if (hs->persist_types[PERSIST_T_REQUEST] != PERSIST_PART)
+                continue;
+
+            for (it=dict_first(hs->requests); it; it=iter_next(it)) {
+                struct helpserv_request *req = iter_data(it);
+
+                if (user != req->user)
+                    continue;
+                if (req->text->used) {
+                    helpserv_message(hs, user, MSGTYPE_REQ_DROPPED);
+                    helpserv_msguser(user, "HSMSG_REQ_DROPPED_PART", chan->name, req->id);
+                    if (req->helper && (hs->notify >= NOTIFY_DROP))
+                        helpserv_notify(req->helper, "HSMSG_NOTIFY_REQ_DROP_PART", req->id, user->nick);
+                }
+                helpserv_log_request(req, "Dropped");
+                dict_remove(hs->requests, iter_key(it));
+                break;
+            }
+        }
+    }
+    
+    if (user->handle_info && (userlist = dict_find(helpserv_users_byhand_dict, user->handle_info->handle, NULL))) {
+        for (i=0; i < userlist->used; i++) {
+            struct helpserv_user *hs_user = userlist->list[i];
+            struct helpserv_bot *hs = hs_user->hs;
+            dict_iterator_t it;
+
+            if ((hs->helpserv == NULL) || (hs->helpchan != chan) || find_handle_in_channel(hs->helpchan, user->handle_info, user))
+                continue;
+
+            /* In case of the clock being set back for whatever reason,
+             * minimize penalty. Don't duplicate this in handle_quit because
+             * when users quit, handle_part is called for every channel first.
+             */
+            if (hs_user->join_time && (hs_user->join_time < now)) {
+                hs_user->time_per_week[0] += (unsigned int)(now - hs_user->join_time);
+                hs_user->time_per_week[4] += (unsigned int)(now - hs_user->join_time);
+            }
+            hs_user->join_time = 0;
+
+            for (it=dict_first(hs->requests); it; it=iter_next(it)) {
+                struct helpserv_request *req=iter_data(it);
+
+                if ((hs->persist_types[PERSIST_T_HELPER] == PERSIST_PART)
+                    && (req->helper == hs_user)) {
+                    char reason[CHANNELLEN + 8];
+                    sprintf(reason, "parted %s", chan->name);
+                    helpserv_page_helper_gone(hs, req, reason);
+                }
+            }
+
+            if (hs->intervals[INTERVAL_EMPTY_INTERVAL] && hs_user->level >= HlHelper) {
+                int num_trials;
+
+                if ((num_trials = find_helpchan_helpers(hs)) >= 0) {
+                    unsigned int num_unh;
+                    struct helpserv_request *unh;
+
+                    for (num_unh=0, unh=hs->unhandled; unh; num_unh++)
+                        unh = unh->next_unhandled;
+
+                    if (num_trials) {
+                        helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_FIRSTONLYTRIALALERT", hs->helpchan->name, user->nick, num_trials, num_unh);
+                    } else {
+                        helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_FIRSTEMPTYALERT", hs->helpchan->name, user->nick, num_unh);
+                    }
+                    if (num_unh || !hs->req_on_join) {
+                        timeq_del(0, run_empty_interval, hs, TIMEQ_IGNORE_WHEN);
+                        timeq_add(now + hs->intervals[INTERVAL_EMPTY_INTERVAL], run_empty_interval, hs);
+                    }
+                }
+            }
+        }
+    }
+}
+
+/* Drop requests that persist until part or quit when a user quits. Otherwise
+ * set req->user to null (it's no longer valid) if they have a handle,
+ * and drop it if they don't (nowhere to store the request).
+ *
+ * Unassign requests where req->helper persists until the helper parts or
+ * quits. */
+static void handle_quit(struct userNode *user, UNUSED_ARG(struct userNode *killer), UNUSED_ARG(const char *why)) {
+    struct helpserv_reqlist *reqlist;
+    struct helpserv_userlist *userlist;
+    unsigned int i, n;
+
+    if (IsLocal(user)) {
+        struct helpserv_bot *hs;
+        if ((hs = dict_find(helpserv_bots_dict, user->nick, NULL))) {
+            hs->helpserv = NULL;
+        }
+        return;
+    }
+
+    if ((reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
+        n = reqlist->used;
+        for (i=0; i < n; i++) {
+            struct helpserv_request *req = reqlist->list[0];
+
+            if ((req->hs->persist_types[PERSIST_T_REQUEST] == PERSIST_QUIT) || !req->handle) {
+                char buf[12];
+                sprintf(buf, "%lu", req->id);
+
+                if (req->helper && (req->hs->notify >= NOTIFY_DROP))
+                    helpserv_notify(req->helper, "HSMSG_NOTIFY_REQ_DROP_QUIT", req->id, req->user->nick);
+
+                helpserv_log_request(req, "Dropped");
+                dict_remove(req->hs->requests, buf);
+            } else {
+                req->user = NULL;
+                req->parent_nick_list = NULL;
+                helpserv_reqlist_remove(reqlist, req);
+
+                if (req->helper && (req->hs->notify >= NOTIFY_USER))
+                    helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_QUIT", req->id, user->nick);
+            }
+        }
+
+        dict_remove(helpserv_reqs_bynick_dict, user->nick);
+    }
+
+    if (user->handle_info && (userlist = dict_find(helpserv_users_byhand_dict, user->handle_info->handle, NULL))) {
+        for (i=0; i < userlist->used; i++) {
+            struct helpserv_user *hs_user = userlist->list[i];
+            struct helpserv_bot *hs = hs_user->hs;
+            dict_iterator_t it;
+
+            if ((hs->helpserv == NULL) || user->next_authed || (user->handle_info->users != user))
+                continue;
+
+            for (it=dict_first(hs->requests); it; it=iter_next(it)) {
+                struct helpserv_request *req=iter_data(it);
+
+                if ((hs->persist_types[PERSIST_T_HELPER] == PERSIST_QUIT) && (req->helper == hs_user)) {
+                    helpserv_page_helper_gone(hs, req, "disconnected");
+                }
+            }
+        }
+    }
+}
+
+static void associate_requests_bybot(struct helpserv_bot *hs, struct userNode *user, int force_greet) {
+    struct helpserv_reqlist *reqlist, *hand_reqlist=NULL;
+    struct helpserv_request *newest=NULL, *nicknewest=NULL;
+    unsigned int i;
+    const int from_opserv = 0; /* For helpserv_notice */
+    
+    if (!(user->handle_info && (hand_reqlist = dict_find(helpserv_reqs_byhand_dict, user->handle_info->handle, NULL))) && !force_greet) {
+        return;
+    }
+
+    reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL);
+
+    if (hand_reqlist) {
+        for (i=0; i < hand_reqlist->used; i++) {
+            struct helpserv_request *req=hand_reqlist->list[i];
+
+            if (req->user || (req->hs != hs))
+                continue;
+
+            req->user = user;
+            if (!reqlist) {
+                reqlist = helpserv_reqlist_alloc();
+                dict_insert(helpserv_reqs_bynick_dict, user->nick, reqlist);
+            }
+            req->parent_nick_list = reqlist;
+            helpserv_reqlist_append(reqlist, req);
+
+            if (req->helper && (hs->notify >= NOTIFY_USER))
+                helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_FOUND", req->id, user->nick);
+
+            if (!newest || (newest->opened < req->opened))
+                newest = req;
+        }
+    }
+
+    /* If it's supposed to force a greeting, only bail out if there are no
+     * requests at all. If it's not supposed to force a greeting, bail out if
+     * nothing was changed. */
+    if (!(newest || (force_greet && reqlist)))
+        return;
+
+    /* Possible conditions here:
+     * 1. newest == NULL, force_greeting == 1, reqlist != NULL
+     * 2. newest != NULL, force_greeting doesn't matter, reqlist != NULL */
+
+    /* Figure out which request will get their next message */
+    for (i=0; i < reqlist->used; i++) {
+        struct helpserv_request *req=reqlist->list[i];
+
+        if (req->hs != hs)
+            continue;
+
+        if (!nicknewest || (nicknewest->opened < req->opened))
+            nicknewest = req;
+
+        if (hs->auto_voice && req->helper)
+        {
+            struct mod_chanmode change;
+            change.modes_set = change.modes_clear = 0;
+            change.argc = 1;
+            change.args[0].mode = MODE_VOICE;
+            if ((change.args[0].member = GetUserMode(hs->helpchan, user)))
+                mod_chanmode_announce(hs->helpserv, hs->helpchan, &change);
+        }
+    }
+
+    if ((force_greet && nicknewest) || (newest && (nicknewest == newest))) {
+        /* Let the user know. Either the user is forced to be greeted, or the
+         * above has changed which request will get their next message. */
+        helpserv_msguser(user, "HSMSG_GREET_EXISTING_REQ", hs->helpchan->name, nicknewest->id);
+    }
+}
+
+static void associate_requests_bychan(struct chanNode *chan, struct userNode *user, int force_greet) {
+    struct helpserv_botlist *botlist;
+    unsigned int i;
+
+    if (!(botlist = dict_find(helpserv_bots_bychan_dict, chan->name, NULL)))
+        return;
+
+    for (i=0; i < botlist->used; i++)
+        associate_requests_bybot(botlist->list[i], user, force_greet);
+}
+
+
+/* Greet users upon joining a helpserv channel (if greeting is set) and set
+ * req->user to the user joining for all requests owned by the user's handle
+ * (if any) with a req->user == NULL */
+static int handle_join(struct modeNode *mNode) {
+    struct userNode *user = mNode->user;
+    struct chanNode *chan = mNode->channel;
+    struct helpserv_botlist *botlist;
+    unsigned int i;
+    const int from_opserv = 0; /* for helpserv_notice */
+
+    if (IsLocal(user))
+        return 0;
+    
+    if (!(botlist = dict_find(helpserv_bots_bychan_dict, chan->name, NULL)))
+        return 0;
+
+    for (i=0; i < botlist->used; i++) {
+        struct helpserv_bot *hs=botlist->list[i];
+
+        if (user->handle_info) {
+            struct helpserv_user *hs_user;
+
+            if ((hs_user = dict_find(hs->users, user->handle_info->handle, NULL))) {
+                if (!hs_user->join_time)
+                    hs_user->join_time = now;
+
+                if (hs_user->level >= HlHelper && hs->intervals[INTERVAL_EMPTY_INTERVAL] && hs->helpchan_empty) {
+                    hs->helpchan_empty = 0;
+                    timeq_del(0, run_empty_interval, hs, TIMEQ_IGNORE_WHEN);
+                    helpserv_page(PGSRC_ALERT, "HSMSG_PAGE_EMPTYNOMORE", user->nick, hs->helpchan->name);
+                }
+                continue; /* Don't want helpers to have request-on-join */
+            }
+        }
+
+        if (self->burst && !hs->req_on_join)
+            continue;
+
+        associate_requests_bybot(hs, user, 1);
+
+        helpserv_message(hs, user, MSGTYPE_GREETING);
+
+        /* Make sure this is at the end (because of the continues) */
+        if (hs->req_on_join) {
+            struct helpserv_reqlist *reqlist;
+            unsigned int j;
+
+            if ((reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
+                for (j=0; j < reqlist->used; j++)
+                    if (reqlist->list[i]->hs == hs)
+                        break;
+                if (j < reqlist->used)
+                    continue;
+            }
+
+            create_request(user, hs, 1);
+        }
+    }
+    return 0;
+}
+
+/* Update helpserv_reqs_bynick_dict upon nick change */
+static void handle_nickchange(struct userNode *user, const char *old_nick) {
+    struct helpserv_reqlist *reqlist;
+    unsigned int i;
+
+    if (!(reqlist = dict_find(helpserv_reqs_bynick_dict, old_nick, NULL)))
+        return;
+
+    /* Don't free the list when we switch it over to the new nick. */
+    dict_remove2(helpserv_reqs_bynick_dict, old_nick, 1);
+    dict_insert(helpserv_reqs_bynick_dict, user->nick, reqlist);
+
+    for (i=0; i < reqlist->used; i++) {
+        struct helpserv_request *req=reqlist->list[i];
+
+        if (req->helper && (req->hs->notify >= NOTIFY_USER))
+            helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_NICK", req->id, old_nick, user->nick);
+    }
+}
+
+/* Also update helpserv_reqs_byhand_dict upon handle rename */
+static void handle_nickserv_rename(struct handle_info *handle, const char *old_handle) {
+    struct helpserv_reqlist *reqlist;
+    struct helpserv_userlist *userlist;
+    unsigned int i;
+
+    /* First, rename the handle in the requests dict */
+    if ((reqlist = dict_find(helpserv_reqs_byhand_dict, old_handle, NULL))) {
+        /* Don't free the list */
+        dict_remove2(helpserv_reqs_byhand_dict, old_handle, 1);
+        dict_insert(helpserv_reqs_byhand_dict, handle->handle, reqlist);
+    }
+
+    /* Second, rename the handle in the users dict */
+    if ((userlist = dict_find(helpserv_users_byhand_dict, old_handle, NULL))) {
+        dict_remove2(helpserv_users_byhand_dict, old_handle, 1);
+
+        for (i=0; i < userlist->used; i++)
+            dict_remove2(userlist->list[i]->hs->users, old_handle, 1);
+
+        dict_insert(helpserv_users_byhand_dict, handle->handle, userlist);
+        for (i=0; i < userlist->used; i++)
+            dict_insert(userlist->list[i]->hs->users, handle->handle, userlist->list[i]);
+    }
+    
+    if (reqlist) {
+        for (i=0; i < reqlist->used; i++) {
+            struct helpserv_request *req=reqlist->list[i];
+
+            if (req->helper && (req->hs->notify >= NOTIFY_HANDLE))
+                helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_RENAME", req->id, old_handle, handle->handle);
+        }
+    }
+}
+
+/* Deals with two cases:
+ * 1. No handle -> handle
+ *    - Bots with a request assigned to both the user (w/o handle) and the
+ *      handle can exist in this case. When a message is sent,
+ *      helpserv_usermsg will append it to the most recently opened request.
+ *    - Requests assigned to the handle are NOT assigned to the user, since
+ *      multiple people can auth to the same handle at once. Wait for them to
+ *      join / privmsg before setting req->user.
+ * 2. Handle -> handle
+ *    - Generally avoided, but sometimes the code may allow this.
+ *    - Requests that persist only until part/quit are brought along to the
+ *      new handle.
+ *    - Requests that persist until closed (stay saved with the handle) are
+ *      left with the old handle. This is to prevent the confusing situation
+ *      where some requests are carried over to the new handle, and some are
+ *      left (because req->handle is the same for all of them, but only some
+ *      have req->user set).
+ * - In either of the above cases, if a user is on a bot's userlist and has
+ *   requests assigned to them, it will give them a list. */
+static void handle_nickserv_auth(struct userNode *user, struct handle_info *old_handle) {
+    struct helpserv_reqlist *reqlist, *dellist=NULL, *hand_reqlist, *oldhand_reqlist;
+    struct helpserv_userlist *userlist;
+    unsigned int i, j;
+    dict_iterator_t it;
+    const int from_opserv = 0; /* for helpserv_notice */
+
+    if (!user->handle_info)
+        return; /* Authed user is quitting */
+
+    if ((userlist = dict_find(helpserv_users_byhand_dict, user->handle_info->handle, NULL))) {
+        for (i=0; i < userlist->used; i++) {
+            struct helpserv_user *hs_user = userlist->list[i];
+            struct helpserv_bot *hs = hs_user->hs;
+            struct helpserv_reqlist helper_reqs;
+            struct helpfile_table tbl;
+
+            if (!hs_user->join_time && find_handle_in_channel(hs->helpchan, hs_user->handle, NULL))
+                hs_user->join_time = now;
+
+            helpserv_reqlist_init(&helper_reqs);
+
+            for (it=dict_first(hs->requests); it; it=iter_next(it)) {
+                struct helpserv_request *req=iter_data(it);
+
+                if (req->helper == hs_user)
+                    helpserv_reqlist_append(&helper_reqs, req);
+            }
+
+            if (helper_reqs.used) {
+                tbl.length = helper_reqs.used+1;
+                tbl.width = 5;
+                tbl.flags = TABLE_NO_FREE;
+                tbl.contents = alloca(tbl.length * sizeof(*tbl.contents));
+                tbl.contents[0] = alloca(tbl.width * sizeof(**tbl.contents));
+                tbl.contents[0][0] = "Bot";
+                tbl.contents[0][1] = "ID#";
+                tbl.contents[0][2] = "Nick";
+                tbl.contents[0][3] = "Account";
+                tbl.contents[0][4] = "Opened";
+
+                for (j=1; j <= helper_reqs.used; j++) {
+                    struct helpserv_request *req=helper_reqs.list[j-1];
+                    char reqid[12], timestr[MAX_LINE_SIZE];
+
+                    tbl.contents[j] = alloca(tbl.width * sizeof(**tbl.contents));
+                    tbl.contents[j][0] = req->hs->helpserv->nick;
+                    sprintf(reqid, "%lu", req->id);
+                    tbl.contents[j][1] = strdup(reqid);
+                    tbl.contents[j][2] = req->user ? req->user->nick : "Not online";
+                    tbl.contents[j][3] = req->handle ? req->handle->handle : "Not authed";
+                    strftime(timestr, MAX_LINE_SIZE, HSFMT_TIME, localtime(&req->opened));
+                    tbl.contents[j][4] = strdup(timestr);
+                }
+
+                helpserv_notice(user, "HSMSG_REQLIST_AUTH");
+                table_send(hs->helpserv, user->nick, 0, NULL, tbl);
+
+                for (j=1; j <= helper_reqs.used; j++) {
+                    free((char *)tbl.contents[j][1]);
+                    free((char *)tbl.contents[j][4]);
+                }
+            }
+
+            helpserv_reqlist_clean(&helper_reqs);
+        }
+    }
+
+
+    if (!(reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
+        for (i=0; i < user->channels.used; i++)
+            associate_requests_bychan(user->channels.list[i]->channel, user, 0);
+        return;
+    }
+
+    if (!(hand_reqlist = dict_find(helpserv_reqs_byhand_dict, user->handle_info->handle, NULL))) {
+        hand_reqlist = helpserv_reqlist_alloc();
+        dict_insert(helpserv_reqs_byhand_dict, user->handle_info->handle, hand_reqlist);
+    }
+
+    if (old_handle) {
+        dellist = helpserv_reqlist_alloc();
+        oldhand_reqlist = dict_find(helpserv_reqs_byhand_dict, old_handle->handle, NULL);
+    } else {
+        oldhand_reqlist = NULL;
+    }
+
+    for (i=0; i < reqlist->used; i++) {
+        struct helpserv_request *req = reqlist->list[i];
+        struct helpserv_bot *hs=req->hs;
+
+        if (!old_handle || hs->persist_types[PERSIST_T_REQUEST] == PERSIST_PART || hs->persist_types[PERSIST_T_REQUEST] == PERSIST_QUIT) {
+            /* The request needs to be assigned to the new handle; either it
+             * only persists until part/quit (so it makes sense to keep it as
+             * close to the user as possible, and if it's made persistent later
+             * then it's attached to the new handle) or there is no old handle.
+             */
+
+            req->handle = user->handle_info;
+
+            req->parent_hand_list = hand_reqlist;
+            helpserv_reqlist_append(hand_reqlist, req);
+
+            if (oldhand_reqlist) {
+                if (oldhand_reqlist->used == 1) {
+                    dict_remove(helpserv_reqs_byhand_dict, old_handle->handle);
+                    oldhand_reqlist = NULL;
+                } else {
+                    helpserv_reqlist_remove(oldhand_reqlist, req);
+                }
+            }
+
+            if (old_handle) {
+                char buf[CHANNELLEN + 14];
+
+                if (hs->persist_types[PERSIST_T_REQUEST] == PERSIST_PART) {
+                    sprintf(buf, "part channel %s", hs->helpchan->name);
+                } else {
+                    strcpy(buf, "quit irc");
+                }
+
+                helpserv_msguser(user, "HSMSG_REQ_AUTH_MOVED", user->handle_info->handle, hs->helpchan->name, req->id, old_handle->handle, buf);
+                if (req->helper && (hs->notify >= NOTIFY_HANDLE))
+                    helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_MOVE", req->id, user->handle_info->handle, old_handle->handle);
+            } else {
+                if (req->helper && (hs->notify >= NOTIFY_HANDLE))
+                    helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_AUTH", req->id, user->nick, user->handle_info->handle);
+            }
+        } else {
+            req->user = NULL;
+            req->parent_nick_list = NULL;
+            /* Would rather not mess with the list while iterating through
+             * it */
+            helpserv_reqlist_append(dellist, req);
+
+            helpserv_msguser(user, "HSMSG_REQ_AUTH_STUCK", user->handle_info->handle, hs->helpchan->name, req->id, old_handle->handle);
+            if (req->helper && (hs->notify >= NOTIFY_HANDLE))
+                helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_STUCK", req->id, user->nick, user->handle_info->handle, old_handle->handle);
+        }
+    }
+
+    if (old_handle) {
+        if (dellist->used) {
+            if (dellist->used == reqlist->used) {
+                dict_remove(helpserv_reqs_bynick_dict, user->nick);
+            } else {
+                for (i=0; i < dellist->used; i++)
+                    helpserv_reqlist_remove(reqlist, dellist->list[i]);
+            }
+        }
+        helpserv_reqlist_free(dellist);
+    }
+
+    for (i=0; i < user->channels.used; i++)
+        associate_requests_bychan(user->channels.list[i]->channel, user, 0);
+}
+
+
+/* Disassociate all requests from the handle. If any have req->user == NULL
+ * then give them to the user doing the unregistration (if not an oper/helper)
+ * otherwise the first nick it finds authed (it lets them know about this). If
+ * there are no users authed to the handle online, the requests are lost. This
+ * may result in the user having >1 request/bot, and messages go to the most
+ * recently opened request.
+ *
+ * Also, remove the user from all bots that it has access in.
+ * helpserv_del_user() will take care of unassigning the requests. */
+static void handle_nickserv_unreg(struct userNode *user, struct handle_info *handle) {
+    struct helpserv_reqlist *hand_reqlist;
+    struct helpserv_userlist *userlist;
+    unsigned int i, n;
+    const int from_opserv = 0; /* for helpserv_notice */
+    struct helpserv_bot *hs; /* for helpserv_notice */
+
+    if ((userlist = dict_find(helpserv_users_byhand_dict, handle->handle, NULL))) {
+        n=userlist->used;
+
+        /* Each time helpserv_del_user is called, that entry is going to be
+         * taken out of userlist... so this should cope with that */
+        for (i=0; i < n; i++) {
+            struct helpserv_user *hs_user=userlist->list[0];
+            helpserv_del_user(hs_user->hs, hs_user);
+        }
+    }
+
+    if (!(hand_reqlist = dict_find(helpserv_reqs_byhand_dict, handle->handle, NULL))) {
+        return;
+    }
+
+    n = hand_reqlist->used;
+    for (i=0; i < n; i++) {
+        struct helpserv_request *req=hand_reqlist->list[0];
+        hs = req->hs;
+
+        req->handle = NULL;
+        req->parent_hand_list = NULL;
+        helpserv_reqlist_remove(hand_reqlist, req);
+        if (user && req->helper && (hs->notify >= NOTIFY_HANDLE))
+            helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_UNREG", req->id, handle->handle, user->nick);
+
+        if (!req->user) {
+            if (!user) {
+                /* This is probably an expire. Silently remove everything. */
+
+                char buf[12];
+                if (req->helper && (hs->notify >= NOTIFY_DROP))
+                    helpserv_notify(req->helper, "HSMSG_NOTIFY_REQ_DROP_UNREGGED", req->id, req->handle->handle);
+                sprintf(buf, "%lu", req->id);
+                helpserv_log_request(req, "Account unregistered");
+                dict_remove(req->hs->requests, buf);
+            } else if (user->handle_info == handle) {
+                req->user = user;
+                if (!(req->parent_nick_list = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
+                    req->parent_nick_list = helpserv_reqlist_alloc();
+                    dict_insert(helpserv_reqs_bynick_dict, user->nick, req->parent_nick_list);
+                }
+                helpserv_reqlist_append(req->parent_nick_list, req);
+
+                if (hs->persist_types[PERSIST_T_REQUEST] == PERSIST_CLOSE)
+                    helpserv_msguser(req->user, "HSMSG_REQ_WARN_UNREG", handle->handle, hs->helpchan->name, req->id);
+            } else {
+                if (handle->users) {
+                    req->user = handle->users;
+
+                    if (!(req->parent_nick_list = dict_find(helpserv_reqs_bynick_dict, req->user->nick, NULL))) {
+                        req->parent_nick_list = helpserv_reqlist_alloc();
+                        dict_insert(helpserv_reqs_bynick_dict, req->user->nick, req->parent_nick_list);
+                    }
+                    helpserv_reqlist_append(req->parent_nick_list, req);
+
+                    helpserv_msguser(req->user, "HSMSG_REQ_ASSIGNED_UNREG", handle->handle, hs->helpchan->name, req->id);
+                    if (req->helper && (hs->notify >= NOTIFY_USER))
+                        helpserv_notify(req->helper, "HSMSG_NOTIFY_USER_MOVE", req->id, handle->handle, req->user->nick);
+                } else {
+                    char buf[12];
+
+                    helpserv_notice(user, "HSMSG_REQ_DROPPED_UNREG", handle->handle, hs->helpchan->name, req->id);
+                    if (req->helper && (hs->notify >= NOTIFY_DROP))
+                        helpserv_notify(req->helper, "HSMSG_NOTIFY_REQ_DROP_UNREGGED", req->id, req->handle->handle);
+                    sprintf(buf, "%lu", req->id);
+                    helpserv_log_request(req, "Account unregistered");
+                    dict_remove(req->hs->requests, buf);
+                }
+            }
+        }
+    }
+
+    dict_remove(helpserv_reqs_byhand_dict, handle->handle);
+}
+
+static void handle_nickserv_merge(struct userNode *user, struct handle_info *handle_to, struct handle_info *handle_from) {
+    struct helpserv_reqlist *reqlist_from, *reqlist_to;
+    unsigned int i;
+
+    reqlist_to = dict_find(helpserv_reqs_byhand_dict, handle_to->handle, NULL);
+
+    if ((reqlist_from = dict_find(helpserv_reqs_byhand_dict, handle_from->handle, NULL))) {
+        for (i=0; i < reqlist_from->used; i++) {
+            struct helpserv_request *req=reqlist_from->list[i];
+
+            if (!reqlist_to) {
+                reqlist_to = helpserv_reqlist_alloc();
+                dict_insert(helpserv_reqs_byhand_dict, handle_to->handle, reqlist_to);
+            }
+            req->parent_hand_list = reqlist_to;
+            req->handle = handle_to;
+            helpserv_reqlist_append(reqlist_to, req);
+        }
+        dict_remove(helpserv_reqs_byhand_dict, handle_from->handle);
+    }
+
+    if (reqlist_to) {
+        for (i=0; i < reqlist_to->used; i++) {
+            struct helpserv_request *req=reqlist_to->list[i];
+
+            if (req->helper && (req->hs->notify >= NOTIFY_HANDLE)) {
+                helpserv_notify(req->helper, "HSMSG_NOTIFY_HAND_MERGE", req->id, handle_to->handle, handle_from->handle, user->nick);
+            }
+        }
+    }
+}
+
+static void handle_nickserv_allowauth(struct userNode *user, struct userNode *target, struct handle_info *handle) {
+    struct helpserv_reqlist *reqlist;
+    unsigned int i;
+
+    if ((reqlist = dict_find(helpserv_reqs_bynick_dict, target->nick, NULL))) {
+        for (i=0; i < reqlist->used; i++) {
+            struct helpserv_request *req=reqlist->list[i];
+
+            if (req->helper && (req->hs->notify >= NOTIFY_HANDLE)) {
+                if (handle) {
+                    helpserv_notify(req->helper, "HSMSG_NOTIFY_ALLOWAUTH", req->id, target->nick, user->nick, handle->handle);
+                } else {
+                    helpserv_notify(req->helper, "HSMSG_NOTIFY_UNALLOWAUTH", req->id, target->nick, user->nick);
+                }
+            }
+        }
+    }
+}
+
+static void handle_nickserv_failpw(struct userNode *user, struct handle_info *handle) {
+    struct helpserv_reqlist *reqlist;
+    unsigned int i;
+
+    if ((reqlist = dict_find(helpserv_reqs_bynick_dict, user->nick, NULL))) {
+        for (i=0; i < reqlist->used; i++) {
+            struct helpserv_request *req=reqlist->list[i];
+            if (req->helper && (req->hs->notify >= NOTIFY_HANDLE))
+                helpserv_notify(req->helper, "HSMSG_NOTIFY_FAILPW", req->id, user->nick, handle->handle);
+        }
+    }
+}
+
+static time_t helpserv_next_stats(time_t after_when) {
+    struct tm *timeinfo = localtime(&after_when);
+
+    /* This works because mktime(3) says it will accept out-of-range values
+     * and fix them for us. tm_wday and tm_yday are ignored. */
+    timeinfo->tm_mday++;
+
+    /* We want to run stats at midnight (local time). */
+    timeinfo->tm_sec = timeinfo->tm_min = timeinfo->tm_hour = 0;
+
+    return mktime(timeinfo);
+}
+
+/* If data != NULL, then don't add to the timeq */
+static void helpserv_run_stats(time_t when) {
+    struct tm when_s;
+    struct helpserv_bot *hs;
+    struct helpserv_user *hs_user;
+    int i;
+    dict_iterator_t it, it2;
+
+    last_stats_update = when;
+    localtime_r(&when, &when_s);
+    for (it=dict_first(helpserv_bots_dict); it; it=iter_next(it)) {
+        hs = iter_data(it);
+
+        for (it2=dict_first(hs->users); it2; it2=iter_next(it2)) {
+            hs_user = iter_data(it2);
+
+            /* Skip the helper if it's not their week-start day. */
+            if (hs_user->week_start != when_s.tm_wday)
+                continue;
+
+            /* Adjust their credit if they are in-channel at rollover. */
+            if (hs_user->join_time) {
+                hs_user->time_per_week[0] += when - hs_user->join_time;
+                hs_user->time_per_week[4] += when - hs_user->join_time;
+                hs_user->join_time = when;
+            }
+
+            /* Shift everything */
+            for (i=3; i > 0; i--) {
+                hs_user->time_per_week[i] = hs_user->time_per_week[i-1];
+                hs_user->picked_up[i] = hs_user->picked_up[i-1];
+                hs_user->closed[i] = hs_user->closed[i-1];
+                hs_user->reassigned_from[i] = hs_user->reassigned_from[i-1];
+                hs_user->reassigned_to[i] = hs_user->reassigned_to[i-1];
+            }
+
+            /* Reset it for this week */
+            hs_user->time_per_week[0] = hs_user->picked_up[0] = hs_user->closed[0] = hs_user->reassigned_from[0] = hs_user->reassigned_to[0] = 0;
+        }
+    }
+}
+
+static void helpserv_timed_run_stats(UNUSED_ARG(void *data)) {
+    helpserv_run_stats(now);
+    timeq_add(helpserv_next_stats(now), helpserv_timed_run_stats, data);
+}
+
+static void
+helpserv_define_option(const char *name, helpserv_option_func_t *func) {
+    dict_insert(helpserv_option_dict, name, func);
+}
+
+static void helpserv_db_cleanup(void) {
+    shutting_down=1;
+    unreg_part_func(handle_part);
+    unreg_del_user_func(handle_quit);
+    close_helpfile(helpserv_helpfile);
+    dict_delete(helpserv_func_dict);
+    dict_delete(helpserv_option_dict);
+    dict_delete(helpserv_usercmd_dict);
+    dict_delete(helpserv_bots_dict);
+    dict_delete(helpserv_bots_bychan_dict);
+    dict_delete(helpserv_reqs_bynick_dict);
+    dict_delete(helpserv_reqs_byhand_dict);
+    dict_delete(helpserv_users_byhand_dict);
+
+    if (reqlog_ctx)
+        saxdb_close_context(reqlog_ctx);
+    if (reqlog_f)
+        fclose(reqlog_f);
+}
+
+int helpserv_init() {
+    HS_LOG = log_register_type("HelpServ", "file:helpserv.log");
+    conf_register_reload(helpserv_conf_read);
+
+    helpserv_func_dict = dict_new();
+    dict_set_free_data(helpserv_func_dict, free);
+    helpserv_define_func("HELP", cmd_help, HlNone, CMD_NOT_OVERRIDE|CMD_IGNORE_EVENT);
+    helpserv_define_func("LIST", cmd_list, HlTrial, CMD_NEED_BOT|CMD_IGNORE_EVENT);
+    helpserv_define_func("NEXT", cmd_next, HlTrial, CMD_NEED_BOT|CMD_NEVER_FROM_OPSERV);
+    helpserv_define_func("PICKUP", cmd_pickup, HlTrial, CMD_NEED_BOT|CMD_NEVER_FROM_OPSERV);
+    helpserv_define_func("REASSIGN", cmd_reassign, HlManager, CMD_NEED_BOT|CMD_NEVER_FROM_OPSERV);
+    helpserv_define_func("CLOSE", cmd_close, HlTrial, CMD_NEED_BOT|CMD_NEVER_FROM_OPSERV);
+    helpserv_define_func("SHOW", cmd_show, HlTrial, CMD_NEED_BOT|CMD_IGNORE_EVENT);
+    helpserv_define_func("ADDNOTE", cmd_addnote, HlTrial, CMD_NEED_BOT);
+    helpserv_define_func("ADDOWNER", cmd_addowner, HlOper, CMD_NEED_BOT|CMD_FROM_OPSERV_ONLY);
+    helpserv_define_func("DELOWNER", cmd_deluser, HlOper, CMD_NEED_BOT|CMD_FROM_OPSERV_ONLY);
+    helpserv_define_func("ADDTRIAL", cmd_addtrial, HlManager, CMD_NEED_BOT);
+    helpserv_define_func("ADDHELPER", cmd_addhelper, HlManager, CMD_NEED_BOT);
+    helpserv_define_func("ADDMANAGER", cmd_addmanager, HlOwner, CMD_NEED_BOT);
+    helpserv_define_func("GIVEOWNERSHIP", cmd_giveownership, HlOwner, CMD_NEED_BOT);
+    helpserv_define_func("DELUSER", cmd_deluser, HlManager, CMD_NEED_BOT);
+    helpserv_define_func("HELPERS", cmd_helpers, HlNone, CMD_NEED_BOT);
+    helpserv_define_func("WLIST", cmd_wlist, HlNone, CMD_NEED_BOT);
+    helpserv_define_func("MLIST", cmd_mlist, HlNone, CMD_NEED_BOT);
+    helpserv_define_func("HLIST", cmd_hlist, HlNone, CMD_NEED_BOT);
+    helpserv_define_func("TLIST", cmd_tlist, HlNone, CMD_NEED_BOT);
+    helpserv_define_func("CLVL", cmd_clvl, HlManager, CMD_NEED_BOT);
+    helpserv_define_func("PAGE", cmd_page, HlTrial, CMD_NEED_BOT);
+    helpserv_define_func("SET", cmd_set, HlHelper, CMD_NEED_BOT);
+    helpserv_define_func("STATS", cmd_stats, HlTrial, CMD_NEED_BOT);
+    helpserv_define_func("STATSREPORT", cmd_statsreport, HlManager, CMD_NEED_BOT);
+    helpserv_define_func("UNREGISTER", cmd_unregister, HlOwner, CMD_NEED_BOT);
+    helpserv_define_func("READHELP", cmd_readhelp, HlOper, CMD_FROM_OPSERV_ONLY);
+    helpserv_define_func("REGISTER", cmd_register, HlOper, CMD_FROM_OPSERV_ONLY);
+    helpserv_define_func("MOVE", cmd_move, HlOper, CMD_FROM_OPSERV_ONLY|CMD_NEED_BOT);
+    helpserv_define_func("BOTS", cmd_bots, HlOper, CMD_FROM_OPSERV_ONLY|CMD_IGNORE_EVENT);
+    helpserv_define_func("EXPIRE", cmd_expire, HlOper, CMD_FROM_OPSERV_ONLY);
+    helpserv_define_func("WEEKSTART", cmd_weekstart, HlTrial, CMD_NEED_BOT);
+
+    helpserv_option_dict = dict_new();
+    helpserv_define_option("PAGETARGET", opt_pagetarget_command);
+    helpserv_define_option("ALERTPAGETARGET", opt_pagetarget_alert);
+    helpserv_define_option("STATUSPAGETARGET", opt_pagetarget_status);
+    helpserv_define_option("PAGE", opt_pagetype);
+    helpserv_define_option("PAGETYPE", opt_pagetype);
+    helpserv_define_option("ALERTPAGETYPE", opt_alert_page_type);
+    helpserv_define_option("STATUSPAGETYPE", opt_status_page_type);
+    helpserv_define_option("GREETING", opt_greeting);
+    helpserv_define_option("REQOPENED", opt_req_opened);
+    helpserv_define_option("REQASSIGNED", opt_req_assigned);
+    helpserv_define_option("REQCLOSED", opt_req_closed);
+    helpserv_define_option("IDLEDELAY", opt_idle_delay);
+    helpserv_define_option("WHINEDELAY", opt_whine_delay);
+    helpserv_define_option("WHINEINTERVAL", opt_whine_interval);
+    helpserv_define_option("EMPTYINTERVAL", opt_empty_interval);
+    helpserv_define_option("STALEDELAY", opt_stale_delay);
+    helpserv_define_option("REQPERSIST", opt_request_persistence);
+    helpserv_define_option("HELPERPERSIST", opt_helper_persistence);
+    helpserv_define_option("NOTIFICATION", opt_notification);
+    helpserv_define_option("REQMAXLEN", opt_req_maxlen);
+    helpserv_define_option("IDWRAP", opt_id_wrap);
+    helpserv_define_option("PRIVMSGONLY", opt_privmsg_only);
+    helpserv_define_option("REQONJOIN", opt_req_on_join);
+    helpserv_define_option("AUTOVOICE", opt_auto_voice);
+    helpserv_define_option("AUTODEVOICE", opt_auto_devoice);
+
+    helpserv_usercmd_dict = dict_new();
+    dict_insert(helpserv_usercmd_dict, "WAIT", usercmd_wait);
+
+    helpserv_bots_dict = dict_new();
+    dict_set_free_data(helpserv_bots_dict, helpserv_free_bot);
+    
+    helpserv_bots_bychan_dict = dict_new();
+    dict_set_free_data(helpserv_bots_bychan_dict, helpserv_botlist_free);
+
+    helpserv_reqs_bynick_dict = dict_new();
+    dict_set_free_data(helpserv_reqs_bynick_dict, helpserv_reqlist_free);
+    helpserv_reqs_byhand_dict = dict_new();
+    dict_set_free_data(helpserv_reqs_byhand_dict, helpserv_reqlist_free);
+
+    helpserv_users_byhand_dict = dict_new();
+    dict_set_free_data(helpserv_users_byhand_dict, helpserv_userlist_free);
+
+    saxdb_register("HelpServ", helpserv_saxdb_read, helpserv_saxdb_write);
+    helpserv_helpfile_read();
+
+    /* Make up for downtime... though this will only really affect the
+     * time_per_week */
+    if (last_stats_update && (helpserv_next_stats(last_stats_update) < now)) {
+        time_t statsrun = last_stats_update;
+        while ((statsrun = helpserv_next_stats(statsrun)) < now)
+            helpserv_run_stats(statsrun);
+    }
+    timeq_add(helpserv_next_stats(now), helpserv_timed_run_stats, NULL);
+
+    reg_join_func(handle_join);
+    reg_part_func(handle_part); /* also deals with kick */
+    reg_nick_change_func(handle_nickchange);
+    reg_del_user_func(handle_quit);
+
+    reg_auth_func(handle_nickserv_auth);
+    reg_handle_rename_func(handle_nickserv_rename);
+    reg_unreg_func(handle_nickserv_unreg);
+    reg_allowauth_func(handle_nickserv_allowauth);
+    reg_failpw_func(handle_nickserv_failpw);
+    reg_handle_merge_func(handle_nickserv_merge);
+
+    reg_exit_func(helpserv_db_cleanup);
+
+    helpserv_module = module_register("helpserv", HS_LOG, HELPSERV_HELPFILE_NAME, helpserv_expand_variable);
+    modcmd_register(helpserv_module, "helpserv", cmd_helpserv, 1, MODCMD_REQUIRE_AUTHED|MODCMD_NO_LOG|MODCMD_NO_DEFAULT_BIND, "level", "800", NULL);
+    message_register_table(msgtab);
+    return 1;
+}
+
+int
+helpserv_finalize(void) {
+    return 1;
+}
diff --git a/src/mod-helpserv.help b/src/mod-helpserv.help
new file mode 100644 (file)
index 0000000..edc7dac
--- /dev/null
@@ -0,0 +1,304 @@
+"<index>" ("Help Queue Manager commands",
+        "The following topics are available:",
+        "  HELP         How to use this service",
+        "  QUEUE        Managing the request queue",
+        "  USERS        Managing $S helpers",
+        "  OPER         IRC operator administration commands");
+"QUEUE" ("Managing the request queue in $S",
+        "  LIST        List open requests",
+        "  NEXT        Pick up the next unassigned request",
+        "  PICKUP      Pick up a particular unassigned request",
+        "  REASSIGN    Assign a request to another helper",
+        "  CLOSE       Close out a request",
+        "  SHOW        Show detailed information on a request",
+        "  ADDNOTE     Add a note to a request",
+        "  PAGE        Try to wake up other helpers",
+        "  STATS       Get statistics on time and requests handled",
+        "  STATSREPORT Shows statistics for all users");
+"USERS" ("Managing helpers in $S",
+        "  USER ACCESS Description of the user access levels",
+        "  HELPERS     List current helpers and their access levels",
+        "  ADDMANAGER  Add a new manager to the service",
+        "  ADDHELPER   Add a helper to the service",
+        "  ADDTRIAL    Add a trial helper to the service",
+        "  DELUSER     Remove a person from the service",
+        "  CLVL        Change a user's access",
+        "  SET         Change $S settings");
+"USER ACCESS" ("User access levels",
+        "There are four levels of user access to $S: $btrial$b helper, $bhelper$b, $bmanager$b, and $bowner$b.",
+        "Helpers can use the normal help queue management functions.",
+        "Trial helpers have the same access as helpers, but they do not stop the \"empty alert\" from occuring",
+        "Managers also have the ability to use the $ureassign$u command, add and remove helpers, and see stats for any helper.",
+        "Owners have those abilities, can add and remove managers, and change queue settings.");
+"OPER" ("IRC operator administration commands",
+        "  REGISTER    Register a new HelpServ bot",
+        "  ADDOWNER    Add an owner to an existing HelpServ",
+        "  DELOWNER    Remove an owner from an existing HelpServ",
+        "  MOVE        Change the channel or nick the service helps on",
+        "  UNREGISTER  Unregister a HelpServ bot",
+        "  BOTS        List registered HelpServ instances",
+        "  EXPIRE      Expire inactive channels",
+        "  VERSION     Show version of HelpServ",
+        "  READHELP    Re-read help file",
+        "  WRITE       Write out database");
+"COMMANDS" "${index}";
+
+"ADDTRIAL" ("$bADDTRIAL$b",
+        "/msg $S ADDTRIAL <nick|*account>",
+        "Add the specified user to the bot as a trial helper.",
+        "$uSee also:$u helpers, addhelper, addmanager, clvl, deluser");
+"ADDHELPER" ("$bADDHELPER$b",
+        "/msg $S ADDHELPER <nick|*account>",
+        "Add the specified user to the bot as a helper.",
+        "$uSee also:$u helpers, addtrial, addmanager, clvl, deluser");
+"ADDMANAGER" ("$bADDMANAGER$b",
+        "/msg $S ADDMANAGER <nick|*account>",
+        "Add the specified user to the bot as a manager.",
+        "$uSee also:$u helpers, addtrial, addhelper, clvl, deluser");
+"ADDOWNER" ("$bADDOWNER$b",
+        "/msg $O HELPSERV ADDOWNER <bot-nick> <nick|*account>",
+        "Add the specified user to the bot as a owner.",
+        "$uSee also:$u clvl, delowner, helpers");
+"ADDNOTE" ("$bADDNOTE$b",
+        "/msg $S ADDNOTE <reqid|nick|*account> <message>",
+        "Adds a helper note to a request (useful for requests that last a long period of time).",
+        "$uSee also:$u show");
+"BOTS" ("$bBOTS$b",
+        "/msg $O HELPSERV BOTS",
+        "Lists the currently defined HelpServ bots (along with their channels and owners' accounts).",
+        "$uSee also:$u EXPIRE");
+"EXPIRE" ("$bEXPIRE$b",
+        "/msg $O HELPSERV EXPIRE",
+        "Expires any HelpServ bots that have been inactive for too long.",
+        "$uSee also:$u BOTS");
+"GIVEOWNERSHIP" ("$bGIVEOWNERSHIP$b",
+        "/msg $S GIVEOWNERSHIP <nick|*account> CONFIRM",
+        "Gives ownership of the bot to another user on the userlist.");
+"CLOSE" ("$bCLOSE$b",
+        "/msg $S CLOSE <reqid|nick|*account> [reason]",
+        "Closes out the specified request. The optional [reason] is included in the request log file.");
+"CLVL" ("$bCLVL$b",
+        "/msg $S CLVL <user|*nick> <new-level>",
+        "Allows an owner or manager to change a user's access with $S.",
+        "$uSee also:$u addtrial, addhelper, addmanager, deluser, helpers");
+"DELOWNER" ("$bDELOWNER$b",
+        "/msg $O HELPSERV DELOWNER <bot-nick> <nick|*account>",
+        "Delete an owner from the channel user list.  (Secretly, this is just an alias for deluser.)",
+        "$uSee also:$u clvl, deluser, helpers");
+"DELUSER" ("$bDELUSER$b",
+        "/msg $S DELUSER <nick|*account>",
+        "Delete a user from the channel user list.",
+        "$uSee also:$u addhelper, addmanager, clvl, helpers");
+"HELP" ("$bHELP$b",
+        "/msg $S HELP [command|topic]",
+        "HELP will show you usage or other information for the command you give.",
+        "All help entries will use the same syntax for usage, with optional parameters listed in [] and meta-parameters listed in <> (a meta-parameter is one that you must provide a value for).",
+        "For example, \"/msg $S VERSION [CVS]\" means that you may send either \"VERSION\" or \"VERSION CVS\" as a command; \"/msg $S HELP [<command>]\" means that you may send \"HELP\" alone, or fill in the name of a command, as in \"HELP VERSION\".",
+        "If you do not give a command or topic for HELP, an index is shown.");
+"HELPERS" ("$bHELPERS$b",
+        "/msg $S HELPERS",
+        "Displays the trials, helpers, managers and owner(s) registered with $S.",
+        "$uSee also:$u addtrial, addhelper, addmanager, deluser, tlist, hlist, mlist, wlist");
+"WLIST" ("$bWLIST$b",
+        "/msg $S WLIST",
+        "Displays the owner(s) registered with $S.",
+        "$uSee also:$u helpers");
+"MLIST" ("$bMLIST$b",
+        "/msg $S MLIST",
+        "Displays the managers registered with $S.",
+        "$uSee also:$u helpers");
+"HLIST" ("$bHLIST$b",
+        "/msg $S HLIST",
+        "Displays the helpers registered with $S.",
+        "$uSee also:$u helpers");
+"TLIST" ("$bTLIST$b",
+        "/msg $S TLIST",
+        "Displays the trials registered with $S.",
+        "$uSee also:$u helpers");
+"LIST" ("$bLIST$b",
+        "/msg $S LIST [unassigned|assigned|me|all]",
+        "Lists open help requests of the specified type (if not specified, defaults to \"unassigned\").",
+        "\"assigned\" means that a helper has picked up the request or been assigned to handle it.",
+        "\"me\" means to only show requests assigned to you.",
+        "$uSee also:$u next, pickup");
+"MOVE" ("$bMOVE$b",
+        "/msg $O HELPSERV MOVE <bot-nick> <new-nick|#new-channel>",
+        "Makes the HelpServ bot use a different nick or channel.");
+"NEXT" ("$bNEXT$b",
+        "/msg $S NEXT",
+        "Assigns the next unhandled help request to you.",
+        "$uSee also:$u list, pickup, reassign, close");
+"PAGE" ("$bPAGE$b",
+        "/msg $S PAGE <reason for page>",
+        "Pages the current page target, requesting attention.",
+        "$uSee also:$u set");
+"PICKUP" ("$bPICKUP$b",
+        "/msg $S PICKUP <reqid|nick|*account>",
+        "Assigns the request with $breqid$b to you.",
+        "$uSee also:$u list, next, reassign, close");
+"READHELP" ("$bREADHELP$b",
+        "/msg $O HELPSERV READHELP",
+        "Re-reads the HelpServ help file.");
+"REASSIGN" ("$bREASSIGN$b",
+        "/msg $S REASSIGN <reqid|nick|*account> <new-helper-nick>",
+        "Reassigns (or assigns) the specified request to $bnew_helper_nick$b.",
+        "$uSee also:$u next, pickup, close");
+"REGISTER" ("$bREGISTER$b",
+        "/msg $O HELPSERV REGISTER <bot-nick> <#channel> <owner-nick|*account>",
+        "Registers a new HelpServ bot to the specified channel, and adds the given user as owner.",
+        "$uSee also:$u unregister, helpers");
+"SET" ("$bSET$b",
+        "/msg $S SET [<option> [new-value]]",
+        "Change service options.  If no option is specified, show all options.  If no value is specified, show current value of option.",
+        "PageTarget       Send command pages to this channel",
+        "PageType         Delivery type for command pages (sent by the PAGE command)",
+        "AlertPageTarget  Send alert pages to this channel",
+        "AlertPageType    Delivery type for alert pages (to gain helpers' attention)",
+        "StatusPageTarget Send status pages to this channel",
+        "StatusPageType   Delivery type for status pages (less urgent information, such as requests being opened/assigned/closed)",
+        "Greeting         Message sent to users joining channel",
+        "ReqOpened        Message sent to user opening a new request",
+        "ReqAssigned      Message sent to user when their request is assigned to a helper",
+        "ReqClosed        Message sent to user when their request is closed",
+        "IdleDelay        Time user may idle in channel before bot complains to StatusPageTarget",
+        "WhineInterval    Time between complaints about unhandled requests and idle users",
+        "WhineDelay       Time before an unhandled request makes bot complain to AlertPageTarget",
+        "EmptyInterval    Time between complaints (to AlertPageTarget) that the help channel is unstaffed",
+        "StaleDelay       Time after which updating an old request causes it to send an activity alert",
+        "ReqPersist       Persistence for new requests",
+        "HelperPersist    Persistence for assignation of requests to helpers",
+        "Notification     Notification to helpers of events concerning their requests",
+        "IdWrap           Maximum request ID plus one (IDs wrap to 0 when they reach this)",
+        "ReqMaxLen        Maximum number of lines of text allowed in a request",
+        "PrivmsgOnly      Messages to users are sent as privmsg (overriding account preference)",
+        "ReqOnJoin        Automatically opens a request for a user who joins the channel",
+        "AutoVoice        Automatically voices users when their request is assigned to a helper",
+        "AutoDeVoice      Automatically devoices users when their request is closed or their helper is unassigned",
+        "$uSee also:$u set <option-name>");
+"SET PAGETARGET" ("$bSET PAGETARGET$b",
+        "/msg $S SET PAGETARGET [#channel-name]",
+        "This sets the destination for pages and other alerts.  The target may be set to * to disable pages entirely.",
+        "Just like $C registration requires an oper or helper, this option can only be set by an oper.",
+        "$uSee also:$u set, set pagetype, set statuspagetype");
+"PAGE TYPES" ("$bPAGE TYPES$b",
+        "The following types of pages may be used:",
+        "None            Disable this particular page type",
+        "Notice          Send as a notice to PageTarget",
+        "Privmsg         Send as a normal message to PageTarget",
+        "Onotice         Send as a notice to channel operators in PageTarget",
+        "$uSee also:$u set, set pagetarget, set pagetype, set statuspagetype");
+"SET PAGETYPE" ("$bSET PAGETYPE$b",
+        "/msg $S SET PAGETYPE <page-type>",
+        "This sets the type of page for pages sent using the $bpage$b command.",
+        "$uSee also:$u set, set pagetarget, page types, page");
+"SET STATUSPAGETYPE" ("$bSET STATUSPAGETYPE$b",
+        "/msg $S SET STATUSPAGETYPE <page-type>",
+        "This sets the type of page sent when a new request is opened.",
+        "$uSee also:$u set, set pagetarget, page types, page");
+"SET GREETING" ("$bSET GREETING$b",
+        "/msg $S SET GREETING <greeting-text>",
+        "This sets the message sent to users when they join the channel -- for example, the URL for a FAQ or \"How to get help\" page.",
+        "You may disable this message by using * as the text.",
+        "$uSee also:$u set");
+"SET OPENREQ" ("$bSET OPENREQ$b",
+        "/msg $S SET OPENREQ <openreq-text>",
+        "This sets a message sent to users when they open a new request with the bot.  It is sent just before more mechanical information about the request ID, current wait time, and request persistence.",
+        "You may disable this message by using * as the text.",
+        "$uSee also:$u set");
+"SET ASSIGNREQ" ("$bSET ASSIGNREQ$b",
+        "/msg $S SET ASSIGNREQ <assignreq-text>",
+        "This sets a message sent to users when their request is assigned to a helper (or re-assigned to another helper).",
+        "You may disable this message by using * as the text.",
+        "$uSee also:$u set");
+"SET CLOSEREQ" ("$bSET CLOSEREQ$b",
+        "/msg $S SET CLOSEREQ <closereq-text>",
+        "This sets a message sent to users when their request is closed.",
+        "You may disable this message by using * as the text.",
+        "$uSee also:$u set");
+"SET IDLEDELAY" ("$bSET IDLEDELAY$b",
+        "/msg $S SET IDLEDELAY <interval>",
+        "If a user idles in the help channel longer than the IdleDelay, then the bot will complain every WhineInterval reminding helpers of all the users that are waiting and idle at least IdleDelay. As a user without voice is forced to be idle, this makes sense primarily if the help channel is moderated (+m) and the helpers voice users (+v) to ask them what they need.",
+        "You may disable this by using 0 as the interval.",
+        "$uSee also:$u set, set whineinterval");
+"SET WHINEDELAY" ("$bSET WHINEDELAY$b",
+        "/msg $S SET WHINEDELAY <interval>",
+        "If any unhandled requests in the queue are at least this old, the bot will send a page requesting someone to handle it.",
+        "If the request remains unhandled, another complaint will be sent every WhineInterval.",
+        "You may disable this by using 0 as the interval.",
+        "$uSee also:$u set, set whineinterval");
+"SET WHINEINTERVAL" ("$bSET WHINEINTERVAL$b",
+        "/msg $S SET WHINEINTERVAL <interval>",
+        "If any unhandled requests in the queue are at least WhineDelay old, then every WhineInterval, the bot will send a complaint about each unhandled request at least WhineDelay old, reminding helpers that the request is still unhandled.",
+        "IdleDelay messages are also sent every WhineInterval.",
+        "If you use 0 as the interval, only the initial request will be sent.",
+        "$uSee also:$u set, set whinedelay, set idledelay");
+"SET EMPTYINTERVAL" ("$bSET EMPTYINTERVAL$b",
+        "/msg $S SET EMPTYINTERVAL <interval>",
+        "If the help channel is unstaffed (there are no helpers in the channel) the bot will send a complaint to PageTarget every EmptyInterval.",
+        "You may disable this by using 0 as the interval.",
+        "$uSee also:$u set");
+"SET STALEDELAY" ("$bSET STALEDELAY$b",
+        "/msg $S SET STALEDELAY <interval>",
+        "If a user adds additional information to a request that has been otherwise inactive for at least this amount of time, the PageTarget is sent a message set by StatusPageType mentioning the update.",
+        "You may disable this by using 0 as the interval.",
+        "$uSee also:$u set, set pagetarget, set statuspagetype");
+"SET REQPERSIST" ("$bSET REQPERSIST$b",
+        "/msg $S SET REQPERSIST <persistence-type>",
+        "This sets the \"request persistence\" for all requests.",
+        "The following types of persistence may be used:",
+        "Part            Request is closed when the user parts the channel",
+        "Quit            Request is closed when the user disconnects from IRC",
+        "Close           Request is only closed when a helper closes it",
+        "$uPart$u and $uQuit$u type requests, and $uClose$u type requests with no associated account, will be automatically closed when the services restart.",
+        "$uSee also:$u set, set helperpersist");
+"SET HELPERPERSIST" ("$bSET HELPERPERSIST$b",
+        "/msg $S SET REQPERSIST <persistence-type>",
+        "This sets the \"helper persistence\" for all requests.",
+        "The following types of persistence may be used:",
+        "Part            Request is marked unassigned when the helper parts the channel",
+        "Quit            Request is marked unassigned when the helper disconnects from IRC",
+        "Close           Request is never marked unassigned",
+        "$uPart$u and $uQuit$u type requests will be marked unassigned when the services restart.",
+        "$uSee also:$u set, set reqpersist");
+"SET NOTIFICATION" ("$bSET NOTIFICATION$b",
+        "/msg $S SET NOTIFICATION <notification-type>",
+        "This sets the \"notification level\" for all requests. It determines which messages are sent to helpers when something happens with their request, or the user/handle the request is associated with.",
+        "The following notification levels may be used:",
+        "None             No notifications are sent",
+        "ReqDrop          Send a notification if a request is dropped",
+        "UserChanges      Above, and also notify of the user changing nick, disconnecting, or coming online",
+        "AccountChanges   Above, and also notify when the user authenticates to a handle or unregisters a handle ($bcan only be set by an IRCOP$b)",
+        "$uSee also:$u set");
+"SET PRIVMSGONLY" ("$bSET PRIVMSGONLY$b",
+        "/msg $S SET PRIVMSGONLY <Yes|No>",
+        "This sets if messages to users communicating with $S will have their responses in privmsg format (overriding their account's preference for privmsg or notice).",
+        "Helpers are not affected by this setting.",
+        "$uSee also:$u set");
+"SET REQONJOIN" ("$bSET REQONJOIN$b",
+        "/msg $S SET REQONJOIN <Yes|No>",
+        "This sets if requests are automatically opened whenever someone joins the help channel. They contain no text by default.",
+        "Helpers are not affected by this setting.",
+        "$uSee also:$u set");
+"SHOW" ("$bSHOW$b",
+        "/msg $S SHOW <reqid|nick|*account>",
+        "Shows the details of the specified request.",
+        "$uSee also:$u list");
+"STATS" ("$bSTATS$b",
+        "/msg $S STATS <nick|*account>",
+        "Shows time and request statistics for the specified helper.  (If no one is specified, shows your own stats.)",
+        "Only managers and owners may look at stats for other users.");
+"STATSREPORT" ("$bSTATSREPORT$b",
+        "/msg $S STATSREPORT [NOTICE]",
+        "Sends a list of how long each helper has been in the help channel for the past 4 weeks (separated by week)",
+        "If you give the \"NOTICE\" argument, it will send the report as NOTICEs instead of PRIVMSGs.",
+        "$uSee also:$u stats");
+"UNREGISTER" ("$bUNREGISTER$b",
+        "/msg $S UNREGISTER CONFIRM",
+        "Unregisters the channel and removes the service from the channel.");
+"VERSION" ("$bVERSION$b",
+        "/msg $S VERSION [CVS]",
+        "This tells you the version of srvx this bot is part of.",
+        "If you give the \"CVS\" argument, the source-code revision of $S is also shown.");
+"WRITE" ("$bWRITE$b",
+        "/msg $O HELPSERV WRITE",
+        "Saves all HelpServ bot data to the disk database.");
diff --git a/src/mod-memoserv.c b/src/mod-memoserv.c
new file mode 100644 (file)
index 0000000..0a0ee69
--- /dev/null
@@ -0,0 +1,623 @@
+/* memoserv.c - MemoServ module for srvx
+ * Copyright 2003-2004 Martijn Smit and srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+/* Adds new section to srvx.conf:
+ * "modules" {
+ *     "memoserv" {
+ *         "bot" "NickServ";
+ *         "message_expiry" "30d"; // age when messages are deleted; set
+ *                                 // to 0 to disable message expiration
+ *     };
+ *  };
+ *
+ * After that, to make the module active on an existing bot:
+ * /msg opserv bind nickserv * *memoserv.*
+ *
+ * If you want a dedicated MemoServ bot, make sure the service control
+ * commands are bound to OpServ:
+ * /msg opserv bind opserv service *modcmd.joiner
+ * /msg opserv bind opserv service\ add *modcmd.service\ add
+ * /msg opserv bind opserv service\ rename *modcmd.service\ rename
+ * /msg opserv bind opserv service\ trigger *modcmd.service\ trigger
+ * /msg opserv bind opserv service\ remove *modcmd.service\ remove
+ * Add the bot:
+ * /msg opserv service add MemoServ User-to-user Memorandum Service
+ * /msg opserv bind memoserv help *modcmd.help
+ * Restart srvx with the updated conf file (as above, butwith "bot"
+ * "MemoServ"), and bind the commands to it:
+ * /msg opserv bind memoserv * *memoserv.*
+ * /msg opserv bind memoserv set *modcmd.joiner
+ */
+
+#include "chanserv.h"
+#include "conf.h"
+#include "modcmd.h"
+#include "saxdb.h"
+#include "timeq.h"
+
+#define KEY_SENT "sent"
+#define KEY_RECIPIENT "to"
+#define KEY_FROM "from"
+#define KEY_MESSAGE "msg"
+#define KEY_READ "read"
+
+static const struct message_entry msgtab[] = {
+    { "MSMSG_CANNOT_SEND", "You cannot send to account $b%s$b." },
+    { "MSMSG_MEMO_SENT", "Message sent to $b%s$b." },
+    { "MSMSG_NO_MESSAGES", "You have no messages." },
+    { "MSMSG_MEMOS_FOUND", "Found $b%d$b matches.\nUse /msg $S READ <ID> to read a message." },
+    { "MSMSG_CLEAN_INBOX", "You have $b%d$b or more messages, please clean out your inbox.\nUse /msg $S READ <ID> to read a message." },
+    { "MSMSG_LIST_HEAD", "$bID$b   $bFrom$b       $bTime Sent$b" },
+    { "MSMSG_LIST_FORMAT", "%-2u     %s           %s" },
+    { "MSMSG_MEMO_HEAD", "Memo %u From $b%s$b, received on %s:" },
+    { "MSMSG_BAD_MESSAGE_ID", "$b%s$b is not a valid message ID (it should be a number between 0 and %u)." },
+    { "MSMSG_NO_SUCH_MEMO", "You have no memo with that ID." },
+    { "MSMSG_MEMO_DELETED", "Memo $b%d$b deleted." },
+    { "MSMSG_EXPIRY_OFF", "I am currently not expiring messages. (turned off)" },
+    { "MSMSG_EXPIRY", "Messages will be expired when they are %s old (%d seconds)." },
+    { "MSMSG_MESSAGES_EXPIRED", "$b%lu$b message(s) expired." },
+    { "MSMSG_MEMOS_INBOX", "You have $b%d$b new message(s) in your inbox and %d old messages.  Use /msg $S LIST to list them." },
+    { "MSMSG_NEW_MESSAGE", "You have a new message from $b%s$b." },
+    { "MSMSG_DELETED_ALL", "Deleted all of your messages." },
+    { "MSMSG_USE_CONFIRM", "Please use /msg $S DELETE * $bCONFIRM$b to delete $uall$u of your messages." },
+    { "MSMSG_STATUS_TOTAL", "I have $b%u$b memos in my database." },
+    { "MSMSG_STATUS_EXPIRED", "$b%ld$b memos expired during the time I am awake." },
+    { "MSMSG_STATUS_SENT", "$b%ld$b memos have been sent." },
+    { "MSMSG_SET_NOTIFY",     "$bNotify:       $b %s" },
+    { "MSMSG_SET_AUTHNOTIFY", "$bAuthNotify:   $b %s" },
+    { "MSMSG_SET_PRIVATE",    "$bPrivate:      $b %s" },
+    { NULL, NULL }
+};
+
+struct memo {
+    struct memo_account *recipient;
+    struct memo_account *sender;
+    char *message;
+    time_t sent;
+    unsigned int is_read : 1;
+};
+
+DECLARE_LIST(memoList, struct memo*);
+DEFINE_LIST(memoList, struct memo*);
+
+/* memo_account.flags fields */
+#define MEMO_NOTIFY_NEW   1
+#define MEMO_NOTIFY_LOGIN 2
+#define MEMO_DENY_NONCHANNEL 4
+
+struct memo_account {
+    struct handle_info *handle;
+    unsigned int flags;
+    struct memoList sent;
+    struct memoList recvd;
+};
+
+static struct {
+    struct userNode *bot;
+    int message_expiry;
+} memoserv_conf;
+
+const char *memoserv_module_deps[] = { NULL };
+static struct module *memoserv_module;
+static struct log_type *MS_LOG;
+static unsigned long memosSent, memosExpired;
+static struct dict *memos; /* memo_account->handle->handle -> memo_account */
+
+static struct memo_account *
+memoserv_get_account(struct handle_info *hi)
+{
+    struct memo_account *ma;
+    if (!hi)
+        return NULL;
+    ma = dict_find(memos, hi->handle, NULL);
+    if (ma)
+        return ma;
+    ma = calloc(1, sizeof(*ma));
+    if (!ma)
+        return ma;
+    ma->handle = hi;
+    ma->flags = MEMO_NOTIFY_NEW | MEMO_NOTIFY_LOGIN;
+    dict_insert(memos, ma->handle->handle, ma);
+    return ma;
+}
+
+static void
+delete_memo(struct memo *memo)
+{
+    memoList_remove(&memo->recipient->recvd, memo);
+    memoList_remove(&memo->sender->sent, memo);
+    free(memo->message);
+    free(memo);
+}
+
+static void
+delete_memo_account(void *data)
+{
+    struct memo_account *ma = data;
+
+    while (ma->recvd.used)
+        delete_memo(ma->recvd.list[0]);
+    while (ma->sent.used)
+        delete_memo(ma->sent.list[0]);
+    memoList_clean(&ma->recvd);
+    memoList_clean(&ma->sent);
+    free(ma);
+}
+
+void
+do_expire(void)
+{
+    dict_iterator_t it;
+    for (it = dict_first(memos); it; it = iter_next(it)) {
+        struct memo_account *acct = iter_data(it);
+        unsigned int ii;
+        for (ii = 0; ii < acct->sent.used; ++ii) {
+            struct memo *memo = acct->sent.list[ii];
+            if ((now - memo->sent) > memoserv_conf.message_expiry) {
+                delete_memo(memo);
+                memosExpired++;
+                ii--;
+            }
+        }
+    }
+}
+
+static void
+expire_memos(UNUSED_ARG(void *data))
+{
+    if (memoserv_conf.message_expiry) {
+        do_expire();
+        timeq_add(now + memoserv_conf.message_expiry, expire_memos, NULL);
+    }
+}
+
+static struct memo*
+add_memo(time_t sent, struct memo_account *recipient, struct memo_account *sender, char *message)
+{
+    struct memo *memo;
+
+    memo = calloc(1, sizeof(*memo));
+    if (!memo)
+        return NULL;
+
+    memo->recipient = recipient;
+    memoList_append(&recipient->recvd, memo);
+    memo->sender = sender;
+    memoList_append(&sender->sent, memo);
+    memo->sent = sent;
+    memo->message = strdup(message);
+    memosSent++;
+    return memo;
+}
+
+static int
+memoserv_can_send(struct userNode *bot, struct userNode *user, struct memo_account *acct)
+{
+    extern struct userData *_GetChannelUser(struct chanData *channel, struct handle_info *handle, int override, int allow_suspended);
+    struct userData *dest;
+
+    if (!user->handle_info)
+        return 0;
+    if (!(acct->flags & MEMO_DENY_NONCHANNEL))
+        return 1;
+    for (dest = acct->handle->channels; dest; dest = dest->u_next)
+        if (_GetChannelUser(dest->channel, user->handle_info, 1, 0))
+            return 1;
+    send_message(user, bot, "MSMSG_CANNOT_SEND", acct->handle->handle);
+    return 0;
+}
+
+static struct memo *find_memo(struct userNode *user, struct svccmd *cmd, struct memo_account *ma, const char *msgid, unsigned int *id)
+{
+    unsigned int memoid;
+    if (!isdigit(msgid[0])) {
+        if (ma->recvd.used)
+            reply("MSMSG_BAD_MESSAGE_ID", msgid, ma->recvd.used - 1);
+        else
+            reply("MSMSG_NO_MESSAGES");
+        return NULL;
+    }
+    memoid = atoi(msgid);
+    if (memoid >= ma->recvd.used) {
+        reply("MSMSG_NO_SUCH_MEMO");
+        return NULL;
+    }
+    return ma->recvd.list[*id = memoid];
+}
+
+static MODCMD_FUNC(cmd_send)
+{
+    char *message;
+    struct handle_info *hi;
+    struct memo_account *ma, *sender;
+
+    if (!(hi = modcmd_get_handle_info(user, argv[1])))
+        return 0;
+    if (!(sender = memoserv_get_account(user->handle_info))
+        || !(ma = memoserv_get_account(hi))) {
+        reply("MSG_INTERNAL_FAILURE");
+        return 0;
+    }
+    if (!(memoserv_can_send(cmd->parent->bot, user, ma)))
+        return 0;
+    message = unsplit_string(argv + 2, argc - 2, NULL);
+    add_memo(now, ma, sender, message);
+    if (ma->flags & MEMO_NOTIFY_NEW) {
+        struct userNode *other;
+        for (other = ma->handle->users; other; other = other->next_authed)
+            send_message(other, cmd->parent->bot, "MSMSG_NEW_MESSAGE", user->nick);
+    }
+    reply("MSMSG_MEMO_SENT", ma->handle->handle);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_list)
+{
+    struct memo_account *ma;
+    struct memo *memo;
+    unsigned int ii;
+    char posted[24];
+    struct tm tm;
+
+    if (!(ma = memoserv_get_account(user->handle_info)))
+        return 0;
+    reply("MSMSG_LIST_HEAD");
+    for (ii = 0; (ii < ma->recvd.used) && (ii < 15); ++ii) {
+        memo = ma->recvd.list[ii];
+        localtime_r(&memo->sent, &tm);
+        strftime(posted, sizeof(posted), "%I:%M %p, %m/%d/%Y", &tm);
+        reply("MSMSG_LIST_FORMAT", ii, memo->sender->handle->handle, posted);
+    }
+    if (ii == 0)
+        reply("MSG_NONE");
+    else if (ii == 15)
+        reply("MSMSG_CLEAN_INBOX", ii);
+    else
+        reply("MSMSG_MEMOS_FOUND", ii);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_read)
+{
+    struct memo_account *ma;
+    unsigned int memoid;
+    struct memo *memo;
+    char posted[24];
+    struct tm tm;
+
+    if (!(ma = memoserv_get_account(user->handle_info)))
+        return 0;
+    if (!(memo = find_memo(user, cmd, ma, argv[1], &memoid)))
+        return 0;
+    localtime_r(&memo->sent, &tm);
+    strftime(posted, sizeof(posted), "%I:%M %p, %m/%d/%Y", &tm);
+    reply("MSMSG_MEMO_HEAD", memoid, memo->sender->handle->handle, posted);
+    send_message_type(4, user, cmd->parent->bot, "%s", memo->message);
+    memo->is_read = 1;
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_delete)
+{
+    struct memo_account *ma;
+    struct memo *memo;
+    unsigned int memoid;
+
+    if (!(ma = memoserv_get_account(user->handle_info)))
+        return 0;
+    if (!irccasecmp(argv[1], "*") || !irccasecmp(argv[1], "all")) {
+        if ((argc < 3) || irccasecmp(argv[2], "confirm")) {
+            reply("MSMSG_USE_CONFIRM");
+            return 0;
+        }
+        while (ma->recvd.used)
+            delete_memo(ma->recvd.list[0]);
+        reply("MSMSG_DELETED_ALL");
+        return 1;
+    }
+
+    if (!(memo = find_memo(user, cmd, ma, argv[1], &memoid)))
+        return 0;
+    delete_memo(memo);
+    reply("MSMSG_MEMO_DELETED", memoid);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_expire)
+{
+    unsigned long old_expired = memosExpired;
+    do_expire();
+    reply("MSMSG_MESSAGES_EXPIRED", memosExpired - old_expired);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_expiry)
+{
+    char interval[INTERVALLEN];
+
+    if (!memoserv_conf.message_expiry) {
+        reply("MSMSG_EXPIRY_OFF");
+        return 1;
+    }
+
+    intervalString(interval, memoserv_conf.message_expiry);
+    reply("MSMSG_EXPIRY", interval, memoserv_conf.message_expiry);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_set_notify)
+{
+    struct memo_account *ma;
+    char *choice;
+
+    if (!(ma = memoserv_get_account(user->handle_info)))
+        return 0;
+    if (argc > 1) {
+        choice = argv[1];
+        if (enabled_string(choice)) {
+            ma->flags |= MEMO_NOTIFY_NEW;
+        } else if (disabled_string(choice)) {
+            ma->flags &= ~MEMO_NOTIFY_NEW;
+        } else {
+            reply("MSG_INVALID_BINARY", choice);
+            return 0;
+        }
+    }
+
+    choice = (ma->flags & MEMO_NOTIFY_NEW) ? "on" : "off";
+    reply("MSMSG_SET_NOTIFY", choice);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_set_authnotify)
+{
+    struct memo_account *ma;
+    char *choice;
+
+    if (!(ma = memoserv_get_account(user->handle_info)))
+        return 0;
+    if (argc > 1) {
+        choice = argv[1];
+        if (enabled_string(choice)) {
+            ma->flags |= MEMO_NOTIFY_LOGIN;
+        } else if (disabled_string(choice)) {
+            ma->flags &= ~MEMO_NOTIFY_LOGIN;
+        } else {
+            reply("MSG_INVALID_BINARY", choice);
+            return 0;
+        }
+    }
+
+    choice = (ma->flags & MEMO_NOTIFY_LOGIN) ? "on" : "off";
+    reply("MSMSG_SET_AUTHNOTIFY", choice);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_set_private)
+{
+    struct memo_account *ma;
+    char *choice;
+
+    if (!(ma = memoserv_get_account(user->handle_info)))
+        return 0;
+    if (argc > 1) {
+        choice = argv[1];
+        if (enabled_string(choice)) {
+            ma->flags |= MEMO_DENY_NONCHANNEL;
+        } else if (disabled_string(choice)) {
+            ma->flags &= ~MEMO_DENY_NONCHANNEL;
+        } else {
+            reply("MSG_INVALID_BINARY", choice);
+            return 0;
+        }
+    }
+
+    choice = (ma->flags & MEMO_DENY_NONCHANNEL) ? "on" : "off";
+    reply("MSMSG_SET_PRIVATE", choice);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_status)
+{
+    reply("MSMSG_STATUS_TOTAL", dict_size(memos));
+    reply("MSMSG_STATUS_EXPIRED", memosExpired);
+    reply("MSMSG_STATUS_SENT", memosSent);
+    return 1;
+}
+
+static void
+memoserv_conf_read(void)
+{
+    dict_t conf_node;
+    const char *str;
+
+    str = "modules/memoserv";
+    if (!(conf_node = conf_get_data(str, RECDB_OBJECT))) {
+        log_module(MS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", str);
+        return;
+    }
+
+    str = database_get_data(conf_node, "message_expiry", RECDB_QSTRING);
+    memoserv_conf.message_expiry = str ? ParseInterval(str) : 60*24*30;
+}
+
+static int
+memoserv_saxdb_read(struct dict *db)
+{
+    char *str;
+    struct handle_info *sender, *recipient;
+    struct record_data *hir;
+    struct memo *memo;
+    dict_iterator_t it;
+    time_t sent;
+
+    for (it = dict_first(db); it; it = iter_next(it)) {
+        hir = iter_data(it);
+        if (hir->type != RECDB_OBJECT) {
+            log_module(MS_LOG, LOG_WARNING, "Unexpected rectype %d for %s.", hir->type, iter_key(it));
+            continue;
+        }
+
+        if (!(str = database_get_data(hir->d.object, KEY_SENT, RECDB_QSTRING))) {
+            log_module(MS_LOG, LOG_ERROR, "Date sent not present in memo %s; skipping", iter_key(it));
+            continue;
+        }
+        sent = atoi(str);
+
+        if (!(str = database_get_data(hir->d.object, KEY_RECIPIENT, RECDB_QSTRING))) {
+            log_module(MS_LOG, LOG_ERROR, "Recipient not present in memo %s; skipping", iter_key(it));
+            continue;
+        } else if (!(recipient = get_handle_info(str))) {
+            log_module(MS_LOG, LOG_ERROR, "Invalid recipient %s in memo %s; skipping", str, iter_key(it));
+            continue;
+        }
+
+        if (!(str = database_get_data(hir->d.object, KEY_FROM, RECDB_QSTRING))) {
+            log_module(MS_LOG, LOG_ERROR, "Sender not present in memo %s; skipping", iter_key(it));
+            continue;
+        } else if (!(sender = get_handle_info(str))) {
+            log_module(MS_LOG, LOG_ERROR, "Invalid sender %s in memo %s; skipping", str, iter_key(it));
+            continue;
+        }
+
+        if (!(str = database_get_data(hir->d.object, KEY_MESSAGE, RECDB_QSTRING))) {
+            log_module(MS_LOG, LOG_ERROR, "Message not present in memo %s; skipping", iter_key(it));
+            continue;
+        }
+
+        memo = add_memo(sent, memoserv_get_account(recipient), memoserv_get_account(sender), str);
+        if ((str = database_get_data(hir->d.object, KEY_READ, RECDB_QSTRING)))
+            memo->is_read = 1;
+    }
+    return 0;
+}
+
+static int
+memoserv_saxdb_write(struct saxdb_context *ctx)
+{
+    dict_iterator_t it;
+    struct memo_account *ma;
+    struct memo *memo;
+    char str[7];
+    unsigned int id = 0, ii;
+
+    for (it = dict_first(memos); it; it = iter_next(it)) {
+        ma = iter_data(it);
+        for (ii = 0; ii < ma->recvd.used; ++ii) {
+            memo = ma->recvd.list[ii];
+            saxdb_start_record(ctx, inttobase64(str, id++, sizeof(str)), 0);
+            saxdb_write_int(ctx, KEY_SENT, memo->sent);
+            saxdb_write_string(ctx, KEY_RECIPIENT, memo->recipient->handle->handle);
+            saxdb_write_string(ctx, KEY_FROM, memo->sender->handle->handle);
+            saxdb_write_string(ctx, KEY_MESSAGE, memo->message);
+            if (memo->is_read)
+                saxdb_write_int(ctx, KEY_READ, 1);
+            saxdb_end_record(ctx);
+        }
+    }
+    return 0;
+}
+
+static void
+memoserv_cleanup(void)
+{
+    dict_delete(memos);
+}
+
+static void
+memoserv_check_messages(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
+{
+    unsigned int ii, unseen;
+    struct memo_account *ma;
+    struct memo *memo;
+
+    if (!(ma = memoserv_get_account(user->handle_info))
+        || !(ma->flags & MEMO_NOTIFY_LOGIN))
+        return;
+    for (ii = unseen = 0; ii < ma->recvd.used; ++ii) {
+        memo = ma->recvd.list[ii];
+        if (!memo->is_read)
+            unseen++;
+    }
+    if (ma->recvd.used && memoserv_conf.bot)
+        send_message(user, memoserv_conf.bot, "MSMSG_MEMOS_INBOX", unseen, ma->recvd.used - unseen);
+}
+
+static void
+memoserv_rename_account(struct handle_info *hi, const char *old_handle)
+{
+    struct memo_account *ma;
+    if (!(ma = dict_find(memos, old_handle, NULL)))
+        return;
+    dict_remove2(memos, old_handle, 1);
+    dict_insert(memos, hi->handle, ma);
+}
+
+static void
+memoserv_unreg_account(UNUSED_ARG(struct userNode *user), struct handle_info *handle)
+{
+    dict_remove(memos, handle->handle);
+}
+
+int
+memoserv_init(void)
+{
+    MS_LOG = log_register_type("MemoServ", "file:memoserv.log");
+    memos = dict_new();
+    dict_set_free_data(memos, delete_memo_account);
+    reg_auth_func(memoserv_check_messages);
+    reg_handle_rename_func(memoserv_rename_account);
+    reg_unreg_func(memoserv_unreg_account);
+    conf_register_reload(memoserv_conf_read);
+    reg_exit_func(memoserv_cleanup);
+    saxdb_register("MemoServ", memoserv_saxdb_read, memoserv_saxdb_write);
+
+    memoserv_module = module_register("MemoServ", MS_LOG, "mod-memoserv.help", NULL);
+    modcmd_register(memoserv_module, "send", cmd_send, 3, MODCMD_REQUIRE_AUTHED, NULL);
+    modcmd_register(memoserv_module, "list", cmd_list, 1, MODCMD_REQUIRE_AUTHED, NULL);
+    modcmd_register(memoserv_module, "read", cmd_read, 2, MODCMD_REQUIRE_AUTHED, NULL);
+    modcmd_register(memoserv_module, "delete", cmd_delete, 2, MODCMD_REQUIRE_AUTHED, NULL);
+    modcmd_register(memoserv_module, "expire", cmd_expire, 1, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
+    modcmd_register(memoserv_module, "expiry", cmd_expiry, 1, 0, NULL);
+    modcmd_register(memoserv_module, "status", cmd_status, 1, 0, NULL);
+    modcmd_register(memoserv_module, "set notify", cmd_set_notify, 1, 0, NULL);
+    modcmd_register(memoserv_module, "set authnotify", cmd_set_authnotify, 1, 0, NULL);
+    modcmd_register(memoserv_module, "set private", cmd_set_private, 1, 0, NULL);
+    message_register_table(msgtab);
+
+    if (memoserv_conf.message_expiry)
+        timeq_add(now + memoserv_conf.message_expiry, expire_memos, NULL);
+    return 1;
+}
+
+int
+memoserv_finalize(void) {
+    dict_t conf_node;
+    const char *str;
+
+    str = "modules/memoserv";
+    if (!(conf_node = conf_get_data(str, RECDB_OBJECT))) {
+        log_module(MS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", str);
+        return 0;
+    }
+
+    str = database_get_data(conf_node, "bot", RECDB_QSTRING);
+    if (str)
+        memoserv_conf.bot = GetUserH(str);
+    return 1;
+}
diff --git a/src/mod-memoserv.help b/src/mod-memoserv.help
new file mode 100644 (file)
index 0000000..88f30dc
--- /dev/null
@@ -0,0 +1,56 @@
+"<INDEX>" ("$b$S Help$b",
+        "$b$S$b is a message relay services, with this - you can relay long distance messages to other (offline) users.",
+        "    SEND - Sends a message.",
+        "    READ - Reads a message.",
+        "  DELETE - Deletes a message.",
+        "    LIST - Lists your messages.",
+        "     SET - Sets certain options in relation with $S.",
+        "  EXPIRY - Displays current usage of expiring old messages.",
+        "  STATUS - Displays a few details about $S's status.",
+        " VERSION - Displays the current version of MemoServ.",
+        "$bPrivileged Commands:$b",
+        "  EXPIRE - Expires messages.",
+        "$b/msg $S help <command>$b for syntax and usage for a command.");
+
+"COMMANDS" "${index}";
+
+"SET" ("/msg $S SET <option> <value>",
+       "Sets a certain option for your account;",
+       "Currently, only $bset notify$b is available, see /msg $S help SET notify for further information.",
+       "$uSee Also:$u set notify");
+       
+"SET NOTIFY" ("$bSET NOTIFY$b",
+       "/msg $S SET notify <1 or 0>",
+       "Decides wether $S should notify you of the messages in your inbox, on authentication with $N AND when someone sends you a new message.");
+       
+"VERSION" ("/msg $S VERSION",
+        "Sends you the srvx version and some additional version information that is specific to $b$S$b.");
+        
+"EXPIRY" ("/msg $S EXPIRY ",
+         "Sends you the current usage of expiring old messages.");
+
+"EXPIRE" ("/msg $S EXPIRE ",
+         "Runs an expire process through $S's messages, deleting any messages that are over date.",
+         "$uSee Also:$u expiry");
+
+"SEND" ("/msg $S SEND <nick|*account> <message>",
+        "Sends a message to an user.",
+        "You may use *Account instead of Nick as the name argument; the * makes $S use the name of an account directly (useful if the user is not online).",
+        "$uSee also:$u read, list");
+
+"READ" ("/msg $S READ <memo id>",
+        "Shows you the message behind <memo id>",
+        "$uSee also:$u send, list");
+
+"DELETE" ("/msg $S DELETE <memo id/*>",
+          "Deletes <memo id> from your inbox.",
+          "NOTE: You may supply $b*$b or $ball$b as the memo id, which will result in $S deleting all your messages.");
+
+"LIST" ("/msg $S LIST",
+        "This will list all the messages in your inbox.",
+        "$uSee also:$u read, send");
+        
+"STATUS" ("/msg $S STATUS",
+        "This will list some details about $S's status, i.e the total ammount of memo's sent and expired.");
+
+"INDEX" "${index}";
diff --git a/src/mod-snoop.c b/src/mod-snoop.c
new file mode 100644 (file)
index 0000000..5ac8b04
--- /dev/null
@@ -0,0 +1,193 @@
+/* mod-snoop.c - User surveillance module (per pomac's spec)
+ * Copyright 2002-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+/* Adds new section to srvx.conf:
+ * "modules" {
+ *     "snoop" {
+ *         // Where to send snoop messages?
+ *         "channel" "#wherever";
+ *         // Which bot?
+ *         "bot" "OpServ";
+ *         // Show new users and joins from net joins?  (off by default)
+ *         "show_bursts" "0";
+ *     };
+ * };
+ */
+
+#include "conf.h"
+#include "helpfile.h"
+#include "nickserv.h"
+
+#ifdef HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#endif
+#ifdef HAVE_ARPA_INET_H
+#include <arpa/inet.h>
+#endif
+
+extern time_t now;
+static struct {
+    struct chanNode *channel;
+    struct userNode *bot;
+    unsigned int show_bursts : 1;
+    unsigned int enabled : 1;
+} snoop_cfg;
+static char timestamp[16];
+const char *snoop_module_deps[] = { NULL };
+
+static int finalized;
+int snoop_finalize(void);
+
+#define SNOOP(FORMAT, ARGS...) send_channel_message(snoop_cfg.channel, snoop_cfg.bot, "%s "FORMAT, timestamp , ## ARGS)
+#define UPDATE_TIMESTAMP() strftime(timestamp, sizeof(timestamp), "[%H:%M:%S]", localtime(&now))
+
+static void
+snoop_nick_change(struct userNode *user, const char *old_nick) {
+    if (!snoop_cfg.enabled) return;
+    UPDATE_TIMESTAMP();
+    SNOOP("$bNICK$b change %s -> %s", old_nick, user->nick);
+}
+
+static int
+snoop_join(struct modeNode *mNode) {
+    struct userNode *user = mNode->user;
+    struct chanNode *chan = mNode->channel;
+    if (!snoop_cfg.enabled) return 0;
+    if (user->uplink->burst && !snoop_cfg.show_bursts) return 0;
+    UPDATE_TIMESTAMP();
+    if (chan->members.used == 1) {
+        SNOOP("$bCREATE$b %s by %s", chan->name, user->nick);
+    } else {
+        SNOOP("$bJOIN$b %s by %s", chan->name, user->nick);
+    }
+    return 0;
+}
+
+static void
+snoop_part(struct userNode *user, struct chanNode *chan, const char *reason) {
+    if (!snoop_cfg.enabled) return;
+    if (user->dead) return;
+    UPDATE_TIMESTAMP();
+    SNOOP("$bPART$b %s by %s (%s)", chan->name, user->nick, reason ? reason : "");
+}
+
+static void
+snoop_kick(struct userNode *kicker, struct userNode *victim, struct chanNode *chan) {
+    if (!snoop_cfg.enabled) return;
+    UPDATE_TIMESTAMP();
+    SNOOP("$bKICK$b %s from %s by %s", victim->nick, chan->name, (kicker ? kicker->nick : "some server"));
+}
+
+static int
+snoop_new_user(struct userNode *user) {
+    if (!snoop_cfg.enabled) return 0;
+    if (user->uplink->burst && !snoop_cfg.show_bursts) return 0;
+    UPDATE_TIMESTAMP();
+    SNOOP("$bNICK$b %s %s@%s [%s] on %s", user->nick, user->ident, user->hostname, inet_ntoa(user->ip), user->uplink->name);
+    return 0;
+}
+
+static void
+snoop_del_user(struct userNode *user, struct userNode *killer, const char *why) {
+    if (!snoop_cfg.enabled) return;
+    UPDATE_TIMESTAMP();
+    if (killer) {
+        SNOOP("$bKILL$b %s (%s@%s, on %s) by %s (%s)", user->nick, user->ident, user->hostname, user->uplink->name, killer->nick, why);
+    } else {
+        SNOOP("$bQUIT$b %s (%s@%s, on %s) (%s)", user->nick, user->ident, user->hostname, user->uplink->name, why);
+    }
+}
+
+static void
+snoop_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle)) {
+    if (!snoop_cfg.enabled) return;
+    if (user->uplink->burst && !snoop_cfg.show_bursts) return;
+    if (user->handle_info) {
+        UPDATE_TIMESTAMP();
+        SNOOP("$bAUTH$b %s as %s", user->nick, user->handle_info->handle);
+    }
+}
+
+static void
+snoop_conf_read(void) {
+    dict_t node;
+    char *str;
+
+    node = conf_get_data("modules/snoop", RECDB_OBJECT);
+    if (!node)
+        return;
+    str = database_get_data(node, "channel", RECDB_QSTRING);
+    if (!str)
+        return;
+    snoop_cfg.channel = AddChannel(str, now, "+sntim", NULL);
+    if (!snoop_cfg.channel)
+        return;
+    str = database_get_data(node, "show_bursts", RECDB_QSTRING);
+    snoop_cfg.show_bursts = str ? enabled_string(str) : 0;
+    snoop_cfg.enabled = 1;
+    if (finalized)
+        snoop_finalize();
+}
+
+void
+snoop_cleanup(void) {
+    snoop_cfg.enabled = 0;
+    unreg_del_user_func(snoop_del_user);
+}
+
+int
+snoop_init(void) {
+    reg_exit_func(snoop_cleanup);
+    conf_register_reload(snoop_conf_read);
+    reg_nick_change_func(snoop_nick_change);
+    reg_join_func(snoop_join);
+    reg_part_func(snoop_part);
+    reg_kick_func(snoop_kick);
+    reg_new_user_func(snoop_new_user);
+    reg_del_user_func(snoop_del_user);
+    reg_auth_func(snoop_auth);
+    /* Not implemented since hooks don't exist or lack data desired:
+     * chanmode (issuing user not listed)
+     * usermode (no hook)
+     */
+    return 1;
+}
+
+int
+snoop_finalize(void) {
+    struct mod_chanmode change;
+    dict_t node;
+    char *str;
+
+    finalized = 1;
+    node = conf_get_data("modules/snoop", RECDB_OBJECT);
+    if (!node)
+        return 0;
+    str = database_get_data(node, "bot", RECDB_QSTRING);
+    if (!str)
+        return 0;
+    snoop_cfg.bot = GetUserH(str);
+    if (!snoop_cfg.bot)
+        return 0;
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    change.args[0].mode = MODE_CHANOP;
+    change.args[0].member = AddChannelUser(snoop_cfg.bot, snoop_cfg.channel);
+    mod_chanmode_announce(snoop_cfg.bot, snoop_cfg.channel, &change);
+    return 1;
+}
diff --git a/src/mod-sockcheck.c b/src/mod-sockcheck.c
new file mode 100644 (file)
index 0000000..1f252c6
--- /dev/null
@@ -0,0 +1,1192 @@
+/* sockcheck.c - insecure proxy checking
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "conf.h"
+#include "gline.h"
+#include "ioset.h"
+#include "modcmd.h"
+#include "timeq.h"
+
+#ifdef HAVE_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif
+#ifdef HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#endif
+#ifdef HAVE_ARPA_INET_H
+#include <arpa/inet.h>
+#endif
+
+/* TODO, 1.3 or later: allow rules like "27374:" "reject:Subseven detected";
+ * (For convenience; right now it assumes that there will be a state
+ * rather than an immediate response upon connection.)
+ */
+
+#if !defined(SOCKCHECK_DEBUG)
+#define SOCKCHECK_DEBUG 0
+#endif
+#define SOCKCHECK_TEST_DB "sockcheck.conf"
+
+enum sockcheck_decision {
+    CHECKING,
+    ACCEPT,
+    REJECT
+};
+
+typedef struct {
+    enum sockcheck_decision decision;
+    time_t last_touched;
+    const char *reason;
+    struct in_addr addr;
+    char hostname[16];
+} *sockcheck_cache_info;
+
+DECLARE_LIST(sci_list, sockcheck_cache_info);
+DEFINE_LIST(sci_list, sockcheck_cache_info)
+
+/* Here's the list of hosts that need to be started on.
+ */
+static struct sci_list pending_sci_list;
+
+/* Map of previously checked IPs state (if we've accepted the address yet).
+ * The data for each entry is a pointer to a sockcheck_cache_info.
+ */
+static dict_t checked_ip_dict;
+
+/* Each sockcheck template is formed as a Mealy state machine (that is,
+ * the output on a state transition is a function of both the current
+ * state and the input).  Mealy state machines require fewer states to
+ * match the same input than Moore machines (where the output is only
+ * a function of the current state).
+ * 
+ * A state is characterized by sending some data (possibly nothing),
+ * waiting a certain amount of time to receive one of zero or more
+ * responses, and a decision (accept, reject, continue to another
+ * state) based on the received response.
+ */
+
+struct sockcheck_response {
+    const char *template;
+    struct sockcheck_state *next;
+};
+
+DECLARE_LIST(response_list, struct sockcheck_response *);
+DEFINE_LIST(response_list, struct sockcheck_response *)
+static unsigned int max_responses;
+
+struct sockcheck_state {
+    unsigned short port;
+    unsigned short timeout;
+    unsigned short reps;
+    enum sockcheck_decision type;
+    const char *template;
+    struct response_list responses;
+};
+
+struct sockcheck_list {
+    unsigned int size, used, refs;
+    struct sockcheck_state **list;
+};
+
+/*
+ * List of tests.
+ */
+static struct sockcheck_list *tests;
+
+/* Stuff to track client state, one instance per open connection. */
+struct sockcheck_client {
+    struct io_fd *fd;
+    struct sockcheck_list *tests;
+    sockcheck_cache_info addr;
+    unsigned int client_index;
+    unsigned int test_index;
+    unsigned short test_rep;
+    struct sockcheck_state *state;
+    unsigned int read_size, read_used, read_pos;
+    char *read;
+    const char **resp_state;
+};
+
+static struct {
+    unsigned int max_clients;
+    unsigned int max_read;
+    unsigned int gline_duration;
+    unsigned int max_cache_age;
+    struct sockaddr_in *local_addr;
+    int local_addr_len;
+} sockcheck_conf;
+
+static unsigned int sockcheck_num_clients;
+static struct sockcheck_client **client_list;
+static unsigned int proxies_detected, checked_ip_count;
+static struct module *sockcheck_module;
+static struct log_type *PC_LOG;
+const char *sockcheck_module_deps[] = { NULL };
+
+static const struct message_entry msgtab[] = {
+    { "PCMSG_PROXY_DEFINITION_FAILED", "Proxy definition failed: %s" },
+    { "PCMSG_PROXY_DEFINITION_SUCCEEDED", "New proxy type defined." },
+    { "PCMSG_UNSCANNABLE_IP", "%s has a spoofed, hidden or localnet IP." },
+    { "PCMSG_ADDRESS_QUEUED", "$b%s$b is now queued to be proxy-checked." },
+    { "PCMSG_ADDRESS_UNRESOLVED", "Unable to resolve $b%s$b to an IP address." },
+    { "PCMSG_CHECKING_ADDRESS", "$b%s$b is currently being checked; unable to clear it." },
+    { "PCMSG_NOT_REMOVED_FROM_CACHE", "$b%s$b was not cached and therefore was not cleared." },
+    { "PCMSG_REMOVED_FROM_CACHE", "$b%s$b was cleared from the cached hosts list." },
+    { "PCMSG_DISABLED", "Proxy scanning is $bdisabled$b." },
+    { "PCMSG_NOT_CACHED", "No proxycheck records exist for IP %s." },
+    { "PCMSG_STATUS_CHECKING", "IP %s proxycheck state: last touched %s ago, still checking" },
+    { "PCMSG_STATUS_ACCEPTED", "IP %s proxycheck state: last touched %s ago, acepted" },
+    { "PCMSG_STATUS_REJECTED", "IP %s proxycheck state: last touched %s ago, rejected: %s" },
+    { "PCMSG_STATUS_UNKNOWN", "IP %s proxycheck state: last touched %s ago, invalid status" },
+    { "PCMSG_STATISTICS", "Since booting, I have checked %d clients for illicit proxies, and detected %d proxy hosts.\nI am currently checking %d clients (out of %d max) and have a backlog of %d more to start on.\nI currently have %d hosts cached.\nI know how to detect %d kinds of proxies." },
+    { NULL, NULL }
+};
+
+static struct sockcheck_list *
+sockcheck_list_alloc(unsigned int size)
+{
+    struct sockcheck_list *list = malloc(sizeof(*list));
+    list->used = 0;
+    list->refs = 1;
+    list->size = size;
+    list->list = malloc(list->size*sizeof(list->list[0]));
+    return list;
+}
+
+static void
+sockcheck_list_append(struct sockcheck_list *list, struct sockcheck_state *new_item)
+{
+    if (list->used == list->size) {
+       list->size <<= 1;
+       list->list = realloc(list->list, list->size*sizeof(list->list[0]));
+    }
+    list->list[list->used++] = new_item;
+}
+
+static struct sockcheck_list *
+sockcheck_list_clone(struct sockcheck_list *old_list)
+{
+    struct sockcheck_list *new_list = malloc(sizeof(*new_list));
+    new_list->used = old_list->used;
+    new_list->refs = 1;
+    new_list->size = old_list->size;
+    new_list->list = malloc(new_list->size*sizeof(new_list->list[0]));
+    memcpy(new_list->list, old_list->list, new_list->used*sizeof(new_list->list[0]));
+    return new_list;
+}
+
+static void
+sockcheck_list_unref(struct sockcheck_list *list)
+{
+    if (!list || --list->refs > 0) return;
+    free(list->list);
+    free(list);
+}
+
+static void
+sockcheck_issue_gline(sockcheck_cache_info sci)
+{
+    char *target = alloca(3+strlen(sci->hostname));
+    strcpy(target, "*@");
+    strcpy(target+2, sci->hostname);
+    log_module(PC_LOG, LOG_INFO, "Issuing gline for client at IP %s hostname %s: %s", inet_ntoa(sci->addr), sci->hostname, sci->reason);
+    gline_add("ProxyCheck", target, sockcheck_conf.gline_duration, sci->reason, now, 1);
+}
+
+static struct sockcheck_client *
+sockcheck_alloc_client(sockcheck_cache_info sci)
+{
+    struct sockcheck_client *client;
+    client = calloc(1, sizeof(*client));
+    client->tests = tests;
+    client->tests->refs++;
+    client->addr = sci;
+    client->read_size = sockcheck_conf.max_read;
+    client->read = malloc(client->read_size);
+    client->resp_state = malloc(max_responses * sizeof(client->resp_state[0]));
+    return client;
+}
+
+static void
+sockcheck_free_client(struct sockcheck_client *client)
+{
+    if (SOCKCHECK_DEBUG) {
+        log_module(PC_LOG, LOG_INFO, "Goodbye %s (%p)!  I set you free!", client->addr->hostname, client);
+    }
+    if (client->fd) ioset_close(client->fd->fd, 1);
+    sockcheck_list_unref(client->tests);
+    free(client->read);
+    free(client->resp_state);
+    free(client);
+}
+
+static void sockcheck_start_client(unsigned int idx);
+static void sockcheck_begin_test(struct sockcheck_client *client);
+static void sockcheck_advance(struct sockcheck_client *client, unsigned int next_state);
+
+static void
+sockcheck_timeout_client(void *data)
+{
+    struct sockcheck_client *client = data;
+    if (SOCKCHECK_DEBUG) {
+        log_module(PC_LOG, LOG_INFO, "Client %s timed out.", client->addr->hostname);
+    }
+    sockcheck_advance(client, client->state->responses.used-1);
+}
+
+static void
+sockcheck_print_client(const struct sockcheck_client *client)
+{
+    static const char *decs[] = {"CHECKING", "ACCEPT", "REJECT"};
+    log_module(PC_LOG, LOG_INFO, "client %p: { addr = %p { decision = %s; last_touched = "FMT_TIME_T"; reason = %s; hostname = \"%s\" }; "
+        "test_index = %d; state = %p { port = %d; type = %s; template = \"%s\"; ... }; "
+        "fd = %p(%d); read = %p; read_size = %d; read_used = %d; read_pos = %d; }",
+        client, client->addr, decs[client->addr->decision], client->addr->last_touched, client->addr->reason, client->addr->hostname,
+        client->test_index, client->state,
+        (client->state ? client->state->port : 0),
+        (client->state ? decs[client->state->type] : "N/A"),
+        (client->state ? client->state->template : "N/A"),
+        client->fd, (client->fd ? client->fd->fd : 0),
+        client->read, client->read_size, client->read_used, client->read_pos);
+}
+
+static char hexvals[256] = {
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0 */
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16 */
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 32 */
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, /* 48 */
+    0,10,11,12,13,14,15, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 64 */
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 80 */
+    0,10,11,12,13,14,15, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 96 */
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0  /* 112 */
+};
+
+static void
+expand_var(const struct sockcheck_client *client, char var, char **p_expansion, unsigned int *p_exp_length)
+{
+    extern struct cManagerNode cManager;
+    const char *expansion;
+    unsigned int exp_length;
+#ifndef __SOLARIS__
+    u_int32_t exp4;
+    u_int16_t exp2;
+#else
+    uint32_t exp4;
+    uint16_t exp2;
+#endif
+    /* expand variable */
+    switch (var) {
+    case 'c':
+        expansion = client->addr->hostname;
+        exp_length = strlen(expansion);
+        break;
+    case 'i':
+       exp4 = client->addr->addr.s_addr;
+       exp_length = sizeof(exp4);
+       expansion = (char*)&exp4;
+       break;
+    case 'p':
+       exp2 = htons(client->state->port);
+       exp_length = sizeof(exp2);
+       expansion = (char*)&exp2;
+       break;
+    case 'u':
+       expansion = cManager.uplink->host;
+       exp_length = strlen(expansion);
+       break;
+    default:
+        log_module(PC_LOG, LOG_WARNING, "Request to expand unknown sockcheck variable $%c, using empty expansion.", var);
+       expansion = "";
+       exp_length = 0;
+    }
+    if (p_expansion) {
+       *p_expansion = malloc(exp_length);
+       memcpy(*p_expansion, expansion, exp_length);
+    }
+    if (p_exp_length) {
+       *p_exp_length = exp_length;
+    }
+}
+
+static int
+sockcheck_check_template(const char *template, int is_input)
+{
+    unsigned int nn;
+    if (is_input && !strcmp(template, "other")) return 1;
+    for (nn=0; template[nn]; nn += 2) {
+        switch (template[nn]) {
+        case '=':
+            if (!template[nn+1]) {
+                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s had = at end of template; needs a second character.", template);
+                return 0;
+            }
+            break;
+        case '$':
+            switch (template[nn+1]) {
+            case 'c': case 'i': case 'p': case 'u': break;
+            default:
+                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s refers to unknown variable %c (pos %d).", template, template[nn+1], nn);
+                return 0;
+            }
+            break;
+        case '.':
+            if (!is_input) {
+                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s: . is only valid in input templates.", template);
+                return 0;
+            }
+            if (template[nn+1] != '.') {
+                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s expects .. to come in twos (pos %d).", template, nn);
+                return 0;
+            }
+            break;
+        case '0': case '1': case '2': case '3': case '4':
+        case '5': case '6': case '7': case '8': case '9':
+        case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+        case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+            if (!hexvals[(unsigned char)template[nn+1]] && (template[nn+1] != '0')) {
+                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s expects hex characters to come in twos (pos %d).", template, nn);
+                return 0;
+            }
+            break;
+        default:
+            log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s: unrecognized character '%c' (pos %d).", template, template[nn], nn);
+            return 0;
+        }
+    }
+    return 1;
+}
+
+static void
+sockcheck_elaborate_state(struct sockcheck_client *client)
+{
+    const char *template;
+    unsigned int nn;
+
+    for (template = client->state->template, nn = 0; template[nn]; nn += 2) {
+        switch (template[nn]) {
+        case '=': ioset_write(client->fd, template+nn+1, 1); break;
+        case '0': case '1': case '2': case '3': case '4':
+        case '5': case '6': case '7': case '8': case '9':
+        case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+        case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': {
+            char ch = hexvals[(unsigned char)template[nn]] << 4
+                | hexvals[(unsigned char)template[nn+1]];
+            ioset_write(client->fd, &ch, 1);
+            break;
+        }
+        case '$': {
+            char *expansion;
+            unsigned int exp_length;
+            expand_var(client, template[nn+1], &expansion, &exp_length);
+            ioset_write(client->fd, expansion, exp_length);
+            free(expansion);
+            break;
+        }
+        }
+    }
+    for (nn=0; nn<client->state->responses.used; nn++) {
+        /* Set their resp_state to the start of the response. */
+       client->resp_state[nn] = client->state->responses.list[nn]->template;
+        /* If it doesn't require reading, take it now. */
+        if (client->resp_state[nn] && !*client->resp_state[nn]) {
+            if (SOCKCHECK_DEBUG) {
+                log_module(PC_LOG, LOG_INFO, "Skipping straight to easy option %d for %p.", nn, client);
+            }
+            sockcheck_advance(client, nn);
+            return;
+        }
+    }
+    timeq_add(now + client->state->timeout, sockcheck_timeout_client, client);
+    if (SOCKCHECK_DEBUG) {
+        log_module(PC_LOG, LOG_INFO, "Elaborated state for %s:", client->addr->hostname);
+        sockcheck_print_client(client);
+    }
+}
+
+static void
+sockcheck_decide(struct sockcheck_client *client, enum sockcheck_decision decision)
+{
+    unsigned int n;
+
+    checked_ip_count++;
+    client->addr->decision = decision;
+    client->addr->last_touched = now;
+    switch (decision) {
+    case ACCEPT:
+       /* do nothing */
+        if (SOCKCHECK_DEBUG) {
+            log_module(PC_LOG, LOG_INFO, "Proxy check passed for client at IP %s hostname %s.", inet_ntoa(client->addr->addr), client->addr->hostname);
+        }
+        break;
+    case REJECT:
+       client->addr->reason = client->state->template;
+       proxies_detected++;
+       sockcheck_issue_gline(client->addr);
+        if (SOCKCHECK_DEBUG) {
+            log_module(PC_LOG, LOG_INFO, "Proxy check rejects client at IP %s hostname %s (%s)", inet_ntoa(client->addr->addr), client->addr->hostname, client->addr->reason);
+        }
+       /* Don't compare test_index != 0 directly, because somebody
+        * else may have reordered the tests already. */
+       if (client->tests->list[client->test_index] != tests->list[0]) {
+           struct sockcheck_list *new_tests = sockcheck_list_clone(tests);
+           struct sockcheck_state *new_first = client->tests->list[client->test_index];
+            for (n=0; (n<tests->used) && (tests->list[n] != new_first); n++) ;
+            for (; n>0; n--) new_tests->list[n] = new_tests->list[n-1];
+           new_tests->list[0] = new_first;
+           sockcheck_list_unref(tests);
+           tests = new_tests;
+       }
+        break;
+    default:
+       log_module(PC_LOG, LOG_ERROR, "BUG: sockcheck_decide(\"%s\", %d): unrecognized decision.", client->addr->hostname, decision);
+    }
+    n = client->client_index;
+    sockcheck_free_client(client);
+    if ((--sockcheck_num_clients < sockcheck_conf.max_clients)
+        && (pending_sci_list.used > 0)) {
+        sockcheck_start_client(n);
+    } else {
+        client_list[n] = 0;
+    }
+}
+
+static void
+sockcheck_advance(struct sockcheck_client *client, unsigned int next_state)
+{
+    struct sockcheck_state *ns;
+
+    timeq_del(0, sockcheck_timeout_client, client, TIMEQ_IGNORE_WHEN);
+    if (SOCKCHECK_DEBUG) {
+        unsigned int n, m;
+        char buffer[201];
+        static const char *hexmap = "0123456789ABCDEF";
+       log_module(PC_LOG, LOG_INFO, "sockcheck_advance(%s) following response %d (type %d) of %d.", client->addr->hostname, next_state, client->state->responses.list[next_state]->next->type, client->state->responses.used);
+       for (n=0; n<client->read_used; n++) {
+           for (m=0; (m<(sizeof(buffer)-1)>>1) && ((n+m) < client->read_used); m++) {
+               buffer[m << 1] = hexmap[client->read[n+m] >> 4];
+               buffer[m << 1 | 1] = hexmap[client->read[n+m] & 15];
+           }
+           buffer[m<<1] = 0;
+           log_module(PC_LOG, LOG_INFO, " .. read data: %s", buffer);
+           n += m;
+       }
+        sockcheck_print_client(client);
+    }
+
+    ns = client->state = client->state->responses.list[next_state]->next;
+    switch (ns->type) {
+    case CHECKING:
+        sockcheck_elaborate_state(client);
+        break;
+    case REJECT:
+        sockcheck_decide(client, REJECT);
+        break;
+    case ACCEPT:
+        if (++client->test_rep < client->tests->list[client->test_index]->reps) {
+            sockcheck_begin_test(client);
+        } else if (++client->test_index < client->tests->used) {
+            client->test_rep = 0;
+            sockcheck_begin_test(client);
+        } else {
+            sockcheck_decide(client, ACCEPT);
+        }
+        break;
+    default:
+        log_module(PC_LOG, LOG_ERROR, "BUG: unknown next-state type %d (after %p).", ns->type, client->state);
+        break;
+    }
+}
+
+static void
+sockcheck_readable(struct io_fd *fd)
+{
+    /* read what we can from the fd */
+    struct sockcheck_client *client = fd->data;
+    unsigned int nn;
+    int res;
+
+    res = read(fd->fd, client->read + client->read_used, client->read_size - client->read_used);
+    if (res < 0) {
+        switch (res = errno) {
+        default:
+           log_module(PC_LOG, LOG_ERROR, "BUG: sockcheck_readable(%d/%s): read() returned errno %d (%s)", fd->fd, client->addr->hostname, errno, strerror(errno));
+        case EAGAIN:
+            return;
+        case ECONNRESET:
+            sockcheck_advance(client, client->state->responses.used - 1);
+            return;
+       }
+    } else if (res == 0) {
+        sockcheck_advance(client, client->state->responses.used - 1);
+        return;
+    } else {
+       client->read_used += res;
+    }
+    if (SOCKCHECK_DEBUG) {
+        unsigned int n, m;
+        char buffer[201];
+        static const char *hexmap = "0123456789ABCDEF";
+       for (n=0; n<client->read_used; n++) {
+           for (m=0; (m<(sizeof(buffer)-1)>>1) && ((n+m) < client->read_used); m++) {
+               buffer[m << 1] = hexmap[client->read[n+m] >> 4];
+               buffer[m << 1 | 1] = hexmap[client->read[n+m] & 15];
+           }
+           buffer[m<<1] = 0;
+           log_module(PC_LOG, LOG_INFO, "read %d bytes data: %s", client->read_used, buffer);
+           n += m;
+       }
+    }
+
+    /* See if what's been read matches any of the expected responses */
+    while (client->read_pos < client->read_used) {
+        unsigned int last_pos = client->read_pos;
+       char bleh;
+       const char *resp_state;
+
+       for (nn=0; nn<(client->state->responses.used-1); nn++) {
+            char *expected;
+            unsigned int exp_length = 1, free_exp = 0;
+           /* compare against possible target */
+           resp_state = client->resp_state[nn];
+           if (resp_state == NULL) continue;
+           switch (*resp_state) {
+           case '=': 
+                bleh = resp_state[1];
+                expected = &bleh;
+                break;
+           case '.':
+                /* any character passes */
+                client->read_pos++;
+                exp_length = 0;
+                break;
+           case '0': case '1': case '2': case '3': case '4':
+           case '5': case '6': case '7': case '8': case '9':
+           case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+           case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+               bleh = hexvals[(unsigned char)resp_state[0]] << 4
+                    | hexvals[(unsigned char)resp_state[1]];
+                expected = &bleh;
+               break;
+           case '$':
+               expand_var(client, resp_state[1], &expected, &exp_length);
+                free_exp = 1;
+                break;
+            }
+            if (client->read_pos+exp_length <= client->read_used) {
+                if (exp_length && memcmp(client->read+client->read_pos, expected, exp_length)) {
+                    resp_state = NULL;
+                } else {
+                    client->read_pos += exp_length;
+                }
+            } else {
+                /* can't check the variable yet, so come back later */
+                resp_state -= 2;
+            }
+            if (free_exp) free(expected);
+           if (resp_state) {
+               client->resp_state[nn] = resp_state = resp_state + 2;
+               if (!*resp_state) {
+                    sockcheck_advance(client, nn);
+                   return;
+               }
+           } else {
+               client->resp_state[nn] = NULL;
+           }
+       }
+        if (last_pos == client->read_pos) break;
+    }
+
+    /* nothing seemed to match.  what now? */
+    if (client->read_used >= client->read_size) {
+       /* we got more data than we expected to get .. don't read any more */
+        if (SOCKCHECK_DEBUG) {
+            log_module(PC_LOG, LOG_INFO, "Buffer filled (unmatched) for client %s", client->addr->hostname);
+        }
+        sockcheck_advance(client, client->state->responses.used-1);
+       return;
+    }
+}
+
+static void
+sockcheck_connected(struct io_fd *fd, int rc)
+{
+    struct sockcheck_client *client = fd->data;
+    client->fd = fd;
+    switch (rc) {
+    default:
+        log_module(PC_LOG, LOG_ERROR, "BUG: connect() got error %d (%s) for client at %s.", rc, strerror(rc), client->addr->hostname);
+    case EHOSTUNREACH:
+    case ECONNREFUSED:
+    case ETIMEDOUT:
+        if (SOCKCHECK_DEBUG) {
+            log_module(PC_LOG, LOG_INFO, "Client %s gave us errno %d (%s)", client->addr->hostname, rc, strerror(rc));
+        }
+        sockcheck_advance(client, client->state->responses.used-1);
+        return;
+    case 0: break;
+    }
+    fd->wants_reads = 1;
+    if (SOCKCHECK_DEBUG) {
+        log_module(PC_LOG, LOG_INFO, "Connected: to %s port %d.", client->addr->hostname, client->state->port);
+    }
+    sockcheck_elaborate_state(client);
+}
+
+static void
+sockcheck_begin_test(struct sockcheck_client *client)
+{
+    struct io_fd *io_fd;
+
+    if (client->fd) {
+        ioset_close(client->fd->fd, 1);
+        client->fd = NULL;
+    }
+    do {
+        client->state = client->tests->list[client->test_index];
+        client->read_pos = 0;
+        client->read_used = 0;
+        client->fd = io_fd = ioset_connect((struct sockaddr*)sockcheck_conf.local_addr, sizeof(struct sockaddr), client->addr->hostname, client->state->port, 0, client, sockcheck_connected);
+        if (!io_fd) {
+            client->test_index++;
+            continue;
+        }
+        io_fd->readable_cb = sockcheck_readable;
+        timeq_add(now + client->state->timeout, sockcheck_timeout_client, client);
+        if (SOCKCHECK_DEBUG) {
+            log_module(PC_LOG, LOG_INFO, "Starting proxy check on %s:%d (test %d) with fd %d (%p).", client->addr->hostname, client->state->port, client->test_index, io_fd->fd, io_fd);
+        }
+        return;
+    } while (client->test_index < client->tests->used);
+    /* Ran out of tests to run; accept this client. */
+    sockcheck_decide(client, ACCEPT);
+}
+
+static void
+sockcheck_start_client(unsigned int idx)
+{
+    sockcheck_cache_info sci;
+    struct sockcheck_client *client;
+
+    if (pending_sci_list.used == 0) return;
+    if (!(sci = pending_sci_list.list[0])) {
+        log_module(PC_LOG, LOG_ERROR, "BUG: sockcheck_start_client(%d) found null pointer in pending_sci_list.", idx);
+        return;
+    }
+    memmove(pending_sci_list.list, pending_sci_list.list+1,
+           (--pending_sci_list.used)*sizeof(pending_sci_list.list[0]));
+    sockcheck_num_clients++;
+    if (!tests) return;
+    client = client_list[idx] = sockcheck_alloc_client(sci);
+    log_module(PC_LOG, LOG_INFO, "Proxy-checking client at %s (%s) as client %d (%p) of %d.", inet_ntoa(sci->addr), sci->hostname, idx, client, sockcheck_num_clients);
+    client->test_rep = 0;
+    client->client_index = idx;
+    sockcheck_begin_test(client);
+}
+
+void
+sockcheck_queue_address(struct in_addr addr)
+{
+    sockcheck_cache_info sci;
+    char *ipstr=inet_ntoa(addr);
+
+    sci = dict_find(checked_ip_dict, ipstr, NULL);
+    if (sci) {
+        switch (sci->decision) {
+        case CHECKING:
+            /* We are already checking this host. */
+            return;
+        case ACCEPT:
+            if ((sci->last_touched + sockcheck_conf.max_cache_age) >= (unsigned)now) return;
+            break;
+        case REJECT:
+            if ((sci->last_touched + sockcheck_conf.gline_duration) >= (unsigned)now) {
+                sockcheck_issue_gline(sci);
+                return;
+            }
+            break;
+        }
+        dict_remove(checked_ip_dict, sci->hostname);
+    }
+    sci = calloc(1, sizeof(*sci));
+    sci->decision = CHECKING;
+    sci->last_touched = now;
+    sci->reason = NULL;
+    sci->addr = addr;
+    strncpy(sci->hostname, ipstr, sizeof(sci->hostname));
+    dict_insert(checked_ip_dict, sci->hostname, sci);
+    sci_list_append(&pending_sci_list, sci);
+    if (sockcheck_num_clients < sockcheck_conf.max_clients)
+        sockcheck_start_client(sockcheck_num_clients);
+}
+
+int
+sockcheck_uncache_host(const char *name)
+{
+    sockcheck_cache_info sci;
+    if ((sci = dict_find(checked_ip_dict, name, NULL))
+        && (sci->decision == CHECKING)) {
+        return -1;
+    }
+    return dict_remove(checked_ip_dict, name);
+}
+
+static int
+sockcheck_create_response(const char *key, void *data, void *extra)
+{
+    const char *str, *end;
+    struct record_data *rd = data;
+    struct sockcheck_state *parent = extra;
+    struct sockcheck_response *resp;
+    dict_t resps;
+    char *templ;
+
+    /* allocate memory and tack it onto parent->responses */
+    resp = malloc(sizeof(*resp));
+    for (end = key; *end != ':' && *end != 0; end += 2 && end) ;
+    templ = malloc(end - key + 1);
+    memcpy(templ, key, end - key);
+    templ[end - key] = 0;
+    resp->template = templ;
+    if (!sockcheck_check_template(resp->template, 1)) _exit(1);
+    resp->next = malloc(sizeof(*resp->next));
+    resp->next->port = parent->port;
+    response_list_append(&parent->responses, resp);
+    /* now figure out how to create resp->next */
+    if ((str = GET_RECORD_QSTRING(rd))) {
+       if (!ircncasecmp(str, "reject", 6)) {
+           resp->next->type = REJECT;
+       } else if (!ircncasecmp(str, "accept", 6)) {
+           resp->next->type = ACCEPT;
+       } else {
+           log_module(PC_LOG, LOG_ERROR, "Error: unknown sockcheck decision `%s', defaulting to accept.", str);
+           resp->next->type = ACCEPT;
+       }
+       if (str[6]) {
+           resp->next->template = strdup(str+7);
+       } else {
+           resp->next->template = strdup("No explanation given");
+       }
+    } else if ((resps = GET_RECORD_OBJECT(rd))) {
+       resp->next->type = CHECKING;
+       response_list_init(&resp->next->responses);
+       if (*end == ':') {
+           resp->next->template = strdup(end+1);
+            if (!sockcheck_check_template(resp->next->template, 0)) _exit(1);
+       } else {
+           resp->next->template = strdup("");
+       }
+       dict_foreach(resps, sockcheck_create_response, resp->next);
+    }
+    return 0;
+}
+
+/* key: PORT:send-pattern, as in keys of sockcheck.conf.example
+ * data: recdb record_data containing response
+ * extra: struct sockcheck_list* to append test to
+ */
+static int
+sockcheck_create_test(const char *key, void *data, void *extra)
+{
+    char *end;
+    struct record_data *rd;
+    dict_t object;
+    struct sockcheck_state *new_test;
+    unsigned int n;
+
+    rd = data;
+    new_test = malloc(sizeof(*new_test));
+    new_test->template = NULL;
+    new_test->reps = 1;
+    new_test->port = strtoul(key, &end, 0);
+    new_test->timeout = 5;
+    new_test->type = CHECKING;
+    response_list_init(&new_test->responses);
+    if (!(object = GET_RECORD_OBJECT(rd))) {
+       log_module(PC_LOG, LOG_ERROR, "Error: misformed sockcheck test `%s', skipping it.", key);
+        free(new_test);
+        return 1;
+    }
+    while (*end) {
+        switch (*end) {
+        case '@': new_test->timeout = strtoul(end+1, &end, 0); break;
+        case '*': new_test->reps = strtoul(end+1, &end, 0); break;
+        case ':':
+            new_test->template = strdup(end+1);
+            end += strlen(end);
+            if (!sockcheck_check_template(new_test->template, 0)) _exit(1);
+            break;
+        default:
+            log_module(PC_LOG, LOG_ERROR, "Error: misformed sockcheck test `%s', skipping it.", key);
+            free(new_test);
+            return 1;
+        }
+    }
+    if (!new_test->template) {
+       log_module(PC_LOG, LOG_ERROR, "Error: misformed sockcheck test `%s', skipping it.", key);
+       free(new_test);
+       return 1;
+    }
+    dict_foreach(object, sockcheck_create_response, new_test);
+    /* If none of the responses have template "other", create a
+     * default response that goes to accept. */
+    for (n=0; n<new_test->responses.used; n++) {
+       if (!strcmp(new_test->responses.list[n]->template, "other")) break;
+    }
+    if (n == new_test->responses.used) {
+       rd = alloc_record_data_qstring("accept");
+       sockcheck_create_response("other", rd, new_test);
+       free_record_data(rd);
+    } else if (n != (new_test->responses.used - 1)) {
+       struct sockcheck_response *tmp;
+       /* switch the response for "other" to the end */
+       tmp = new_test->responses.list[new_test->responses.used - 1];
+       new_test->responses.list[new_test->responses.used - 1] = new_test->responses.list[n];
+       new_test->responses.list[n] = tmp;
+    }
+    if (new_test->responses.used > max_responses) {
+       max_responses = new_test->responses.used;
+    }
+    sockcheck_list_append(extra, new_test);
+    return 0;
+}
+
+static void
+sockcheck_read_tests(void)
+{
+    dict_t test_db;
+    struct sockcheck_list *new_tests;
+    test_db = parse_database(SOCKCHECK_TEST_DB);
+    if (!test_db)
+       return;
+    if (dict_size(test_db) > 0) {
+       new_tests = sockcheck_list_alloc(dict_size(test_db));
+       dict_foreach(test_db, sockcheck_create_test, new_tests);
+       if (tests) sockcheck_list_unref(tests);
+       tests = new_tests;
+    } else {
+       log_module(PC_LOG, LOG_ERROR, "%s was empty - disabling sockcheck.", SOCKCHECK_TEST_DB);
+    }
+    free_database(test_db);
+}
+
+void
+sockcheck_free_state(struct sockcheck_state *state)
+{
+    unsigned int n;
+    if (state->type == CHECKING) {
+       for (n=0; n<state->responses.used; n++) {
+           free((char*)state->responses.list[n]->template);
+           sockcheck_free_state(state->responses.list[n]->next);
+           free(state->responses.list[n]);
+       }
+       response_list_clean(&state->responses);
+    }
+    free((char*)state->template);
+    free(state);
+}
+
+const char *
+sockcheck_add_test(const char *desc)
+{
+    struct sockcheck_list *new_tests;
+    const char *reason;
+    char *name;
+    struct record_data *rd;
+
+    if ((reason = parse_record(desc, &name, &rd)))
+        return reason;
+    new_tests = sockcheck_list_clone(tests);
+    if (sockcheck_create_test(name, rd, new_tests)) {
+       sockcheck_list_unref(new_tests);
+       return "Sockcheck test parse error";
+    }
+    sockcheck_list_unref(tests);
+    tests = new_tests;
+    return 0;
+}
+
+static void
+sockcheck_shutdown(void)
+{
+    unsigned int n;
+
+    if (client_list) {
+        for (n=0; n<sockcheck_conf.max_clients; n++) {
+            if (client_list[n])
+                sockcheck_free_client(client_list[n]);
+        }
+        free(client_list);
+    }
+    sockcheck_num_clients = 0;
+    dict_delete(checked_ip_dict);
+    sci_list_clean(&pending_sci_list);
+    if (tests)
+       for (n=0; n<tests->used; n++)
+           sockcheck_free_state(tests->list[n]);
+    sockcheck_list_unref(tests);
+    if (sockcheck_conf.local_addr) {
+       free(sockcheck_conf.local_addr);
+       sockcheck_conf.local_addr_len = 0;
+    }
+}
+
+static void
+sockcheck_clean_cache(UNUSED_ARG(void *data))
+{
+    dict_t curr_clients;
+    dict_iterator_t it, next;
+    sockcheck_cache_info sci;
+    unsigned int nn;
+    int max_age;
+
+    if (SOCKCHECK_DEBUG) {
+        struct string_buffer sb;
+        string_buffer_init(&sb);
+        /* Remember which clients we're still checking; we're not allowed to remove them. */
+        for (curr_clients = dict_new(), nn=0; nn < sockcheck_conf.max_clients; nn++) {
+            if (!client_list[nn])
+                continue;
+            dict_insert(curr_clients, client_list[nn]->addr->hostname, client_list[nn]);
+            string_buffer_append(&sb, ' ');
+            string_buffer_append_string(&sb, client_list[nn]->addr->hostname);
+        }
+        string_buffer_append(&sb, '\0');
+        log_module(PC_LOG, LOG_INFO, "Cleaning sockcheck cache at "FMT_TIME_T"; current clients: %s.", now, sb.list);
+        string_buffer_clean(&sb);
+    } else {
+        for (curr_clients = dict_new(), nn=0; nn < sockcheck_conf.max_clients; nn++) {
+            if (!client_list[nn])
+                continue;
+            dict_insert(curr_clients, client_list[nn]->addr->hostname, client_list[nn]);
+        }
+    }
+
+    for (it=dict_first(checked_ip_dict); it; it=next) {
+        next = iter_next(it);
+        sci = iter_data(it);
+        max_age = (sci->decision == REJECT) ? sockcheck_conf.gline_duration : sockcheck_conf.max_cache_age;
+        if (((sci->last_touched + max_age) < now)
+            && !dict_find(curr_clients, sci->hostname, NULL)) {
+            if (SOCKCHECK_DEBUG) {
+                log_module(PC_LOG, LOG_INFO, " .. nuking %s (last touched "FMT_TIME_T").", sci->hostname, sci->last_touched);
+            }
+            dict_remove(checked_ip_dict, sci->hostname);
+        }
+    }
+    dict_delete(curr_clients);
+    timeq_add(now+sockcheck_conf.max_cache_age, sockcheck_clean_cache, 0);
+}
+
+static MODCMD_FUNC(cmd_defproxy)
+{
+    const char *reason;
+
+    if ((reason = sockcheck_add_test(unsplit_string(argv+1, argc-1, NULL)))) {
+       reply("PCMSG_PROXY_DEFINITION_FAILED", reason);
+       return 0;
+    }
+    reply("PCMSG_PROXY_DEFINITION_SUCCEEDED");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_hostscan)
+{
+    unsigned int n;
+    unsigned long addr;
+    struct in_addr ipaddr;
+    char hnamebuf[64];
+
+    for (n=1; n<argc; n++) {
+       struct userNode *un = GetUserH(argv[n]);
+
+        if (un) {
+            if ((un->ip.s_addr == 0) || (ntohl(un->ip.s_addr) == INADDR_LOOPBACK)) {
+                reply("PCMSG_UNSCANNABLE_IP", un->nick);
+            } else {
+                strcpy(hnamebuf, inet_ntoa(un->ip));
+                sockcheck_queue_address(un->ip);
+                reply("PCMSG_ADDRESS_QUEUED", hnamebuf);
+            }
+        } else {
+            char *scanhost = argv[n];
+            if (getipbyname(scanhost, &addr)) {
+                ipaddr.s_addr = htonl(addr);
+                sockcheck_queue_address(ipaddr);
+                reply("PCMSG_ADDRESS_QUEUED", scanhost);
+            } else {
+                reply("PCMSG_ADDRESS_UNRESOLVED", scanhost);
+            }
+        }
+    }
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_clearhost)
+{
+    unsigned int n;
+    char hnamebuf[64];
+
+    for (n=1; n<argc; n++) {
+        struct userNode *un = GetUserH(argv[n]);
+        const char *scanhost;
+
+        if (un) {
+            strcpy(hnamebuf, inet_ntoa(un->ip));
+            scanhost = hnamebuf;
+        } else {
+            scanhost = argv[n];
+        }
+        switch (sockcheck_uncache_host(scanhost)) {
+        case -1:
+           reply("PCMSG_CHECKING_ADDRESS", scanhost);
+            break;
+        case 0:
+           reply("PCMSG_NOT_REMOVED_FROM_CACHE", scanhost);
+            break;
+        default:
+            reply("PCMSG_REMOVED_FROM_CACHE", scanhost);
+            break;
+        }
+    }
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_proxycheck)
+{
+    if (argc > 1) {
+        const char *hostname = argv[1];
+        char elapse_buf[INTERVALLEN];
+        const char *msg;
+
+        sockcheck_cache_info sci = dict_find(checked_ip_dict, hostname, NULL);
+        if (!sci) {
+            reply("PCMSG_NOT_CACHED", hostname);
+            return 0;
+        }
+        intervalString(elapse_buf, now - sci->last_touched);
+        switch (sci->decision) {
+        case CHECKING: msg = "PCMSG_STATUS_CHECKING"; break;
+        case ACCEPT: msg = "PCMSG_STATUS_ACCEPTED"; break;
+        case REJECT: msg = "PCMSG_STATUS_REJECTED"; break;
+        default: msg = "PCMSG_STATUS_UNKNOWN"; break;
+        }
+        reply(msg, sci->hostname, elapse_buf, sci->reason);
+        return 1;
+    } else {
+        reply("PCMSG_STATISTICS", checked_ip_count, proxies_detected, sockcheck_num_clients, sockcheck_conf.max_clients, pending_sci_list.used, dict_size(checked_ip_dict), (tests ? tests->used : 0));
+        return 1;
+    }
+}
+
+static int
+sockcheck_new_user(struct userNode *user) {
+    /* If they have a bum IP, or are bursting in, don't proxy-check or G-line them. */
+    if (user->ip.s_addr
+        && (ntohl(user->ip.s_addr) != INADDR_LOOPBACK)
+        && !user->uplink->burst)
+        sockcheck_queue_address(user->ip);
+    return 0;
+}
+
+static void
+_sockcheck_init(void)
+{
+    checked_ip_dict = dict_new();
+    dict_set_free_data(checked_ip_dict, free);
+    sci_list_init(&pending_sci_list);
+    sockcheck_num_clients = 0;
+    sockcheck_read_tests();
+    timeq_del(0, sockcheck_clean_cache, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+    client_list = calloc(sockcheck_conf.max_clients, sizeof(client_list[0]));
+    timeq_add(now+sockcheck_conf.max_cache_age, sockcheck_clean_cache, 0);
+}
+
+static void
+sockcheck_read_conf(void)
+{
+    dict_t my_node;
+    const char *str;
+
+    /* set the defaults here in case the entire record is missing */
+    sockcheck_conf.max_clients = 32;
+    sockcheck_conf.max_read = 1024;
+    sockcheck_conf.gline_duration = 3600;
+    sockcheck_conf.max_cache_age = 60;
+    if (sockcheck_conf.local_addr) {
+        free(sockcheck_conf.local_addr);
+        sockcheck_conf.local_addr = NULL;
+    }
+    /* now try to read from the conf database */
+    if ((my_node = conf_get_data("modules/sockcheck", RECDB_OBJECT))) {
+       str = database_get_data(my_node, "max_sockets", RECDB_QSTRING);
+       if (str) sockcheck_conf.max_clients = strtoul(str, NULL, 0);
+       str = database_get_data(my_node, "max_clients", RECDB_QSTRING);
+       if (str) sockcheck_conf.max_clients = strtoul(str, NULL, 0);
+       str = database_get_data(my_node, "max_read", RECDB_QSTRING);
+       if (str) sockcheck_conf.max_read = strtoul(str, NULL, 0);
+       str = database_get_data(my_node, "max_cache_age", RECDB_QSTRING);
+       if (str) sockcheck_conf.max_cache_age = ParseInterval(str);
+        str = database_get_data(my_node, "gline_duration", RECDB_QSTRING);
+        if (str) sockcheck_conf.gline_duration = ParseInterval(str);
+       str = database_get_data(my_node, "address", RECDB_QSTRING);
+       if (str) {
+           struct sockaddr_in *sin;
+           unsigned long addr;
+
+           sockcheck_conf.local_addr_len = sizeof(*sin);
+           if (getipbyname(str, &addr)) {
+               sin = malloc(sockcheck_conf.local_addr_len);
+               sin->sin_family = AF_INET;
+               sin->sin_port = 0;
+               sin->sin_addr.s_addr = addr;
+#ifdef HAVE_SIN_LEN
+               sin->sin_len = 0;
+#endif
+               memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
+               sockcheck_conf.local_addr = sin;
+           } else {
+               log_module(PC_LOG, LOG_ERROR, "Error: Unable to get host named `%s', not checking a specific address.", str);
+               sockcheck_conf.local_addr = NULL;
+           }
+       }
+    }
+}
+
+int
+sockcheck_init(void)
+{
+    PC_LOG = log_register_type("ProxyCheck", "file:proxycheck.log");
+    conf_register_reload(sockcheck_read_conf);
+    reg_exit_func(sockcheck_shutdown);
+    _sockcheck_init();
+    message_register_table(msgtab);
+
+    sockcheck_module = module_register("ProxyCheck", PC_LOG, "mod-sockcheck.help", NULL);
+    modcmd_register(sockcheck_module, "defproxy", cmd_defproxy, 2, 0, "level", "999", NULL);
+    modcmd_register(sockcheck_module, "hostscan", cmd_hostscan, 2, 0, "level", "650", NULL);
+    modcmd_register(sockcheck_module, "clearhost", cmd_clearhost, 2, 0, "level", "650", NULL);
+    modcmd_register(sockcheck_module, "stats proxycheck", cmd_stats_proxycheck, 0, 0, NULL);
+    reg_new_user_func(sockcheck_new_user);
+    return 1;
+}
+
+int
+sockcheck_finalize(void)
+{
+    return 1;
+}
diff --git a/src/mod-sockcheck.help b/src/mod-sockcheck.help
new file mode 100644 (file)
index 0000000..08b4c38
--- /dev/null
@@ -0,0 +1,10 @@
+"PROXY" ("$bPROXY COMMANDS$b",
+        "Proxy checking control commands include CLEARHOST, DEFPROXY and HOSTSCAN.");
+"CLEARHOST" ("/msg $O CLEARHOST <host|nick>",
+        "Removes any data for the given host in the proxy checker's results cache.",
+        "$uSee Also:$u hostscan");
+"DEFPROXY" ("/msg $O DEFPROXY <definition>",
+        "Inserts a proxy check definition into the proxy-check database. Extreme care should be used with this command.");
+"HOSTSCAN" ("/msg $O HOSTSCAN <host|nick>",
+        "Adds the speicified host to the proxy-check queue. If the host is found to have a running proxy, then that host is G-lined for an hour.",
+        "$uSee Also:$u trace");
diff --git a/src/modcmd.c b/src/modcmd.c
new file mode 100644 (file)
index 0000000..301bdfb
--- /dev/null
@@ -0,0 +1,2237 @@
+/* modcmd.c - Generalized module command support
+ * Copyright 2002-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "arch-version.h"
+#include "chanserv.h"
+#include "conf.h"
+#include "modcmd.h"
+#include "saxdb.h"
+
+struct pending_template {
+    struct svccmd *cmd;
+    char *base;
+    struct pending_template *next;
+};
+
+static struct dict *modules;
+static struct dict *services;
+static struct pending_template *pending_templates;
+static struct module *modcmd_module;
+static struct modcmd *bind_command, *help_command;
+static const struct message_entry msgtab[] = {
+    { "MCMSG_VERSION", "$b"PACKAGE_STRING"$b ("CODENAME"), Built: " __DATE__ ", " __TIME__"." },
+    { "MCMSG_BARE_FLAG", "Flag %.*s must be preceeded by a + or -." },
+    { "MCMSG_UNKNOWN_FLAG", "Unknown module flag %.*s." },
+    { "MCMSG_BAD_OPSERV_LEVEL", "Invalid $O access level %s." },
+    { "MCMSG_BAD_CHANSERV_LEVEL", "Invalid $C access level %s." },
+    { "MCMSG_LEVEL_TOO_LOW", "You cannot set the access requirements for %s (your level is too low.)" },
+    { "MCMSG_LEVEL_TOO_HIGH", "You cannot set the access requirements to %s (that is too high)." },
+    { "MCMSG_BAD_OPTION", "Unknown option %s." },
+    { "MCMSG_MUST_QUALIFY", "You $bMUST$b \"/msg %s@$s %s\" (not just /msg %s)." },
+    { "MCMSG_ACCOUNT_SUSPENDED", "Your account has been suspended." },
+    { "MCMSG_CHAN_NOT_REGISTERED", "%s has not been registered with $C." },
+    { "MCMSG_CHAN_SUSPENDED", "$b$C$b access to $b%s$b has been temporarily suspended (%s)." },
+    { "MCMSG_NO_CHANNEL_ACCESS", "You lack access to %s." },
+    { "MCMSG_LOW_CHANNEL_ACCESS", "You lack sufficient access in %s to use this command." },
+    { "MCMSG_REQUIRES_JOINABLE", "You must be in %s (or on its userlist) to use this command." },
+    { "MCMSG_MUST_BE_HELPING", "You must have security override (helping mode) on to use this command." },
+    { "MCMSG_MISSING_COMMAND", "You must specify a command as well as a channel." },
+    { "MCMSG_NO_CHANNEL_BEFORE", "You may not give a channel name before this command." },
+    { "MCMSG_NO_PLUS_CHANNEL", "You may not use a +channel with this command." },
+    { "MCMSG_COMMAND_ALIASES", "%s is an alias for: %s" },
+    { "MCMSG_COMMAND_BINDING", "%s is a binding of: %s" },
+    { "MCMSG_ALIAS_ERROR", "Error in alias expansion for %s; check the error log for details." },
+    { "MCMSG_INTERNAL_COMMAND", "$b%s$b is an internal command and cannot be called directly; please check command bindings." },
+    { "MCMSG_UNKNOWN_MODULE", "Unknown module %s." },
+    { "MCMSG_UNKNOWN_SERVICE", "Unknown service %s." },
+    { "MCMSG_ALREADY_BOUND", "%s already has a command bound as %s." },
+    { "MCMSG_UNKNOWN_COMMAND_2", "Unknown command name %s (relative to service %s)." },
+    { "MCMSG_COMMAND_MODIFIED", "Option $b%s$b for $b%s$b has been set." },
+    { "MCMSG_INSPECTION_REFUSED", "You do not have access to inspect command %s." },
+    { "MCMSG_CANNOT_DOUBLE_ALIAS", "You cannot bind to a complex (argument-carrying) bind." },
+    { "MCMSG_BAD_ALIAS_ARGUMENT", "Invalid alias argument $b%s$b." },
+    { "MCMSG_COMMAND_BOUND", "New command %s bound to %s." },
+    { "MCMSG_MODULE_BOUND", "Bound %d commands from %s to %s." },
+    { "MCMSG_NO_COMMAND_BOUND", "%s has nothing bound as command %s." },
+    { "MCMSG_UNBIND_PROHIBITED", "It wouldn't be very much fun to unbind the last %s command, now would it?" },
+    { "MCMSG_COMMAND_UNBOUND", "Unbound command %s from %s." },
+    { "MCMSG_HELPFILE_UNBOUND", "Since that was the last command from module %s on the service, the helpfile for %s was removed." },
+    { "MCMSG_NO_HELPFILE", "Module %s does not have a help file." },
+    { "MCMSG_HELPFILE_ERROR", "Syntax error reading %s; help contents not changed." },
+    { "MCMSG_HELPFILE_READ", "Read %s help database in "FMT_TIME_T".%03lu seconds." },
+    { "MCMSG_COMMAND_TIME", "Command $b%s$b finished in "FMT_TIME_T".%06lu seconds." },
+    { "MCMSG_NEED_OPSERV_LEVEL", "You must have $O access of at least $b%u$b." },
+    { "MCMSG_NEED_CHANSERV_LEVEL", "You must have $C access of at least $b%u$b in the channel." },
+    { "MCMSG_NEED_ACCOUNT_FLAGS", "You must have account flags $b%s$b." },
+    { "MCMSG_NEED_NOTHING", "Anyone may use the $b%s$b command." },
+    { "MCMSG_NEED_STAFF_ACCESS", "You must be network staff." },
+    { "MCMSG_NEED_STAFF_OPER", "You must be an IRC operator." },
+    { "MCMSG_NEED_STAFF_NETHELPER", "You must be a network helper." },
+    { "MCMSG_NEED_STAFF_NETHELPER_OR_OPER", "You must be a network helper or IRC operator." },
+    { "MCMSG_NEED_STAFF_SHELPER", "You must be a support helper." },
+    { "MCMSG_NEED_STAFF_SHELPER_OR_OPER", "You must be a support helper or IRC operator." },
+    { "MCMSG_NEED_STAFF_HELPER", "You must be a network or support helper." },
+    { "MCMSG_NEED_JOINABLE", "The channel must be open or you must be in the channel or on its userlist." },
+    { "MCMSG_NEED_CHANUSER_CSUSPENDABLE", "You must be on the channel's userlist, and the channel can be suspended." },
+    { "MCMSG_NEED_CHANUSER", "You must be on the channel's userlist." },
+    { "MCMSG_NEED_REGCHAN", "You must specify a channel registered with $C." },
+    { "MCMSG_NEED_CHANNEL", "You must specify a channel that exists." },
+    { "MCMSG_NEED_AUTHED", "You must be authenticated with $N." },
+    { "MCMSG_IS_TOY", "$b%s$b is a toy command." },
+    { "MCMSG_END_REQUIREMENTS", "End of requirements for $b%s$b." },
+    { "MCMSG_ALREADY_HELPING", "You already have security override enabled." },
+    { "MCMSG_ALREADY_NOT_HELPING", "You already have security override disabled." },
+    { "MCMSG_NOW_HELPING", "Security override has been enabled." },
+    { "MCMSG_NOW_NOT_HELPING", "Security override has been disabled." },
+    { "MCMSG_JOINER_CHOICES", "Subcommands of %s: %s" },
+    { "MCMSG_MODULE_INFO", "Commands exported by module $b%s$b:" },
+    { "MCMSG_SERVICE_INFO", "Commands bound to service $b%s$b:" },
+    { "MCMSG_TOYS_DISABLED", "Toys are disabled in %s." },
+    { "MCMSG_PUBLIC_DENY", "Public commands in $b%s$b are restricted." },
+    { "MCMSG_HELPFILE_SEQUENCE", "Help priority %d: %s" },
+    { "MCMSG_HELPFILE_SEQUENCE_SET", "Set helpfile priority sequence for %s." },
+    { "MCMSG_BAD_SERVICE_NICK", "$b%s$b is an invalid nickname." },
+    { "MCMSG_ALREADY_SERVICE", "$b%s$b is already a service." },
+    { "MCMSG_NEW_SERVICE", "Added new service bot $b%s$b." },
+    { "MCMSG_SERVICE_RENAMED", "Service renamed to $b%s$b." },
+    { "MCMSG_NO_TRIGGER", "$b%s$b does not have an in-channel trigger." },
+    { "MCMSG_REMOVED_TRIGGER", "Removed trigger from $b%s$b." },
+    { "MCMSG_DUPLICATE_TRIGGER", "$b%s$b already uses trigger $b%c$b." },
+    { "MCMSG_CURRENT_TRIGGER", "Trigger for $b%s$b is $b%c$b." },
+    { "MCMSG_NEW_TRIGGER", "Changed trigger for $b%s$b to $b%c$b." },
+    { "MCMSG_SERVICE_REMOVED", "Service $b%s$b has been deleted." },
+    { "MCMSG_FILE_NOT_OPENED", "Unable to open file $b%s$b for writing." },
+    { "MCMSG_MESSAGES_DUMPED", "Messages written to $b%s$b." },
+    { NULL, NULL }
+};
+struct userData *_GetChannelUser(struct chanData *channel, struct handle_info *handle, int override, int allow_suspended);
+
+#define ACTION_ALLOW     1
+#define ACTION_OVERRIDE  2
+#define ACTION_NOCHANNEL 4
+#define ACTION_STAFF     8
+
+#define RESOLVE_DEPTH    4
+
+static struct modcmd_flag {
+    const char *name;
+    unsigned int flag;
+} flags[] = {
+    { "acceptchan", MODCMD_ACCEPT_CHANNEL },
+    { "acceptpluschan", MODCMD_ACCEPT_PCHANNEL },
+    { "authed", MODCMD_REQUIRE_AUTHED },
+    { "channel", MODCMD_REQUIRE_CHANNEL },
+    { "chanuser", MODCMD_REQUIRE_CHANUSER },
+    { "disabled", MODCMD_DISABLED },
+    { "ignore_csuspend", MODCMD_IGNORE_CSUSPEND },
+    { "joinable", MODCMD_REQUIRE_JOINABLE },
+    { "keepbound", MODCMD_KEEP_BOUND },
+    { "loghostmask", MODCMD_LOG_HOSTMASK },
+    { "nolog", MODCMD_NO_LOG },
+    { "networkhelper", MODCMD_REQUIRE_NETWORK_HELPER },
+    { "never_csuspend", MODCMD_NEVER_CSUSPEND },
+    { "oper", MODCMD_REQUIRE_OPER },
+    { "qualified", MODCMD_REQUIRE_QUALIFIED },
+    { "regchan", MODCMD_REQUIRE_REGCHAN },
+    { "supporthelper", MODCMD_REQUIRE_SUPPORT_HELPER },
+    { "helping", MODCMD_REQUIRE_HELPING },
+    { "toy", MODCMD_TOY },
+    { NULL, 0 }
+};
+
+static int
+flags_bsearch(const void *a, const void *b) {
+    const char *key = a;
+    const struct modcmd_flag *flag = b;
+    return ircncasecmp(key, flag->name, strlen(flag->name));
+}
+
+static int
+flags_qsort(const void *a, const void *b) {
+    const struct modcmd_flag *fa = a, *fb = b;
+    return irccasecmp(fa->name, fb->name);
+}
+
+DEFINE_LIST(svccmd_list, struct svccmd*);
+DEFINE_LIST(module_list, struct module*);
+
+static void
+free_service_command(void *data) {
+    struct svccmd *svccmd;
+    unsigned int nn;
+
+    svccmd = data;
+    if (svccmd->alias.used) {
+        for (nn=0; nn<svccmd->alias.used; ++nn)
+            free(svccmd->alias.list[nn]);
+        free(svccmd->alias.list);
+    }
+    free(svccmd->name);
+    free(svccmd);
+}
+
+static void
+free_service(void *data) {
+    struct service *service = data;
+    dict_delete(service->commands);
+    module_list_clean(&service->modules);
+    free(service);
+}
+
+static void
+free_module_command(void *data) {
+    struct modcmd *modcmd = data;
+    free_service_command(modcmd->defaults);
+    free(modcmd->name);
+    free(modcmd);
+}
+
+static void
+free_module(void *data) {
+    struct module *module = data;
+    dict_delete(module->commands);
+    close_helpfile(module->helpfile);
+    free(module->name);
+    free(module);
+}
+
+struct module *
+module_register(const char *name, struct log_type *clog, const char *helpfile_name, expand_func_t expand_help) {
+    struct module *newmod;
+
+    newmod = calloc(1, sizeof(*newmod));
+    newmod->name = strdup(name);
+    newmod->commands = dict_new();
+    dict_set_free_data(newmod->commands, free_module_command);
+    newmod->clog = clog;
+    newmod->helpfile_name = helpfile_name;
+    newmod->expand_help = expand_help;
+    if (newmod->helpfile_name) {
+        newmod->helpfile = open_helpfile(newmod->helpfile_name, newmod->expand_help);
+    }
+    dict_insert(modules, newmod->name, newmod);
+    return newmod;
+}
+
+struct module *
+module_find(const char *name) {
+    return dict_find(modules, name, NULL);
+}
+
+static void
+add_pending_template(struct svccmd *cmd, const char *target) {
+    struct pending_template *pending = calloc(1, sizeof(*pending));
+    pending->cmd = cmd;
+    pending->base = strdup(target);
+    pending->next = pending_templates;
+    pending_templates = pending;
+}
+
+static struct svccmd *
+svccmd_resolve_name(struct svccmd *origin, const char *name) {
+    char *sep, svcname[MAXLEN];
+
+    if ((sep = strchr(name, '.'))) {
+        memcpy(svcname, name, sep-name);
+        svcname[sep-name] = 0;
+        name = sep + 1;
+        if (svcname[0] == '*') {
+            struct module *module = module_find(svcname+1);
+            struct modcmd *cmd = module ? dict_find(module->commands, name, NULL) : NULL;
+            return cmd ? cmd->defaults : NULL;
+        } else {
+            struct service *service = service_find(svcname);
+            return service ? dict_find(service->commands, name, NULL) : NULL;
+        }
+    } else {
+        if (origin->parent) {
+            return dict_find(origin->parent->commands, name, NULL);
+        } else {
+            struct modcmd *cmd = dict_find(origin->command->parent->commands, name, NULL);
+            return cmd ? cmd->defaults : NULL;
+        }
+    }
+}
+
+static void
+modcmd_set_effective_flags(struct svccmd *cmd) {
+    int flags = cmd->flags | cmd->command->flags;
+    if (cmd->min_opserv_level > 0)
+        flags |= MODCMD_REQUIRE_OPER;
+    if (cmd->min_channel_access > 0)
+        flags |= MODCMD_REQUIRE_CHANUSER;
+    if (flags & MODCMD_REQUIRE_CHANUSER)
+        flags |= MODCMD_REQUIRE_REGCHAN;
+    if (flags & MODCMD_REQUIRE_REGCHAN)
+        flags |= MODCMD_REQUIRE_CHANNEL;
+    if (flags & (MODCMD_REQUIRE_STAFF|MODCMD_REQUIRE_HELPING))
+        flags |= MODCMD_REQUIRE_AUTHED;
+    cmd->effective_flags = flags;
+}
+
+static void
+svccmd_copy_rules(struct svccmd *dest, struct svccmd *src) {
+    dest->flags |= src->flags;
+    dest->req_account_flags |= src->req_account_flags;
+    dest->deny_account_flags |= src->deny_account_flags;
+    if (src->min_opserv_level > dest->min_opserv_level)
+        dest->min_opserv_level = src->min_opserv_level;
+    if (src->min_channel_access > dest->min_channel_access)
+        dest->min_channel_access = src->min_channel_access;
+    modcmd_set_effective_flags(dest);
+}
+
+static int
+svccmd_configure(struct svccmd *cmd, struct userNode *user, struct userNode *bot, const char *param, const char *value) {
+    if (!irccasecmp(param, "flags")) {
+        unsigned int set_flags, rem_flags;
+        struct modcmd_flag *flag;
+        int opt, end;
+
+        for (set_flags = rem_flags = 0; 1; value += end) {
+            end = strcspn(value, ",");
+            if (*value == '+')
+                opt = 1;
+            else if (*value == '-')
+                opt = 0;
+            else {
+                if (user)
+                    send_message(user, bot, "MCMSG_BARE_FLAG", end, value);
+                else
+                    log_module(MAIN_LOG, LOG_ERROR, "Flag %.*s must be preceded by a + or - (for command %s).", end, value, cmd->name);
+                return 0;
+            }
+            value++;
+            flag = bsearch(value, flags, ArrayLength(flags)-1, sizeof(flags[0]), flags_bsearch);
+            if (!flag) {
+                if (user)
+                    send_message(user, bot, "MCMSG_UNKNOWN_FLAG", end, value);
+                else
+                    log_module(MAIN_LOG, LOG_ERROR, "Unknown module flag %.*s (for command %s).", end, value, cmd->name);
+                return 0;
+            }
+            if (opt)
+                set_flags |= flag->flag, rem_flags &= ~flag->flag;
+            else
+                rem_flags |= flag->flag, set_flags &= ~flag->flag;
+            if (!value[end-1])
+                break;
+        }
+        cmd->flags = (cmd->flags | set_flags) & ~rem_flags;
+        return 1;
+    } else if (!irccasecmp(param, "channel_level") || !irccasecmp(param, "channel_access") || !irccasecmp(param, "access")) {
+        unsigned short ul;
+        if (!irccasecmp(value, "none")) {
+            cmd->min_channel_access = 0;
+            return 1;
+        } else if ((ul = user_level_from_name(value, UL_OWNER)) > 0) {
+            cmd->min_channel_access = ul;
+            return 1;
+        } else if (user) {
+            send_message(user, bot, "MCMSG_BAD_CHANSERV_LEVEL", value);
+            return 0;
+        } else {
+            log_module(MAIN_LOG, LOG_ERROR, "Invalid ChanServ access level %s (for command %s).", value, cmd->name);
+            return 0;
+        }
+    } else if (!irccasecmp(param, "oper_level") || !irccasecmp(param, "oper_access") || !irccasecmp(param, "level")) {
+        unsigned int newval = atoi(value);
+        if (!isdigit(value[0]) || (newval > 1000)) {
+            if (user)
+                send_message(user, bot, "MCMSG_BAD_OPSERV_LEVEL", value);
+            else
+                log_module(MAIN_LOG, LOG_ERROR, "Invalid OpServ access level %s (for command %s).", value, cmd->name);
+            return 0;
+        }
+        if (user && (!user->handle_info || (cmd->min_opserv_level > user->handle_info->opserv_level))) {
+            send_message(user, bot, "MCMSG_LEVEL_TOO_LOW", cmd->name);
+            return 0;
+        }
+        if (user && (!user->handle_info || (newval > user->handle_info->opserv_level))) {
+            send_message(user, bot, "MCMSG_LEVEL_TOO_HIGH", value);
+            return 0;
+        }
+        cmd->min_opserv_level = newval;
+        return 1;
+    } else if (!irccasecmp(param, "account_flags")) {
+        return nickserv_modify_handle_flags(user, bot, value, &cmd->req_account_flags, &cmd->deny_account_flags);
+    } else {
+        if (user)
+            send_message(user, bot, "MCMSG_BAD_OPTION", param);
+        else
+            log_module(MAIN_LOG, LOG_ERROR, "Unknown option %s (for command %s).", param, cmd->name);
+        return 0;
+    }
+}
+
+struct modcmd *
+modcmd_register(struct module *module, const char *name, modcmd_func_t func, unsigned int min_argc, unsigned int flags, ...) {
+    struct modcmd *newcmd;
+    va_list args;
+    const char *param, *value;
+
+    newcmd = calloc(1, sizeof(*newcmd));
+    newcmd->name = strdup(name);
+    newcmd->parent = module;
+    newcmd->func = func;
+    newcmd->min_argc = min_argc;
+    newcmd->flags = flags;
+    newcmd->defaults = calloc(1, sizeof(*newcmd->defaults));
+    newcmd->defaults->name = strdup(newcmd->name);
+    newcmd->defaults->command = newcmd;
+    dict_insert(module->commands, newcmd->name, newcmd);
+    if (newcmd->flags & (MODCMD_REQUIRE_REGCHAN|MODCMD_REQUIRE_CHANNEL|MODCMD_REQUIRE_CHANUSER|MODCMD_REQUIRE_JOINABLE)) {
+        newcmd->defaults->flags |= MODCMD_ACCEPT_CHANNEL;
+    }
+    if (newcmd->flags & MODCMD_REQUIRE_STAFF) {
+        newcmd->defaults->flags |= MODCMD_REQUIRE_AUTHED;
+    }
+    va_start(args, flags);
+    while ((param = va_arg(args, const char*))) {
+        value = va_arg(args, const char*);
+        if (!irccasecmp(param, "template")) {
+            struct svccmd *svccmd = svccmd_resolve_name(newcmd->defaults, value);
+            if (svccmd) {
+                svccmd_copy_rules(newcmd->defaults, svccmd);
+            } else {
+                log_module(MAIN_LOG, LOG_ERROR, "Unable to resolve template name %s for %s.%s.", value, newcmd->parent->name, newcmd->name);
+            }
+            add_pending_template(newcmd->defaults, value);
+        } else {
+            svccmd_configure(newcmd->defaults, NULL, NULL, param, value);
+        }
+    }
+    modcmd_set_effective_flags(newcmd->defaults);
+    va_end(args);
+    return newcmd;
+}
+
+/* This is kind of a lame hack, but it is actually simpler than having
+ * the permission check vary based on the command itself, or having a
+ * more generic rule system.
+ */
+int
+svccmd_can_invoke(struct userNode *user, struct userNode *bot, struct svccmd *cmd, struct chanNode *channel, int options) {
+    unsigned int uData_checked = 0;
+    struct userData *uData = NULL;
+    int rflags = 0, flags = cmd->effective_flags;
+
+    if (flags & MODCMD_DISABLED) {
+        if (options & SVCCMD_NOISY)
+            send_message(user, bot, "MSG_COMMAND_DISABLED", cmd->name);
+        return 0;
+    }
+    if ((flags & MODCMD_REQUIRE_QUALIFIED) && !(options & SVCCMD_QUALIFIED)) {
+        if (options & SVCCMD_NOISY)
+            send_message(user, bot, "MCMSG_MUST_QUALIFY", bot->nick, cmd->name, bot->nick);
+        return 0;
+    }
+    if (flags & MODCMD_REQUIRE_AUTHED) {
+        if (!user->handle_info) {
+            if (options & SVCCMD_NOISY)
+                send_message(user, bot, "MSG_AUTHENTICATE");
+            return 0;
+        }
+        if (HANDLE_FLAGGED(user->handle_info, SUSPENDED)) {
+            if (options & SVCCMD_NOISY)
+                send_message(user, bot, "MCMSG_ACCOUNT_SUSPENDED");
+            return 0;
+        }
+    }
+    if (channel || (options & SVCCMD_NOISY)) {
+        if ((flags & MODCMD_REQUIRE_CHANNEL) && !channel) {
+            if (options & SVCCMD_NOISY)
+                send_message(user, bot, "MSG_INVALID_CHANNEL");
+            return 0;
+        }
+        if (flags & MODCMD_REQUIRE_REGCHAN) {
+            if (!channel->channel_info) {
+                if (options & SVCCMD_NOISY)
+                    send_message(user, bot, "MCMSG_CHAN_NOT_REGISTERED", channel->name);
+                return 0;
+            } else if (IsSuspended(channel->channel_info) && !(flags & MODCMD_IGNORE_CSUSPEND)) {
+                /* allow security-override users to always ignore channel suspensions, but flag it as a staff command */
+                if (!user->handle_info
+                    || !HANDLE_FLAGGED(user->handle_info, HELPING)
+                    || (flags & MODCMD_NEVER_CSUSPEND)) {
+                    if (options & SVCCMD_NOISY)
+                        send_message(user, bot, "MCMSG_CHAN_SUSPENDED", channel->name, channel->channel_info->suspended->reason);
+                    return 0;
+                }
+                rflags |= ACTION_STAFF;
+            }
+        }
+        if (flags & MODCMD_REQUIRE_CHANUSER) {
+            if (!uData_checked)
+                uData = _GetChannelUser(channel->channel_info, user->handle_info, 1, 0), uData_checked = 1;
+            if (!uData) {
+                if (options & SVCCMD_NOISY)
+                    send_message(user, bot, "MCMSG_NO_CHANNEL_ACCESS", channel->name);
+                return 0;
+            } else if (uData->access < cmd->min_channel_access) {
+                if (options & SVCCMD_NOISY)
+                    send_message(user, bot, "MCMSG_LOW_CHANNEL_ACCESS", channel->name);
+                return 0;
+            }
+        }
+        if ((flags & MODCMD_REQUIRE_JOINABLE) && channel) {
+            if (!uData_checked)
+                uData = _GetChannelUser(channel->channel_info, user->handle_info, 1, 0), uData_checked = 1;
+            if ((channel->modes & (MODE_INVITEONLY|MODE_KEY|MODE_SECRET))
+                && !uData
+                && !IsService(user)
+                && !GetUserMode(channel, user)) {
+                if (options & SVCCMD_NOISY)
+                    send_message(user, bot, "MCMSG_REQUIRES_JOINABLE", channel->name);
+                return 0;
+            }
+        }
+        if ((flags & MODCMD_TOY) && channel) {
+            if (!channel->channel_info)
+                rflags |= ACTION_NOCHANNEL;
+            else switch (channel->channel_info->chOpts[chToys]) {
+            case 'd':
+                if (options & SVCCMD_NOISY)
+                    send_message(user, bot, "MCMSG_TOYS_DISABLED", channel->name);
+                return 0;
+            case 'n':
+                rflags |= ACTION_NOCHANNEL;
+                break;
+            case 'p':
+                break;
+            }
+        }
+    }
+    if (flags & MODCMD_REQUIRE_STAFF) {
+        if (((flags & MODCMD_REQUIRE_OPER) && IsOper(user))
+            || ((flags & MODCMD_REQUIRE_NETWORK_HELPER) && IsNetworkHelper(user))
+            || ((flags & MODCMD_REQUIRE_SUPPORT_HELPER) && IsSupportHelper(user))) {
+            /* allow it */
+            rflags |= ACTION_STAFF;
+        } else {
+            if (options & SVCCMD_NOISY)
+                send_message(user, bot, "MSG_COMMAND_PRIVILEGED", cmd->name);
+            return 0;
+        }
+    }
+    if (flags & MODCMD_REQUIRE_HELPING) {
+        if (!HANDLE_FLAGGED(user->handle_info, HELPING)) {
+            if (options & SVCCMD_NOISY)
+                send_message(user, bot, "MCMSG_MUST_BE_HELPING");
+            return 0;
+        }
+        rflags |= ACTION_STAFF;
+    }
+    if (cmd->min_opserv_level > 0) {
+        if (!oper_has_access(user, bot, cmd->min_opserv_level, !(options & SVCCMD_NOISY))) return 0;
+        rflags |= ACTION_STAFF;
+    }
+    if (cmd->req_account_flags || cmd->deny_account_flags) {
+        if (!user->handle_info) {
+            if (options & SVCCMD_NOISY)
+                send_message(user, bot, "MSG_AUTHENTICATE");
+            return 0;
+        }
+        /* Do we want separate or different messages here? */
+        if ((cmd->req_account_flags & ~user->handle_info->flags)
+            || (cmd->deny_account_flags & user->handle_info->flags)) {
+            if (options & SVCCMD_NOISY)
+                send_message(user, bot, "MSG_COMMAND_PRIVILEGED", cmd->name);
+            return 0;
+        }
+    }
+
+    /* If it's an override, return a special value. */
+    if ((flags & MODCMD_REQUIRE_CHANUSER)
+        && (options & SVCCMD_NOISY)
+        && (uData->access > 500)
+        && (!(uData = _GetChannelUser(channel->channel_info, user->handle_info, 0, 0))
+            || uData->access < cmd->min_channel_access)
+        && !(flags & (MODCMD_REQUIRE_STAFF|MODCMD_REQUIRE_HELPING))) {
+        rflags |= ACTION_OVERRIDE;
+    }
+    return rflags | ACTION_ALLOW;
+}
+
+static int
+svccmd_expand_alias(struct svccmd *cmd, unsigned int old_argc, char *old_argv[], char *new_argv[]) {
+    unsigned int ii, new_argc;
+    char *arg;
+
+    for (ii=new_argc=0; ii<cmd->alias.used; ++ii) {
+        arg = cmd->alias.list[ii];
+        if (arg[0] != '$') {
+            new_argv[new_argc++] = arg;
+            continue;
+        }
+        if (arg[1] == '$') {
+            new_argv[new_argc++] = arg + 1;
+        } else if (isdigit(arg[1])) {
+            unsigned int lbound, ubound, jj;
+            char *end_num;
+
+            lbound = strtoul(arg+1, &end_num, 10);
+            switch (end_num[0]) {
+            case 0: ubound = lbound; break;
+            case '-':
+                if (end_num[1] == 0) {
+                    ubound = old_argc - 1;
+                    break;
+                } else if (isdigit(end_num[1])) {
+                    ubound = strtoul(end_num+1, NULL, 10);
+                    break;
+                }
+                /* else fall through to default case */
+            default:
+                log_module(MAIN_LOG, LOG_ERROR, "Alias expansion parse error in %s (near %s; %s.%s arg %d).", arg, end_num, cmd->parent->bot->nick, cmd->name, ii);
+                return 0;
+            }
+            if (ubound >= old_argc)
+                ubound = old_argc - 1;
+            if (lbound < old_argc)
+                for (jj = lbound; jj <= ubound; )
+                    new_argv[new_argc++] = old_argv[jj++];
+        } else {
+            log_module(MAIN_LOG, LOG_ERROR, "Alias expansion: I do not know how to handle %s (%s.%s arg %d).", arg, cmd->parent->bot->nick, cmd->name, ii);
+            return 0;
+        }
+    }
+    return new_argc;
+}
+
+int
+svccmd_invoke_argv(struct userNode *user, struct service *service, struct chanNode *channel, unsigned int argc, char *argv[], unsigned int server_qualified) {
+    extern struct userNode *chanserv;
+    struct svccmd *cmd;
+    unsigned int cmd_arg, perms, flags, options;
+    char channel_name[CHANNELLEN+1];
+
+    /* First check pubcmd for the channel. */
+    if (channel && (channel->channel_info) && (service->bot == chanserv)
+        && !check_user_level(channel, user, lvlPubCmd, 1, 0)) {
+        send_message(user, service->bot, "MCMSG_PUBLIC_DENY", channel->name);
+        return 0;
+    }
+
+    options = (server_qualified ? SVCCMD_QUALIFIED : 0) | SVCCMD_DEBIT | SVCCMD_NOISY;
+    /* Find the command argument. */
+    cmd_arg = IsChannelName(argv[0]) ? 1 : 0;
+    if (argc < cmd_arg+1) {
+        send_message(user, service->bot, "MCMSG_MISSING_COMMAND");
+        return 0;
+    }
+    if (!isalnum(*argv[cmd_arg])) {
+        /* Silently ignore stuff that doesn't begin with a letter or number. */
+        return 0;
+    }
+    cmd = dict_find(service->commands, argv[cmd_arg], NULL);
+    if (!cmd) {
+        send_message(user, service->bot, "MSG_COMMAND_UNKNOWN", argv[cmd_arg]);
+        return 0;
+    }
+    flags = cmd->effective_flags;
+    /* If they put a channel name first, check if the command allows
+     * it.  If so, swap it with the command name.
+     */
+    if (cmd_arg == 1) {
+        char *tmp;
+        /* Complain if we're not supposed to accept the channel. */
+        if (!(flags & MODCMD_ACCEPT_CHANNEL)) {
+            send_message(user, service->bot, "MCMSG_NO_CHANNEL_BEFORE");
+            return 0;
+        }
+        if (!(flags & MODCMD_ACCEPT_PCHANNEL)
+            && (argv[0][0] == '+')) {
+            send_message(user, service->bot, "MCMSG_NO_PLUS_CHANNEL");
+            return 0;
+        }
+        tmp = argv[1];
+        argv[1] = argv[0];
+        argv[0] = tmp;
+    }
+
+    /* Try to grab a channel handle before alias expansion.
+     * If the command accepts a channel name, and argv[1] is
+     * one, use it as a channel name, and hide it from logging.
+     */
+    if ((argc > 1)
+        && (flags & MODCMD_ACCEPT_CHANNEL)
+        && IsChannelName(argv[1])
+        && ((argv[1][0] != '+') || (flags & MODCMD_ACCEPT_PCHANNEL))
+        && (channel = dict_find(channels, argv[1], NULL))) {
+        argv[1] = argv[0];
+        argv++, argc--;
+        cmd_arg = 1;
+    }
+
+    /* Expand the alias arguments, if there are any. */
+    if (cmd->alias.used) {
+        char *new_argv[MAXNUMPARAMS];
+        argc = svccmd_expand_alias(cmd, argc, argv, new_argv);
+        if (!argc) {
+            send_message(service->bot, user, "MCMSG_ALIAS_ERROR", cmd->name);
+            return 0;
+        }
+        argv = new_argv;
+
+        /* Try again to grab a handle to the channel after alias
+         * expansion, overwriting any previous channel. This should,
+         * of course, only be done again if an alias was acually
+         * expanded. */
+        if ((argc > 1)
+            && (flags & MODCMD_ACCEPT_CHANNEL)
+            && IsChannelName(argv[1])
+            && ((argv[1][0] != '+') || (flags & MODCMD_ACCEPT_PCHANNEL))
+            && (channel = dict_find(channels, argv[1], NULL))) {
+            argv[1] = argv[0];
+            argv++, argc--;
+            cmd_arg = 1;
+        }
+    }
+
+    /* Figure out what actions we should do for it.. */
+    if (cmd_arg && (flags & MODCMD_TOY)) {
+        /* Do not let user manually specify a channel. */
+        channel = NULL;
+    }
+    if (argc < cmd->command->min_argc) {
+        send_message(user, service->bot, "MSG_MISSING_PARAMS", cmd->name);
+        return 0;
+    }
+    if (!cmd->command->func) {
+        send_message(user, service->bot, "MCMSG_INTERNAL_COMMAND", cmd->name);
+        return 0;
+    }
+    perms = svccmd_can_invoke(user, service->bot, cmd, channel, options);
+    if (!perms)
+        return 0;
+    cmd->uses++;
+    if (perms & ACTION_NOCHANNEL)
+        channel = NULL;
+
+    if (channel)
+        safestrncpy(channel_name, channel->name, sizeof(channel_name));
+    else
+        channel_name[0] = 0;
+    if (!cmd->command->func(user, channel, argc, argv, cmd))
+        return 0;
+    if (!(flags & MODCMD_NO_LOG)) {
+        enum log_severity slvl;
+        if (perms & ACTION_STAFF)
+            slvl = LOG_STAFF;
+        else if (perms & ACTION_OVERRIDE)
+            slvl = LOG_OVERRIDE;
+        else
+            slvl = LOG_COMMAND;
+        /* Unsplit argv after running the function to get the benefit
+         * of any mangling/hiding done by the commands. */
+        log_audit(cmd->command->parent->clog, slvl, user, service->bot, channel_name, ((flags & MODCMD_LOG_HOSTMASK) ? AUDIT_HOSTMASK : 0), unsplit_string(argv, argc, NULL));
+    }
+    return 1;
+}
+
+int
+svccmd_send_help(struct userNode *user, struct userNode *bot, struct svccmd *cmd) {
+    char cmdname[MAXLEN];
+    unsigned int nn;
+    /* Show command name (in bold). */
+    for (nn=0; cmd->name[nn]; nn++)
+        cmdname[nn] = toupper(cmd->name[nn]);
+    cmdname[nn] = 0;
+    send_message_type(4, user, bot, "$b%s$b", cmdname);
+    /* If it's an alias, show what it's an alias for. */
+    if (cmd->alias.used) {
+        char alias_text[MAXLEN];
+        unsplit_string((char**)cmd->alias.list, cmd->alias.used, alias_text);
+        send_message(user, bot, "MCMSG_COMMAND_ALIASES", cmd->name, alias_text);
+    }
+    /* Show the help entry for the underlying command. */
+    return send_help(user, bot, cmd->command->parent->helpfile, cmd->command->name);
+}
+
+int
+svccmd_send_help_2(struct userNode *user, struct service *service, const char *topic) {
+    struct module *module;
+    struct svccmd *cmd;
+    unsigned int ii;
+
+    if ((cmd = dict_find(service->commands, topic, NULL)))
+        return svccmd_send_help(user, service->bot, cmd);
+    if (!topic)
+        topic = "<index>";
+    for (ii = 0; ii < service->modules.used; ++ii) {
+        module = service->modules.list[ii];
+        if (!module->helpfile)
+            continue;
+        if (dict_find(module->helpfile->db, topic, NULL))
+            return send_help(user, service->bot, module->helpfile, topic);
+    }
+    send_message(user, service->bot, "MSG_TOPIC_UNKNOWN");
+    return 0;
+}
+
+static int
+svccmd_invoke(struct userNode *user, struct service *service, struct chanNode *channel, char *text, int server_qualified) {
+    unsigned int argc;
+    char *argv[MAXNUMPARAMS];
+
+    if (!*text)
+        return 0;
+    if (service->privileged) {
+        if (!IsOper(user)) {
+            send_message(user, service->bot, "MSG_SERVICE_PRIVILEGED", service->bot->nick);
+            return 0;
+        }
+        if (!user->handle_info) {
+            send_message(user, service->bot, "MSG_AUTHENTICATE");
+            return 0;
+        }
+        if (HANDLE_FLAGGED(user->handle_info, OPER_SUSPENDED)) {
+            send_message(user, service->bot, "MSG_OPER_SUSPENDED");
+            return 0;
+        }
+    }
+    argc = split_line(text, false, ArrayLength(argv), argv);
+    return argc ? svccmd_invoke_argv(user, service, channel, argc, argv, server_qualified) : 0;
+}
+
+void
+modcmd_privmsg(struct userNode *user, struct userNode *bot, char *text, int server_qualified) {
+    struct service *service;
+    if (!(service = dict_find(services, bot->nick, NULL))) {
+        log_module(MAIN_LOG, LOG_ERROR, "modcmd_privmsg got privmsg for unhandled service %s, unregistering.", bot->nick);
+        reg_privmsg_func(bot, NULL);
+        return;
+    }
+    if (service->msg_hook && service->msg_hook(user, bot, text, server_qualified))
+        return;
+    svccmd_invoke(user, service, NULL, text, server_qualified);
+}
+
+void
+modcmd_chanmsg(struct userNode *user, struct chanNode *chan, char *text, struct userNode *bot) {
+    struct service *service;
+    if (!(service = dict_find(services, bot->nick, NULL))) return;
+    svccmd_invoke(user, service, chan, text, 0);
+}
+
+struct service *
+service_register(struct userNode *bot, char trigger) {
+    struct service *service;
+    if ((service = dict_find(services, bot->nick, NULL)))
+        return service;
+    service = calloc(1, sizeof(*service));
+    module_list_init(&service->modules);
+    service->commands = dict_new();
+    service->bot = bot;
+    service->trigger = trigger;
+    dict_set_free_data(service->commands, free_service_command);
+    dict_insert(services, service->bot->nick, service);
+    reg_privmsg_func(bot, modcmd_privmsg);
+    if (trigger)
+        reg_chanmsg_func(trigger, bot, modcmd_chanmsg);
+    return service;
+}
+
+struct service *
+service_find(const char *name) {
+    return dict_find(services, name, NULL);
+}
+
+static void
+svccmd_insert(struct service *service, char *name, struct svccmd *svccmd, struct modcmd *modcmd) {
+    unsigned int ii;
+    svccmd->parent = service;
+    svccmd->name = name;
+    svccmd->command = modcmd;
+    svccmd->command->bind_count++;
+    dict_insert(service->commands, svccmd->name, svccmd);
+    for (ii=0; ii<service->modules.used; ++ii) {
+        if (service->modules.list[ii] == svccmd->command->parent) break;
+    }
+    if (ii == service->modules.used) {
+        module_list_append(&service->modules, svccmd->command->parent);
+    }
+}
+
+struct svccmd *
+service_bind_modcmd(struct service *service, struct modcmd *cmd, const char *name) {
+    struct svccmd *svccmd;
+    if ((svccmd = dict_find(service->commands, name, NULL))) {
+        if (svccmd->command == cmd) return svccmd;
+        log_module(MAIN_LOG, LOG_ERROR, "Tried to bind command %s.%s into service %s as %s, but already bound (as %s.%s).", cmd->parent->name, cmd->name, service->bot->nick, name, svccmd->command->parent->name, svccmd->command->name);
+        return NULL;
+    }
+    svccmd = calloc(1, sizeof(*svccmd));
+    svccmd_insert(service, strdup(name), svccmd, cmd);
+    svccmd_copy_rules(svccmd, cmd->defaults);
+    return svccmd;
+}
+
+static unsigned int
+service_bind_module(struct service *service, struct module *module) {
+    dict_iterator_t it;
+    struct modcmd *modcmd;
+    unsigned int count;
+
+    count = 0;
+    for (it = dict_first(module->commands); it; it = iter_next(it)) {
+        modcmd = iter_data(it);
+        if (!((modcmd->flags | modcmd->defaults->flags) & MODCMD_NO_DEFAULT_BIND))
+            if (service_bind_modcmd(service, modcmd, iter_key(it)))
+                count++;
+    }
+    return count;
+}
+
+/* This MUST return argc if the alias expansion code knows how to deal
+ * with every argument in argv; otherwise, it MUST return the index of
+ * an argument that the expansion code does not know how to deal with.
+ */
+static unsigned int
+check_alias_args(char *argv[], unsigned int argc) {
+    unsigned int arg;
+
+    for (arg=0; arg<argc; ++arg) {
+        if (argv[arg][0] != '$') {
+            continue;
+        } else if (argv[arg][1] == '$') {
+            continue;
+        } else if (isdigit(argv[arg][1])) {
+            char *end_num;
+
+            strtoul(argv[arg]+1, &end_num, 10);
+            switch (end_num[0]) {
+            case 0:
+                continue;
+            case '-':
+                if (end_num[1] == 0)
+                    continue;
+                else if (isdigit(end_num[1]))
+                    continue;
+                /* else fall through to default case */
+            default:
+                return arg;
+            }
+        } else
+            return arg;
+    }
+    return arg;
+}
+
+static unsigned int
+collapse_cmdname(char *argv[], unsigned int argc, char *dest) {
+    unsigned int ii, pos, arg;
+    if (!argc) {
+        dest[0] = 0;
+        return 0;
+    }
+    for (ii=pos=0, arg=0; argv[arg][ii]; ) {
+        if (argv[arg][ii] == '\\') {
+            if (argv[arg][ii+1]) {
+                /* escaping a real character just puts it in literally */
+                dest[pos++] = argv[arg][++ii];
+            } else if ((arg+1) == argc) {
+                /* we ran to the end of the argument list; abort */
+                break;
+            } else {
+                /* escape at end of a word is a space */
+                dest[pos++] = ' ';
+                ii = 0;
+                arg++;
+            }
+        } else {
+            /* normal characters don't need escapes */
+            dest[pos++] = argv[arg][ii++];
+        }
+    }
+    dest[pos] = 0;
+    return arg + 1;
+}
+
+static MODCMD_FUNC(cmd_bind) {
+    struct service *service;
+    struct svccmd *template, *newcmd;
+    char *svcname, *dot;
+    char newname[MAXLEN], cmdname[MAXLEN];
+    unsigned int arg, diff;
+
+    assert(argc > 3);
+    svcname = argv[1];
+    arg = collapse_cmdname(argv+2, argc-2, newname) + 2;
+    if (!arg) {
+        reply("MSG_MISSING_PARAMS", cmd->name);
+        return 0;
+    }
+    diff = collapse_cmdname(argv+arg, argc-arg, cmdname);
+    if (!diff) {
+        reply("MSG_MISSING_PARAMS", cmd->name);
+        return 0;
+    }
+    arg += diff;
+
+    if (!(service = service_find(svcname))) {
+        reply("MCMSG_UNKNOWN_SERVICE", svcname);
+        return 0;
+    }
+
+    if ((newcmd = dict_find(service->commands, newname, NULL))) {
+        reply("MCMSG_ALREADY_BOUND", service->bot->nick, newname);
+        return 0;
+    }
+
+    if ((dot = strchr(cmdname, '.')) && (dot[1] == '*') && (dot[2] == 0)) {
+        unsigned int count;
+        struct module *module;
+        *dot = 0;
+        module = module_find((cmdname[0] == '*') ? cmdname+1 : cmdname);
+        if (!module) {
+            reply("MSG_MODULE_UNKNOWN", cmdname);
+            return 0;
+        }
+        count = service_bind_module(service, module);
+        reply("MCMSG_MODULE_BOUND", count, module->name, service->bot->nick);
+        return count != 0;
+    }
+    newcmd = calloc(1, sizeof(*newcmd));
+    newcmd->name = strdup(newname);
+    newcmd->parent = service;
+    if (!(template = svccmd_resolve_name(newcmd, cmdname))) {
+        reply("MCMSG_UNKNOWN_COMMAND_2", cmdname, service->bot->nick);
+        free(newcmd->name);
+        free(newcmd);
+        return 0;
+    }
+    if (template->alias.used) {
+        reply("MCMSG_CANNOT_DOUBLE_ALIAS");
+        free(newcmd->name);
+        free(newcmd);
+        return 0;
+    }
+
+    if (argc > arg) {
+        /* a more complicated alias; fix it up */
+        unsigned int nn;
+
+        arg -= diff;
+        nn = check_alias_args(argv+arg, argc-arg);
+        if (nn+arg < argc) {
+            reply("MCMSG_BAD_ALIAS_ARGUMENT", argv[nn+arg]);
+            free(newcmd->name);
+            free(newcmd);
+            return 0;
+        }
+        newcmd->alias.used = newcmd->alias.size = argc-arg;
+        newcmd->alias.list = calloc(newcmd->alias.size, sizeof(newcmd->alias.list[0]));
+        for (nn=0; nn<newcmd->alias.used; ++nn)
+            newcmd->alias.list[nn] = strdup(argv[nn+arg]);
+    }
+
+    svccmd_insert(service, newcmd->name, newcmd, template->command);
+    svccmd_copy_rules(newcmd, template);
+    reply("MCMSG_COMMAND_BOUND", newcmd->name, newcmd->parent->bot->nick);
+    return 1;
+}
+
+static int
+service_recheck_bindings(struct service *service, struct module *module) {
+    dict_iterator_t it;
+    struct svccmd *cmd;
+
+    for (it = dict_first(service->commands); it; it = iter_next(it)) {
+        cmd = iter_data(it);
+        if (cmd->command->parent == module) return 0;
+    }
+    /* No more bindings, remove it from our list. */
+    module_list_remove(&service->modules, module);
+    return 1;
+}
+
+static svccmd_unbind_func_t *suf_list;
+unsigned int suf_size, suf_used;
+
+void
+reg_svccmd_unbind_func(svccmd_unbind_func_t handler) {
+    if (suf_used == suf_size) {
+        if (suf_size) {
+            suf_size <<= 1;
+            suf_list = realloc(suf_list, suf_size*sizeof(svccmd_unbind_func_t));
+        } else {
+            suf_size = 8;
+            suf_list = malloc(suf_size*sizeof(svccmd_unbind_func_t));
+        }
+    }
+    suf_list[suf_used++] = handler;
+}
+
+static MODCMD_FUNC(cmd_unbind) {
+    struct service *service;
+    struct userNode *bot;
+    struct svccmd *bound;
+    struct module *module;
+    const char *svcname;
+    unsigned int arg, ii;
+    char cmdname[MAXLEN];
+
+    assert(argc > 2);
+    svcname = argv[1];
+    arg = collapse_cmdname(argv+2, argc-2, cmdname) + 2;
+    if (!arg) {
+        reply("MSG_MISSING_PARAMS", cmd->name);
+        return 0;
+    }
+    if (!(service = service_find(svcname))) {
+        reply("MCMSG_UNKNOWN_SERVICE", svcname);
+        return 0;
+    }
+    if (!(bound = dict_find(service->commands, cmdname, NULL))) {
+        reply("MCMSG_NO_COMMAND_BOUND", service->bot->nick, cmdname);
+        return 0;
+    }
+    if ((bound->command->flags & MODCMD_KEEP_BOUND) && (bound->command->bind_count == 1)) {
+        reply("MCMSG_UNBIND_PROHIBITED", bound->command->name);
+        return 0;
+    }
+
+    for (ii=0; ii<suf_used; ii++)
+        suf_list[ii](bound);
+    /* If this command binding is removing itself, we must take care
+     * not to dereference it after the dict_remove.
+     */
+    bot = cmd->parent->bot;
+    module = cmd->command->parent;
+    dict_remove(service->commands, bound->name);
+    send_message(user, bot, "MCMSG_COMMAND_UNBOUND", cmdname, service->bot->nick);
+    if (service_recheck_bindings(service, module))
+        send_message(user, bot, "MCMSG_HELPFILE_UNBOUND", module->name, module->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_readhelp) {
+    const char *modname;
+    struct module *module;
+    struct helpfile *old_helpfile;
+    struct timeval start, stop;
+
+    assert(argc > 1);
+    modname = argv[1];
+    if (!(module = module_find(modname))) {
+        reply("MSG_MODULE_UNKNOWN", modname);
+        return 0;
+    }
+    if (!module->helpfile_name) {
+        reply("MCMSG_NO_HELPFILE", module->name);
+        return 0;
+    }
+    old_helpfile = module->helpfile;
+    gettimeofday(&start, NULL);
+    module->helpfile = open_helpfile(module->helpfile_name, module->expand_help);
+    if (!module->helpfile) {
+        module->helpfile = old_helpfile;
+        reply("MCMSG_HELPFILE_ERROR", module->helpfile_name);
+        return 0;
+    }
+    if (old_helpfile) close_helpfile(old_helpfile);
+    gettimeofday(&stop, NULL);
+    stop.tv_sec -= start.tv_sec;
+    stop.tv_usec -= start.tv_usec;
+    if (stop.tv_usec < 0) {
+       stop.tv_sec -= 1;
+       stop.tv_usec += 1000000;
+    }
+    reply("MCMSG_HELPFILE_READ", module->name, stop.tv_sec, stop.tv_usec/1000);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_help) {
+    const char *topic;
+
+    topic = (argc < 2) ? NULL : unsplit_string(argv+1, argc-1, NULL);
+    return svccmd_send_help_2(user, cmd->parent, topic);
+}
+
+static MODCMD_FUNC(cmd_timecmd) {
+    struct timeval start, stop;
+    char cmd_text[MAXLEN];
+
+    unsplit_string(argv+1, argc-1, cmd_text);
+    gettimeofday(&start, NULL);
+    svccmd_invoke(user, cmd->parent, channel, cmd_text, 0);
+    gettimeofday(&stop, NULL);
+    stop.tv_sec -= start.tv_sec;
+    stop.tv_usec -= start.tv_usec;
+    if (stop.tv_usec < 0) {
+       stop.tv_sec -= 1;
+       stop.tv_usec += 1000000;
+    }
+    reply("MCMSG_COMMAND_TIME", cmd_text, stop.tv_sec, stop.tv_usec);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_command) {
+    struct svccmd *svccmd;
+    const char *cmd_name, *fmt_str;
+    unsigned int flags, shown_flags, nn, pos;
+    char buf[MAXLEN];
+
+    assert(argc >= 2);
+    cmd_name = unsplit_string(argv+1, argc-1, NULL);
+    if (!(svccmd = svccmd_resolve_name(cmd, cmd_name))) {
+        reply("MCMSG_UNKNOWN_COMMAND_2", cmd_name, cmd->parent->bot->nick);
+        return 0;
+    }
+    pos = snprintf(buf, sizeof(buf), "%s.%s", svccmd->command->parent->name, svccmd->command->name);
+    if (svccmd->alias.used) {
+        buf[pos++] = ' ';
+        unsplit_string((char**)svccmd->alias.list+1, svccmd->alias.used-1, buf+pos);
+        reply("MCMSG_COMMAND_ALIASES", svccmd->name, buf);
+    } else {
+        reply("MCMSG_COMMAND_BINDING", svccmd->name, buf);
+    }
+    flags = svccmd->effective_flags;
+    if ((svccmd->parent && svccmd->parent->privileged && !IsOper(user))
+        || ((flags & MODCMD_REQUIRE_STAFF)
+            && !IsOper(user) && !IsNetworkHelper(user) && !IsSupportHelper(user))) {
+        reply("MCMSG_INSPECTION_REFUSED", svccmd->name);
+        return 0;
+    }
+    if (flags & MODCMD_DISABLED) {
+        reply("MSG_COMMAND_DISABLED", svccmd->name);
+        return 1;
+    }
+    shown_flags = 0;
+    if (svccmd->min_opserv_level > 0) {
+        reply("MCMSG_NEED_OPSERV_LEVEL", svccmd->min_opserv_level);
+        shown_flags |= MODCMD_REQUIRE_OPER | MODCMD_REQUIRE_AUTHED;
+    }
+    if (svccmd->min_channel_access > 0) {
+        reply("MCMSG_NEED_CHANSERV_LEVEL", svccmd->min_channel_access);
+        shown_flags |= MODCMD_REQUIRE_CHANUSER | MODCMD_REQUIRE_REGCHAN | MODCMD_REQUIRE_CHANNEL | MODCMD_REQUIRE_AUTHED;
+    }
+    if (svccmd->req_account_flags) {
+        for (nn=pos=0; nn<32; nn++) {
+            if (!(svccmd->req_account_flags & (1 << nn))) continue;
+            buf[pos++] = HANDLE_FLAGS[nn];
+        }
+        buf[pos] = 0;
+        reply("MCMSG_NEED_ACCOUNT_FLAGS", buf);
+        shown_flags |= MODCMD_REQUIRE_AUTHED;
+    }
+    if (!flags && !shown_flags) {
+        reply("MCMSG_NEED_NOTHING", svccmd->name);
+        return 1;
+    }
+    if (flags & ~shown_flags & MODCMD_REQUIRE_HELPING) {
+        reply("MCMSG_MUST_BE_HELPING");
+        shown_flags |= MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_STAFF;
+    }
+    if (flags & ~shown_flags & MODCMD_REQUIRE_STAFF) {
+        switch (flags & MODCMD_REQUIRE_STAFF) {
+        default: case MODCMD_REQUIRE_STAFF:
+            fmt_str = "MCMSG_NEED_STAFF_ACCESS";
+            break;
+        case MODCMD_REQUIRE_OPER:
+            fmt_str = "MCMSG_NEED_STAFF_OPER";
+            break;
+        case MODCMD_REQUIRE_NETWORK_HELPER:
+            fmt_str = "MCMSG_NEED_STAFF_NETHELPER";
+            break;
+        case MODCMD_REQUIRE_OPER|MODCMD_REQUIRE_NETWORK_HELPER:
+            fmt_str = "MCMSG_NEED_STAFF_NETHELPER_OR_OPER";
+            break;
+        case MODCMD_REQUIRE_SUPPORT_HELPER:
+            fmt_str = "MCMSG_NEED_STAFF_SHELPER";
+            break;
+        case MODCMD_REQUIRE_OPER|MODCMD_REQUIRE_SUPPORT_HELPER:
+            fmt_str = "MCMSG_NEED_STAFF_SHELPER_OR_OPER";
+            break;
+        case MODCMD_REQUIRE_SUPPORT_HELPER|MODCMD_REQUIRE_NETWORK_HELPER:
+            fmt_str = "MCMSG_NEED_STAFF_HELPER";
+            break;
+        }
+        reply(fmt_str);
+        shown_flags |= MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_STAFF;
+    }
+    if (flags & ~shown_flags & MODCMD_REQUIRE_JOINABLE) {
+        reply("MCMSG_NEED_JOINABLE");
+        shown_flags |= MODCMD_REQUIRE_CHANUSER;
+    }
+    if (flags & ~shown_flags & MODCMD_REQUIRE_CHANUSER) {
+        if (flags & ~shown_flags & MODCMD_IGNORE_CSUSPEND)
+            reply("MCMSG_NEED_CHANUSER_CSUSPENDABLE");
+        else
+            reply("MCMSG_NEED_CHANUSER");
+        shown_flags |= MODCMD_IGNORE_CSUSPEND | MODCMD_REQUIRE_REGCHAN | MODCMD_REQUIRE_CHANNEL | MODCMD_REQUIRE_AUTHED;
+    }
+    if (flags & ~shown_flags & MODCMD_REQUIRE_REGCHAN) {
+        reply("MCMSG_NEED_REGCHAN");
+        shown_flags |= MODCMD_REQUIRE_CHANNEL;
+    }
+    if (flags & ~shown_flags & MODCMD_REQUIRE_CHANNEL)
+        reply("MCMSG_NEED_CHANNEL");
+    if (flags & ~shown_flags & MODCMD_REQUIRE_AUTHED)
+        reply("MCMSG_NEED_AUTHED");
+    if (flags & ~shown_flags & MODCMD_TOY)
+        reply("MCMSG_IS_TOY", svccmd->name);
+    if (flags & ~shown_flags & MODCMD_REQUIRE_QUALIFIED) {
+        const char *botnick = svccmd->parent ? svccmd->parent->bot->nick : "SomeBot";
+        reply("MCMSG_MUST_QUALIFY", botnick, svccmd->name, botnick);
+    }
+    reply("MCMSG_END_REQUIREMENTS", svccmd->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_modcmd) {
+    struct svccmd *svccmd;
+    unsigned int arg, changed;
+    char cmdname[MAXLEN];
+
+    assert(argc >= 2);
+    arg = collapse_cmdname(argv+1, argc-1, cmdname) + 1;
+    if (!arg || (arg+2 < argc)) {
+        reply("MSG_MISSING_PARAMS", cmd->name);
+        return 0;
+    }
+    if (!(svccmd = svccmd_resolve_name(cmd, cmdname))) {
+        reply("MCMSG_UNKNOWN_COMMAND_2", cmdname, cmd->parent->bot->nick);
+        return 0;
+    }
+    changed = 0;
+    while (arg+1 < argc) {
+        if (svccmd_configure(svccmd, user, cmd->parent->bot, argv[arg], argv[arg+1])) {
+            reply("MCMSG_COMMAND_MODIFIED", argv[arg], svccmd->name);
+            changed = 1;
+        }
+        arg += 2;
+    }
+    if (changed)
+        modcmd_set_effective_flags(svccmd);
+    return changed;
+}
+
+static MODCMD_FUNC(cmd_god) {
+    int helping;
+
+    if (argc > 1) {
+        if (enabled_string(argv[1])) {
+            if (HANDLE_FLAGGED(user->handle_info, HELPING)) {
+                reply("MCMSG_ALREADY_HELPING");
+                return 0;
+            }
+           helping = 1;
+        } else if (disabled_string(argv[1])) {
+            if (!HANDLE_FLAGGED(user->handle_info, HELPING)) {
+                reply("MCMSG_ALREADY_NOT_HELPING");
+                return 0;
+            }
+           helping = 0;
+        } else {
+            reply("MSG_INVALID_BINARY", argv[1]);
+            return 0;
+       }
+    } else {
+        helping = !IsHelping(user);
+    }
+
+    if (helping) {
+        HANDLE_SET_FLAG(user->handle_info, HELPING);
+        reply("MCMSG_NOW_HELPING");
+    } else {
+        HANDLE_CLEAR_FLAG(user->handle_info, HELPING);
+        reply("MCMSG_NOW_NOT_HELPING");
+    }
+
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_joiner) {
+    char cmdname[80];
+
+    if (argc < 2) {
+        int len = sprintf(cmdname, "%s ", cmd->name);
+        dict_iterator_t it;
+        struct string_buffer sbuf;
+
+        string_buffer_init(&sbuf);
+        for (it = dict_first(cmd->parent->commands); it; it = iter_next(it)) {
+            if (!ircncasecmp(iter_key(it), cmdname, len)) {
+                if (sbuf.used) string_buffer_append_string(&sbuf, ", ");
+                string_buffer_append_string(&sbuf, iter_key(it));
+            }
+        }
+        if (!sbuf.used) string_buffer_append(&sbuf, 0);
+        reply("MCMSG_JOINER_CHOICES", cmd->name, sbuf.list);
+        string_buffer_clean(&sbuf);
+        return 1;
+    }
+    sprintf(cmdname, "%s %s", cmd->name, argv[1]);
+    argv[1] = cmdname;
+    svccmd_invoke_argv(user, cmd->parent, channel, argc-1, argv+1, 0);
+    return 0; /* never try to log this; the recursive one logs it */
+}
+
+static MODCMD_FUNC(cmd_stats_modules) {
+    struct helpfile_table tbl;
+    dict_iterator_t it;
+    unsigned int ii;
+    struct module *mod;
+    struct modcmd *modcmd;
+
+    if (argc < 2) {
+        tbl.length = dict_size(modules) + 1;
+        tbl.width = 3;
+        tbl.flags = TABLE_PAD_LEFT;
+        tbl.contents = calloc(tbl.length, sizeof(tbl.contents[0]));
+        tbl.contents[0] = calloc(tbl.width, sizeof(tbl.contents[0][0]));
+        tbl.contents[0][0] = "Module";
+        tbl.contents[0][1] = "Commands";
+        tbl.contents[0][2] = "Helpfile";
+        for (ii=1, it=dict_first(modules); it; it=iter_next(it), ii++) {
+            mod = iter_data(it);
+            tbl.contents[ii] = calloc(tbl.width, sizeof(tbl.contents[ii][0]));
+            tbl.contents[ii][0] = mod->name;
+            tbl.contents[ii][1] = strtab(dict_size(mod->commands));
+            tbl.contents[ii][2] = mod->helpfile_name ? mod->helpfile_name : "(none)";
+        }
+    } else if (!(mod = dict_find(modules, argv[1], NULL))) {
+        reply("MCMSG_UNKNOWN_MODULE", argv[1]);
+        return 0;
+    } else {
+        reply("MCMSG_MODULE_INFO", mod->name);
+        tbl.length = dict_size(mod->commands) + 1;
+        tbl.width = 3;
+        tbl.flags = TABLE_PAD_LEFT;
+        tbl.contents = calloc(tbl.length, sizeof(tbl.contents[0]));
+        tbl.contents[0] = calloc(tbl.width, sizeof(tbl.contents[0][0]));
+        tbl.contents[0][0] = "Command";
+        tbl.contents[0][1] = "Min. Args";
+        tbl.contents[0][2] = "Bind Count";
+        for (ii=1, it=dict_first(mod->commands); it; it=iter_next(it), ii++) {
+            modcmd = iter_data(it);
+            tbl.contents[ii] = calloc(tbl.width, sizeof(tbl.contents[ii][0]));
+            tbl.contents[ii][0] = modcmd->name;
+            tbl.contents[ii][1] = strtab(modcmd->min_argc);
+            tbl.contents[ii][2] = strtab(modcmd->bind_count);
+        }
+    }
+    table_send(cmd->parent->bot, user->nick, 0, 0, tbl);
+    return 0;
+}
+
+static MODCMD_FUNC(cmd_stats_services) {
+    struct helpfile_table tbl;
+    dict_iterator_t it;
+    unsigned int ii;
+    struct service *service;
+    struct svccmd *svccmd;
+    char *extra;
+
+    if (argc < 2) {
+        tbl.length = dict_size(services) + 1;
+        tbl.width = 4;
+        tbl.flags = TABLE_PAD_LEFT;
+        tbl.contents = calloc(tbl.length, sizeof(tbl.contents[0]));
+        tbl.contents[0] = calloc(tbl.width, sizeof(tbl.contents[0][0]));
+        tbl.contents[0][0] = "Service";
+        tbl.contents[0][1] = "Commands";
+        tbl.contents[0][2] = "Priv'd?";
+        tbl.contents[0][3] = "Trigger";
+        extra = calloc(2, tbl.length);
+        for (ii=1, it=dict_first(services); it; it=iter_next(it), ii++) {
+            service = iter_data(it);
+            tbl.contents[ii] = calloc(tbl.width, sizeof(tbl.contents[ii][0]));
+            tbl.contents[ii][0] = service->bot->nick;
+            tbl.contents[ii][1] = strtab(dict_size(service->commands));
+            tbl.contents[ii][2] = service->privileged ? "yes" : "no";
+            extra[ii*2] = service->trigger;
+            tbl.contents[ii][3] = extra+ii*2;
+        }
+        table_send(cmd->parent->bot, user->nick, 0, 0, tbl);
+        free(extra);
+        return 0;
+    } else if (!(service = dict_find(services, argv[1], NULL))) {
+        reply("MCMSG_UNKNOWN_SERVICE", argv[1]);
+        return 0;
+    } else {
+        tbl.length = dict_size(service->commands) + 1;
+        tbl.width = 5;
+        tbl.flags = TABLE_PAD_LEFT | TABLE_NO_FREE;
+        tbl.contents = calloc(tbl.length, sizeof(tbl.contents[0]));
+        tbl.contents[0] = calloc(tbl.width, sizeof(tbl.contents[0][0]));
+        tbl.contents[0][0] = "Command";
+        tbl.contents[0][1] = "Module";
+        tbl.contents[0][2] = "ModCmd";
+        tbl.contents[0][3] = "Alias?";
+        tbl.contents[0][4] = strdup("Uses");
+        for (ii=1, it=dict_first(service->commands); it; it=iter_next(it), ii++) {
+            svccmd = iter_data(it);
+            tbl.contents[ii] = calloc(tbl.width, sizeof(tbl.contents[ii][0]));
+            tbl.contents[ii][0] = svccmd->name;
+            tbl.contents[ii][1] = svccmd->command->parent->name;
+            tbl.contents[ii][2] = svccmd->command->name;
+            tbl.contents[ii][3] = svccmd->alias.used ? "yes" : "no";
+            tbl.contents[ii][4] = extra = malloc(12);
+            sprintf(extra, "%u", svccmd->uses);
+        }
+        reply("MCMSG_SERVICE_INFO", service->bot->nick);
+        table_send(cmd->parent->bot, user->nick, 0, 0, tbl);
+        for (ii=0; ii<tbl.length; ii++) {
+            free((char*)tbl.contents[ii][4]);
+            free(tbl.contents[ii]);
+        }
+        free(tbl.contents);
+        return 0;
+    }
+}
+
+static MODCMD_FUNC(cmd_showcommands) {
+    struct svccmd_list commands;
+    struct helpfile_table tbl;
+    struct svccmd *svccmd;
+    dict_iterator_t it;
+    unsigned int ii, ignore_flags = 0;
+    unsigned int max_opserv_level = 1000;
+    unsigned short max_chanserv_level = 500;
+    char show_opserv_level = 0, show_channel_access = 0;
+
+    /* Check to see what the max access they want to see is. */
+    for (ii=1; ii<argc; ++ii) {
+        if (isdigit(argv[ii][0]))
+            max_opserv_level = atoi(argv[ii]);
+        else
+            max_chanserv_level = user_level_from_name(argv[ii], UL_OWNER);
+    }
+
+    /* Find the matching commands. */
+    svccmd_list_init(&commands);
+    if (cmd->parent->privileged) ignore_flags = MODCMD_REQUIRE_OPER;
+    for (it = dict_first(cmd->parent->commands); it; it = iter_next(it)) {
+        svccmd = iter_data(it);
+        if (strchr(svccmd->name, ' '))
+            continue;
+        if (!svccmd_can_invoke(user, svccmd->parent->bot, svccmd, channel, SVCCMD_QUALIFIED))
+            continue;
+        if (svccmd->min_opserv_level > max_opserv_level)
+            continue;
+        if (svccmd->min_channel_access > max_chanserv_level)
+            continue;
+        if (svccmd->min_opserv_level > 0)
+            show_opserv_level = 1;
+        if (svccmd->min_channel_access > 0)
+            show_channel_access = 1;
+        if (svccmd->effective_flags
+            & (MODCMD_REQUIRE_STAFF|MODCMD_REQUIRE_HELPING)
+            & ~ignore_flags) {
+            show_channel_access = 1;
+        }
+        svccmd_list_append(&commands, svccmd);
+    }
+
+    /* Build the table. */
+    tbl.length = commands.used + 1;
+    tbl.width = 1 + show_opserv_level + show_channel_access;
+    tbl.flags = TABLE_REPEAT_ROWS;
+    tbl.contents = calloc(tbl.length, sizeof(tbl.contents[0]));
+    tbl.contents[0] = calloc(tbl.width, sizeof(tbl.contents[0][0]));
+    tbl.contents[0][ii = 0] = "Command";
+    if (show_opserv_level)
+        tbl.contents[0][++ii] = "OpServ Level";
+    if (show_channel_access)
+        tbl.contents[0][++ii] = "ChanServ Access";
+    for (ii=0; ii<commands.used; ++ii) {
+        svccmd = commands.list[ii];
+        tbl.contents[ii+1] = calloc(tbl.width, sizeof(tbl.contents[0][0]));
+        tbl.contents[ii+1][0] = svccmd->name;
+        if (show_opserv_level)
+            tbl.contents[ii+1][1] = strtab(svccmd->min_opserv_level);
+        if (show_channel_access) {
+            const char *access;
+            int flags = svccmd->effective_flags;
+            if (flags & MODCMD_REQUIRE_HELPING)
+                access = "helping";
+            else if (flags & MODCMD_REQUIRE_STAFF) {
+                switch (flags & MODCMD_REQUIRE_STAFF) {
+                case MODCMD_REQUIRE_OPER: access = "oper"; break;
+                case MODCMD_REQUIRE_OPER | MODCMD_REQUIRE_NETWORK_HELPER:
+                case MODCMD_REQUIRE_NETWORK_HELPER: access = "net.helper"; break;
+                default: access = "staff"; break;
+                }
+            } else
+                access = strtab(svccmd->min_channel_access);
+            tbl.contents[ii+1][1+show_opserv_level] = access;
+        }
+    }
+    svccmd_list_clean(&commands);
+    table_send(cmd->parent->bot, user->nick, 0, 0, tbl);
+    return 0;
+}
+
+static MODCMD_FUNC(cmd_helpfiles) {
+    struct service *service;
+    unsigned int ii;
+
+    if (!(service = dict_find(services, argv[1], NULL))) {
+        reply("MCMSG_UNKNOWN_SERVICE", argv[1]);
+        return 0;
+    }
+
+    if (argc < 3) {
+        for (ii=0; ii<service->modules.used; ++ii)
+            reply("MCMSG_HELPFILE_SEQUENCE", ii+1, service->modules.list[ii]->name);
+        return 0;
+    }
+
+    service->modules.used = 0;
+    for (ii=0; ii<argc-2; ii++) {
+        struct module *module = dict_find(modules, argv[ii+2], NULL);
+        if (!module) {
+            reply("MCMSG_UNKNOWN_MODULE", argv[ii+2]);
+            continue;
+        }
+        module_list_append(&service->modules, module);
+    }
+    reply("MCMSG_HELPFILE_SEQUENCE_SET", service->bot->nick);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_service_add) {
+    const char *nick, *desc;
+    struct userNode *bot;
+
+    nick = argv[1];
+    if (!is_valid_nick(nick)) {
+        reply("MCMSG_BAD_SERVICE_NICK", nick);
+        return 0;
+    }
+    desc = unsplit_string(argv+2, argc-2, NULL);
+    bot = GetUserH(nick);
+    if (bot && IsService(bot)) {
+        reply("MCMSG_ALREADY_SERVICE", bot->nick);
+        return 0;
+    }
+    bot = AddService(nick, desc);
+    service_register(bot, '\0');
+    reply("MCMSG_NEW_SERVICE", bot->nick);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_service_rename) {
+    struct service *service;
+
+    if (!(service = service_find(argv[1]))) {
+        reply("MCMSG_UNKNOWN_SERVICE", argv[1]);
+        return 0;
+    }
+    NickChange(service->bot, argv[2], 0);
+    reply("MCMSG_SERVICE_RENAMED", service->bot->nick);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_service_trigger) {
+    struct userNode *bogon;
+    struct service *service;
+
+    if (!(service = service_find(argv[1]))) {
+        reply("MCMSG_UNKNOWN_SERVICE", argv[1]);
+        return 0;
+    }
+    if (argc < 3) {
+        if (service->trigger)
+            reply("MCMSG_CURRENT_TRIGGER", service->bot->nick, service->trigger);
+        else
+            reply("MCMSG_NO_TRIGGER", service->bot->nick);
+        return 1;
+    }
+    if (service->trigger)
+        reg_chanmsg_func(service->trigger, NULL, NULL);
+    if (!irccasecmp(argv[2], "none") || !irccasecmp(argv[2], "remove")) {
+        service->trigger = 0;
+        reply("MCMSG_REMOVED_TRIGGER", service->bot->nick);
+    } else if ((bogon = get_chanmsg_bot(argv[2][0]))) {
+        reply("MCMSG_DUPLICATE_TRIGGER", bogon->nick, argv[2][0]);
+        return 1;
+    } else {
+        service->trigger = argv[2][0];
+        reg_chanmsg_func(service->trigger, service->bot, modcmd_chanmsg);
+        reply("MCMSG_NEW_TRIGGER", service->bot->nick, service->trigger);
+    }
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_service_remove) {
+    char *name, *reason;
+    struct service *service;
+
+    name = argv[1];
+    if (argc > 2)
+        reason = unsplit_string(argv+2, argc-2, NULL);
+    else
+        reason = "Removing bot";
+    if (!(service = service_find(name))) {
+        reply("MCMSG_UNKNOWN_SERVICE", name);
+        return 0;
+    }
+    DelUser(service->bot, NULL, 1, reason);
+    reply("MCMSG_SERVICE_REMOVED", name);
+    dict_remove(services, name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_dump_messages) {
+    const char *fname = "strings.db";
+    struct saxdb_context *ctx;
+    dict_iterator_t it;
+    FILE *pf;
+
+    if (!(pf = fopen(fname, "w"))) {
+        reply("MCMSG_FILE_NOT_OPENED", fname);
+        return 0;
+    }
+    if (!(ctx = saxdb_open_context(pf))) {
+        reply("MSG_INTERNAL_FAILURE");
+        return 0;
+    }
+    for (it = dict_first(lang_C->messages); it; it = iter_next(it))
+        saxdb_write_string(ctx, iter_key(it), iter_data(it));
+    saxdb_close_context(ctx);
+    fclose(pf);
+    reply("MCMSG_MESSAGES_DUMPED", fname);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_version) {
+    reply("MCMSG_VERSION");
+    if ((argc > 1) && !irccasecmp(argv[1], "arch"))
+        send_message_type(4, user, cmd->parent->bot, "%s", ARCH_VERSION);
+    return 1;
+}
+
+
+void
+modcmd_nick_change(struct userNode *user, const char *old_nick) {
+    struct service *svc;
+    if (!(svc = dict_find(services, old_nick, NULL)))
+        return;
+    dict_remove2(services, old_nick, 1);
+    dict_insert(services, user->nick, svc);
+}
+
+void
+modcmd_cleanup(void) {
+    dict_delete(services);
+    dict_delete(modules);
+    if (suf_list)
+        free(suf_list);
+}
+
+static void
+modcmd_saxdb_write_command(struct saxdb_context *ctx, struct svccmd *cmd) {
+    char buf[MAXLEN];
+    unsigned int nn, len, pos;
+    struct svccmd *template = cmd->command->defaults;
+
+    saxdb_start_record(ctx, cmd->name, 0);
+    sprintf(buf, "%s.%s", cmd->command->parent->name, cmd->command->name);
+    saxdb_write_string(ctx, "command", buf);
+    if (cmd->alias.used)
+        saxdb_write_string_list(ctx, "aliased", &cmd->alias);
+    if (cmd->min_opserv_level != template->min_opserv_level)
+        saxdb_write_int(ctx, "oper_access", cmd->min_opserv_level);
+    if (cmd->min_channel_access != template->min_channel_access)
+        saxdb_write_int(ctx, "channel_access", cmd->min_channel_access);
+    if (cmd->flags != template->flags) {
+        if (cmd->flags) {
+            for (nn=pos=0; flags[nn].name; ++nn) {
+                if (cmd->flags & flags[nn].flag) {
+                    buf[pos++] = '+';
+                    len = strlen(flags[nn].name);
+                    memcpy(buf+pos, flags[nn].name, len);
+                    pos += len;
+                    buf[pos++] = ',';
+                }
+            }
+        } else {
+            pos = 1;
+        }
+        buf[--pos] = 0;
+        saxdb_write_string(ctx, "flags", buf);
+    }
+    if ((cmd->req_account_flags != template->req_account_flags)
+        || (cmd->deny_account_flags != template->req_account_flags)) {
+        pos = 0;
+        if (cmd->req_account_flags) {
+            buf[pos++] = '+';
+            for (nn=0; nn<32; nn++)
+                if (cmd->req_account_flags & (1 << nn))
+                    buf[pos++] = handle_flags[nn];
+        }
+        if (cmd->deny_account_flags) {
+            buf[pos++] = '-';
+            for (nn=0; nn<32; nn++)
+                if (cmd->deny_account_flags & (1 << nn))
+                    buf[pos++] = handle_flags[nn];
+        }
+        buf[pos] = 0;
+        saxdb_write_string(ctx, "account_flags", buf);
+    }
+    saxdb_end_record(ctx);
+}
+
+static int
+modcmd_saxdb_write(struct saxdb_context *ctx) {
+    struct string_list slist;
+    dict_iterator_t it, it2;
+    struct service *service;
+    unsigned int ii;
+
+    saxdb_start_record(ctx, "bots", 1);
+    for (it = dict_first(services); it; it = iter_next(it)) {
+        char buff[16];
+        service = iter_data(it);
+        saxdb_start_record(ctx, service->bot->nick, 1);
+        if (service->trigger) {
+            buff[0] = service->trigger;
+            buff[1] = '\0';
+            saxdb_write_string(ctx, "trigger", buff);
+        }
+        saxdb_write_string(ctx, "description", service->bot->info);
+        saxdb_end_record(ctx);
+    }
+    saxdb_end_record(ctx);
+
+    saxdb_start_record(ctx, "services", 1);
+    for (it = dict_first(services); it; it = iter_next(it)) {
+        service = iter_data(it);
+        saxdb_start_record(ctx, service->bot->nick, 1);
+        for (it2 = dict_first(service->commands); it2; it2 = iter_next(it2))
+            modcmd_saxdb_write_command(ctx, iter_data(it2));
+        saxdb_end_record(ctx);
+    }
+    saxdb_end_record(ctx);
+
+    saxdb_start_record(ctx, "helpfiles", 1);
+    slist.size = 0;
+    for (it = dict_first(services); it; it = iter_next(it)) {
+        service = iter_data(it);
+        slist.used = 0;
+        for (ii = 0; ii < service->modules.used; ++ii)
+            string_list_append(&slist, service->modules.list[ii]->name);
+        saxdb_write_string_list(ctx, iter_key(it), &slist);
+    }
+    if (slist.list)
+        free(slist.list);
+    saxdb_end_record(ctx);
+
+    return 0;
+}
+
+static int
+append_entry(const char *key, UNUSED_ARG(void *data), void *extra) {
+    struct helpfile_expansion *exp = extra;
+    int row = exp->value.table.length++;
+    exp->value.table.contents[row] = calloc(1, sizeof(char*));
+    exp->value.table.contents[row][0] = key;
+    return 0;
+}
+
+static struct helpfile_expansion
+modcmd_expand(const char *variable) {
+    struct helpfile_expansion exp;
+    extern struct userNode *message_source;
+    struct service *service;
+
+    service = dict_find(services, message_source->nick, NULL);
+    if (!irccasecmp(variable, "index")) {
+        exp.type = HF_TABLE;
+        exp.value.table.length = 1;
+        exp.value.table.width = 1;
+        exp.value.table.flags = TABLE_REPEAT_ROWS;
+        exp.value.table.contents = calloc(dict_size(service->commands)+1, sizeof(char**));
+        exp.value.table.contents[0] = calloc(1, sizeof(char*));
+        exp.value.table.contents[0][0] = "Commands:";
+        dict_foreach(service->commands, append_entry, &exp);
+        return exp;
+    } else if (!irccasecmp(variable, "languages")) {
+        struct string_buffer sbuf;
+        dict_iterator_t it;
+        sbuf.used = sbuf.size = 0;
+        sbuf.list = NULL;
+        for (it = dict_first(languages); it; it = iter_next(it)) {
+            string_buffer_append_string(&sbuf, iter_key(it));
+            string_buffer_append(&sbuf, ' ');
+        }
+        sbuf.list[--sbuf.used] = 0;
+        exp.type = HF_STRING;
+        exp.value.str = sbuf.list;
+        return exp;
+    }
+    exp.type = HF_STRING;
+    exp.value.str = NULL;
+    return exp;
+}
+
+static void
+modcmd_load_bots(struct dict *db) {
+    dict_iterator_t it;
+
+    for (it = dict_first(db); it; it = iter_next(it)) {
+        struct record_data *rd;
+        struct userNode *bot;
+        const char *nick, *desc;
+        char trigger;
+
+        rd = iter_data(it);
+        if (rd->type != RECDB_OBJECT) {
+            log_module(MAIN_LOG, LOG_ERROR, "Bad type for 'bots/%s' in modcmd db (expected object).", iter_key(it));
+            continue;
+        }
+        nick = database_get_data(rd->d.object, "nick", RECDB_QSTRING);
+        if (!nick)
+            nick = iter_key(it);
+        if (service_find(nick))
+            continue;
+        desc = database_get_data(rd->d.object, "trigger", RECDB_QSTRING);
+        trigger = desc ? desc[0] : '\0';
+        desc = database_get_data(rd->d.object, "description", RECDB_QSTRING);
+        if (desc)
+        {
+            bot = AddService(nick, desc);
+            service_register(bot, trigger);
+        }
+    }
+}
+
+static void
+modcmd_conf_read(void) {
+    modcmd_load_bots(conf_get_data("services", RECDB_OBJECT));
+}
+
+void
+modcmd_init(void) {
+    qsort(flags, ArrayLength(flags)-1, sizeof(flags[0]), flags_qsort);
+    modules = dict_new();
+    dict_set_free_data(modules, free_module);
+    services = dict_new();
+    dict_set_free_data(services, free_service);
+    reg_nick_change_func(modcmd_nick_change);
+    reg_exit_func(modcmd_cleanup);
+    conf_register_reload(modcmd_conf_read);
+
+    modcmd_module = module_register("modcmd", MAIN_LOG, "modcmd.help", modcmd_expand);
+    bind_command = modcmd_register(modcmd_module, "bind", cmd_bind, 4, MODCMD_KEEP_BOUND, "level", "800", NULL);
+    help_command = modcmd_register(modcmd_module, "help", cmd_help, 1, 0, "flags", "+nolog", NULL);
+    modcmd_register(modcmd_module, "command", cmd_command, 2, 0, "flags", "+nolog", NULL);
+    modcmd_register(modcmd_module, "modcmd", cmd_modcmd, 4, MODCMD_KEEP_BOUND, "template", "bind", NULL);
+    modcmd_register(modcmd_module, "god", cmd_god, 0, MODCMD_REQUIRE_AUTHED, "flags", "+oper,+networkhelper", NULL);
+    modcmd_register(modcmd_module, "readhelp", cmd_readhelp, 2, 0, "level", "650", NULL);
+    modcmd_register(modcmd_module, "timecmd", cmd_timecmd, 2, 0, "level", "1", NULL);
+    modcmd_register(modcmd_module, "unbind", cmd_unbind, 3, 0, "template", "bind", NULL);
+    modcmd_register(modcmd_module, "joiner", cmd_joiner, 1, 0, NULL);
+    modcmd_register(modcmd_module, "stats modules", cmd_stats_modules, 1, 0, "level", "0", NULL);
+    modcmd_register(modcmd_module, "stats services", cmd_stats_services, 1, 0, "level", "0", NULL);
+    modcmd_register(modcmd_module, "showcommands", cmd_showcommands, 1, 0, "flags", "+acceptchan", NULL);
+    modcmd_register(modcmd_module, "helpfiles", cmd_helpfiles, 2, 0, "template", "bind", NULL);
+    modcmd_register(modcmd_module, "service add", cmd_service_add, 3, 0, NULL);
+    modcmd_register(modcmd_module, "service rename", cmd_service_rename, 3, 0, NULL);
+    modcmd_register(modcmd_module, "service trigger", cmd_service_trigger, 2, 0, NULL);
+    modcmd_register(modcmd_module, "service remove", cmd_service_remove, 2, 0, NULL);
+    modcmd_register(modcmd_module, "dumpmessages", cmd_dump_messages, 1, 0, "oper_level", "1000", NULL);
+    modcmd_register(modcmd_module, "version", cmd_version, 1, 0, NULL);
+    message_register_table(msgtab);
+}
+
+static void
+modcmd_db_load_command(struct service *service, const char *cmdname, struct dict *obj) {
+    struct svccmd *svccmd;
+    struct module *module;
+    struct modcmd *modcmd;
+    struct string_list *slist;
+    const char *str, *sep;
+    char buf[MAXLEN];
+
+    str = database_get_data(obj, "command", RECDB_QSTRING);
+    if (!str) {
+        log_module(MAIN_LOG, LOG_ERROR, "Missing command for service %s command %s in modcmd.db", service->bot->nick, cmdname);
+        return;
+    }
+    sep = strchr(str, '.');
+    if (!sep) {
+        log_module(MAIN_LOG, LOG_ERROR, "Invalid command %s for service %s command %s in modcmd.db", str, service->bot->nick, cmdname);
+        return;
+    }
+    memcpy(buf, str, sep-str);
+    buf[sep-str] = 0;
+    if (!(module = module_find(buf))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unknown module %s for service %s command %s in modcmd.db", buf, service->bot->nick, cmdname);
+        return;
+    }
+    if (!(modcmd = dict_find(module->commands, sep+1, NULL))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unknown command %s in module %s for service %s command %s", sep+1, module->name, service->bot->nick, cmdname);
+        return;
+    }
+    /* Now that we know we have a command to use, fill in the basics. */
+    svccmd = calloc(1, sizeof(*svccmd));
+    svccmd_insert(service, strdup(cmdname), svccmd, modcmd);
+    if ((str = database_get_data(obj, "template", RECDB_QSTRING))) {
+        add_pending_template(svccmd, str);
+    } else {
+        svccmd_copy_rules(svccmd, modcmd->defaults);
+    }
+    if ((str = database_get_data(obj, "account_flags", RECDB_QSTRING))) {
+        svccmd->req_account_flags = svccmd->deny_account_flags = 0;
+        svccmd_configure(svccmd, NULL, service->bot, "account_flags", str);
+    }
+    if ((str = database_get_data(obj, "flags", RECDB_QSTRING))) {
+        svccmd->flags = 0;
+        svccmd_configure(svccmd, NULL, service->bot, "flags", str);
+    }
+    if ((str = database_get_data(obj, "oper_access", RECDB_QSTRING))
+        || (str = database_get_data(obj, "opserv_level", RECDB_QSTRING))) {
+        svccmd_configure(svccmd, NULL, service->bot, "oper_access", str);
+    }
+    if ((str = database_get_data(obj, "channel_access", RECDB_QSTRING))
+        || (str = database_get_data(obj, "chanserv_level", RECDB_QSTRING))) {
+        svccmd_configure(svccmd, NULL, service->bot, "channel_access", str);
+    }
+    if ((slist = database_get_data(obj, "aliased", RECDB_STRING_LIST))) {
+        unsigned int nn;
+        svccmd->alias.used = svccmd->alias.size = slist->used;
+        svccmd->alias.list = calloc(svccmd->alias.size, sizeof(svccmd->alias.list[0]));
+        for (nn=0; nn<slist->used; ++nn)
+            svccmd->alias.list[nn] = strdup(slist->list[nn]);
+    }
+    modcmd_set_effective_flags(svccmd);
+}
+
+static struct svccmd *
+service_make_alias(struct service *service, const char *alias, ...) {
+    char *arg, *argv[MAXNUMPARAMS];
+    unsigned int nn, argc;
+    struct svccmd *svccmd, *template;
+    va_list args;
+
+    va_start(args, alias);
+    argc = 0;
+    while (1) {
+        arg = va_arg(args, char*);
+        if (!arg)
+            break;
+        argv[argc++] = arg;
+    }
+    va_end(args);
+    svccmd = calloc(1, sizeof(*svccmd));
+    if (!(template = svccmd_resolve_name(svccmd, argv[0]))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Invalid base command %s for alias %s in service %s", argv[0], alias, service->bot->nick);
+        free(svccmd->name);
+        free(svccmd);
+        return NULL;
+    }
+    if (argc > 1) {
+        svccmd->alias.used = svccmd->alias.size = argc;
+        svccmd->alias.list = calloc(svccmd->alias.size, sizeof(svccmd->alias.list[0]));
+        for (nn=0; nn<argc; nn++)
+            svccmd->alias.list[nn] = strdup(argv[nn]);
+    }
+    svccmd_insert(service, strdup(alias), svccmd, template->command);
+    svccmd_copy_rules(svccmd, template);
+    return svccmd;
+}
+
+static int saxdb_present;
+
+static int
+modcmd_saxdb_read(struct dict *db) {
+    struct dict *db2;
+    dict_iterator_t it, it2;
+    struct record_data *rd, *rd2;
+    struct service *service;
+
+    modcmd_load_bots(database_get_data(db, "bots", RECDB_OBJECT));
+    db2 = database_get_data(db, "services", RECDB_OBJECT);
+    if (!db2) {
+        log_module(MAIN_LOG, LOG_ERROR, "Missing section 'services' in modcmd db.");
+        return 1;
+    }
+    for (it = dict_first(db2); it; it = iter_next(it)) {
+        rd = iter_data(it);
+        if (rd->type != RECDB_OBJECT) {
+            log_module(MAIN_LOG, LOG_ERROR, "Bad type for 'services/%s' in modcmd db (expected object).", iter_key(it));
+            continue;
+        }
+        if (!(service = service_find(iter_key(it)))) {
+            log_module(MAIN_LOG, LOG_ERROR, "Unknown service '%s' listed in modcmd db.", iter_key(it));
+            continue;
+        }
+        for (it2 = dict_first(rd->d.object); it2; it2 = iter_next(it2)) {
+            rd2 = iter_data(it2);
+            if (rd2->type != RECDB_OBJECT) {
+                log_module(MAIN_LOG, LOG_ERROR, "Bad type for 'services/%s/%s' in modcmd db (expected object).", iter_key(it), iter_key(it2));
+                continue;
+            }
+            modcmd_db_load_command(service, iter_key(it2), rd2->d.object);
+        }
+    }
+    db2 = database_get_data(db, "helpfiles", RECDB_OBJECT);
+    for (it = dict_first(db2); it; it = iter_next(it)) {
+        struct module *module;
+        struct string_list *slist;
+        unsigned int ii;
+
+        rd = iter_data(it);
+        if (rd->type != RECDB_STRING_LIST) {
+            log_module(MAIN_LOG, LOG_ERROR, "Bad type for 'helpfiles/%s' in modcmd db (expected string list).", iter_key(it));
+            continue;
+        }
+        slist = rd->d.slist;
+        if (!(service = service_find(iter_key(it)))) {
+            /* We probably whined about the service being missing above. */
+            continue;
+        }
+        service->modules.used = 0;
+        for (ii=0; ii<slist->used; ++ii) {
+            if (!(module = dict_find(modules, slist->list[ii], NULL))) {
+                log_module(MAIN_LOG, LOG_ERROR, "Unknown module '%s' listed in modcmd 'helpfiles/%s'.", slist->list[ii], iter_key(it));
+                continue;
+            }
+            module_list_append(&service->modules, module);
+        }
+    }
+    saxdb_present = 1;
+    return 0;
+}
+
+static void
+create_default_binds(void) {
+    /* Which services should import which modules by default? */
+    struct {
+        const char *svcname;
+        /* C is lame and requires a fixed size for this array.
+         * Be sure you NULL-terminate each array and increment the
+         * size here if you add more default modules to any
+         * service. */
+        const char *modnames[8];
+    } def_binds[] = {
+        { "ChanServ", { "ChanServ", NULL } },
+        { "Global", { "Global", NULL } },
+        { "NickServ", { "NickServ", NULL } },
+        { "OpServ", { "OpServ", "modcmd", "sendmail", "saxdb", "proxycheck", NULL } },
+        { NULL, { NULL } }
+    };
+    unsigned int ii, jj;
+    char buf[128], *nick;
+    struct service *service;
+    struct module *module;
+
+    for (ii = 0; def_binds[ii].svcname; ++ii) {
+        sprintf(buf, "services/%s/nick", def_binds[ii].svcname);
+        if (!(nick = conf_get_data(buf, RECDB_QSTRING)))
+            continue;
+        if (!(service = service_find(nick)))
+            continue;
+        if (dict_size(service->commands) > 0)
+            continue;
+        /* Bind the default modules for this service to it */
+        for (jj = 0; def_binds[ii].modnames[jj]; ++jj) {
+            if (!(module = module_find(def_binds[ii].modnames[jj])))
+                continue;
+            service_bind_module(service, module);
+        }
+        /* Bind the help command to this service */
+        service_bind_modcmd(service, help_command, help_command->name);
+        /* Now some silly hax.. (aliases that most people want) */
+        if (!irccasecmp(def_binds[ii].svcname, "ChanServ")) {
+            service_make_alias(service, "addowner", "*chanserv.adduser", "owner", "$1", NULL);
+            service_make_alias(service, "addcoowner", "*chanserv.adduser", "coowner", "$1", NULL);
+            service_make_alias(service, "addmaster", "*chanserv.adduser", "master", "$1", NULL);
+            service_make_alias(service, "addop", "*chanserv.adduser", "op", "$1", NULL);
+            service_make_alias(service, "addpeon", "*chanserv.adduser", "peon", "$1", NULL);
+            service_make_alias(service, "delowner", "*chanserv.deluser", "owner", "$1", NULL);
+            service_make_alias(service, "delcoowner", "*chanserv.deluser", "coowner", "$1", NULL);
+            service_make_alias(service, "delmaster", "*chanserv.deluser", "master", "$1", NULL);
+            service_make_alias(service, "delop", "*chanserv.deluser", "op", "$1", NULL);
+            service_make_alias(service, "delpeon", "*chanserv.deluser", "peon", "$1", NULL);
+            service_make_alias(service, "command", "*modcmd.command", NULL);
+            service_make_alias(service, "god", "*modcmd.god", NULL);
+        } else if (!irccasecmp(def_binds[ii].svcname, "OpServ")) {
+            struct svccmd *svccmd;
+            svccmd = service_make_alias(service, "stats", "*modcmd.joiner", NULL);
+            svccmd->min_opserv_level = 101;
+            svccmd = service_make_alias(service, "service", "*modcmd.joiner", NULL);
+            svccmd->min_opserv_level = 900;
+        }
+    }
+}
+
+static void
+import_aliases_db() {
+    struct dict *db;
+    dict_iterator_t it, it2;
+    struct record_data *rd, *rd2;
+    struct service *service;
+    struct module *module;
+
+    if (!(db = parse_database("aliases.db")))
+        return;
+    for (it = dict_first(db); it; it = iter_next(it)) {
+        service = service_find(iter_key(it));
+        if (!service)
+            continue;
+        module = module_find(service->bot->nick);
+        rd = iter_data(it);
+        if (rd->type != RECDB_OBJECT)
+            continue;
+        for (it2 = dict_first(rd->d.object); it2; it2 = iter_next(it2)) {
+            struct modcmd *command;
+            rd2 = iter_data(it2);
+            if (rd2->type != RECDB_QSTRING)
+                continue;
+            command = dict_find(module->commands, rd2->d.qstring, NULL);
+            if (!command)
+                continue;
+            service_bind_modcmd(service, command, iter_key(it2));
+        }
+    }
+}
+
+void
+modcmd_finalize(void) {
+    /* Check databases. */
+    saxdb_register("modcmd", modcmd_saxdb_read, modcmd_saxdb_write);
+    create_default_binds();
+    if (!saxdb_present)
+        import_aliases_db();
+
+    /* Resolve command rule-templates. */
+    while (pending_templates) {
+        struct pending_template *ptempl = pending_templates;
+        struct svccmd *svccmd;
+
+        pending_templates = ptempl->next;
+        /* Only overwrite the current template if we have a valid template. */
+        if (!strcmp(ptempl->base, "*")) {
+            /* Do nothing. */
+        } else if ((svccmd = svccmd_resolve_name(ptempl->cmd, ptempl->base))) {
+            svccmd_copy_rules(ptempl->cmd, svccmd);
+        } else {
+            assert(ptempl->cmd->parent);
+            log_module(MAIN_LOG, LOG_ERROR, "Unable to resolve template name %s for command %s in service %s.", ptempl->base, ptempl->cmd->name, ptempl->cmd->parent->bot->nick);
+        }
+        free(ptempl->base);
+        free(ptempl);
+    }
+}
diff --git a/src/modcmd.h b/src/modcmd.h
new file mode 100644 (file)
index 0000000..599a7b6
--- /dev/null
@@ -0,0 +1,188 @@
+/* modcmd.h - Generalized module command support
+ * Copyright 2002-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#if !defined(MODCMDS_H)
+#define MODCMDS_H
+
+#include "recdb.h"
+#include "helpfile.h"
+#include "log.h"
+
+struct service;
+struct svccmd;
+struct module;
+struct modcmd;
+
+#define MODCMD_FUNC(NAME) int NAME(struct userNode *user, UNUSED_ARG(struct chanNode *channel), UNUSED_ARG(unsigned int argc), UNUSED_ARG(char **argv), UNUSED_ARG(struct svccmd *cmd))
+typedef MODCMD_FUNC(modcmd_func_t);
+#define SVCMSG_HOOK(NAME) int NAME(struct userNode *user, struct userNode *target, char *text, int server_qualified);
+typedef SVCMSG_HOOK(svcmsg_hook_t);
+
+DECLARE_LIST(svccmd_list, struct svccmd*);
+DECLARE_LIST(module_list, struct module*);
+
+#if defined(__GNUC__) && (__GNUC__ < 3)
+#define reply(FMT...) send_message(user, cmd->parent->bot, FMT)
+#elif !defined(S_SPLINT_S) /* doesn't recognize C99 variadic macros */
+#define reply(...) send_message(user, cmd->parent->bot, __VA_ARGS__)
+#endif
+#define modcmd_get_handle_info(USER, NAME) smart_get_handle_info(cmd->parent->bot, USER, NAME)
+#define modcmd_chanmode_announce(CHANGE) mod_chanmode_announce(cmd->parent->bot, channel, CHANGE)
+#define modcmd_chanmode(ARGV, ARGC, FLAGS) mod_chanmode(cmd->parent->bot, channel, ARGV, ARGC, FLAGS)
+
+/* Miscellaneous flags controlling a command */
+#define MODCMD_DISABLED                  0x001
+#define MODCMD_NO_LOG                    0x002
+#define MODCMD_KEEP_BOUND                0x004
+#define MODCMD_ACCEPT_CHANNEL            0x008
+#define MODCMD_ACCEPT_PCHANNEL           0x010
+#define MODCMD_NO_DEFAULT_BIND           0x020
+#define MODCMD_LOG_HOSTMASK              0x040
+#define MODCMD_IGNORE_CSUSPEND           0x080
+#define MODCMD_NEVER_CSUSPEND            0x100
+/* Requirement (access control) flags */
+#define MODCMD_REQUIRE_AUTHED         0x001000
+#define MODCMD_REQUIRE_CHANNEL        0x002000
+#define MODCMD_REQUIRE_REGCHAN        0x004000
+#define MODCMD_REQUIRE_CHANUSER       0x008000
+#define MODCMD_REQUIRE_JOINABLE       0x010000
+#define MODCMD_REQUIRE_QUALIFIED      0x020000
+#define MODCMD_REQUIRE_OPER           0x040000
+#define MODCMD_REQUIRE_NETWORK_HELPER 0x080000
+#define MODCMD_REQUIRE_SUPPORT_HELPER 0x100000
+#define MODCMD_REQUIRE_HELPING        0x200000
+#define MODCMD_TOY                    0x400000
+#define MODCMD_REQUIRE_STAFF          (MODCMD_REQUIRE_OPER|MODCMD_REQUIRE_NETWORK_HELPER|MODCMD_REQUIRE_SUPPORT_HELPER)
+
+#define SVCCMD_QUALIFIED              0x000001
+#define SVCCMD_DEBIT                  0x000002
+#define SVCCMD_NOISY                  0x000004
+
+/* Modularized commands work like this:
+ *
+ * Modules define commands.  Services contain "bindings" of those
+ * commands to names.
+ *
+ * The module-defined commands (modcmd structs) contain the parameters
+ * fixed by code; for example, assuming a channel was provided, or
+ * that the user has ChanServ access to that channel.
+ *
+ * Service command bindings (svccmd structs) define additional access
+ * controls (and a count of how many times the command has been used)
+ * as well as a link to the modcmd providing the function.
+ *
+ * Aliased commands are special svccmds that have alias expansion
+ * information in an "extra" pointer.  In the future, this may be
+ * moved into the svccmd struct if there are no other commands that
+ * need "extra" data.
+ *
+ * The user must meet all the requirements (in flags, access levels,
+ * etc.) before the command is executed.  As an exception, for the
+ * "staff" permission checks (oper/network helper/support helper), any
+ * one is sufficient to permit the command usage.
+ */
+
+struct service {
+    struct userNode *bot;
+    struct module_list modules;
+    struct dict *commands; /* contains struct svccmd* */
+    svcmsg_hook_t *msg_hook;
+    unsigned int privileged : 1;
+    char trigger;
+};
+
+struct svccmd {
+    char *name;
+    struct service *parent; /* where is this command bound? */
+    struct modcmd *command; /* what is the implementation? */
+    struct string_list alias; /* if it's a complicated binding, what is the expansion? */
+    unsigned int uses; /* how many times was this command used? */
+    unsigned int flags;
+    unsigned long req_account_flags;
+    unsigned long deny_account_flags;
+    unsigned int min_opserv_level;
+    unsigned int min_channel_access;
+    unsigned int effective_flags;
+};
+
+struct module {
+    char *name;                /* name of module */
+    struct dict *commands;     /* contains struct modcmd* */
+    struct log_type *clog;     /* where to send logged commands */
+    const char *helpfile_name; /* name to use for helpfile */
+    expand_func_t expand_help; /* expander function for helpfile */
+    struct helpfile *helpfile; /* help file to use in case of syntax error */
+};
+
+struct modcmd {
+    char *name;
+    struct module *parent;
+    modcmd_func_t *func;
+    struct svccmd *defaults;
+    unsigned int min_argc;
+    unsigned int flags;
+    unsigned int bind_count;
+};
+
+/* Register a command.  The varadic argument section consists of a set
+ * of name/value pairs, where the name and value are strings that give
+ * the default parameters for the command.  (The "flags" argument
+ * gives the required parameters.)  The set is ended by a null name
+ * pointer (without any value argument).
+ */
+struct modcmd *modcmd_register(struct module *module, const char *name, modcmd_func_t func, unsigned int min_argc, unsigned int flags, ...);
+
+/* Register a command-providing module.  clog is where to log loggable
+ * commands (those without the MODCMD_NO_LOG flag and which succeed).
+ */
+struct module *module_register(const char *name, struct log_type *clog, const char *helpfile_name, expand_func_t expand_help);
+/* Find a module by name.  Returns NULL if no such module is registered. */
+struct module *module_find(const char *name);
+
+/* Register a command-using service. */
+struct service *service_register(struct userNode *bot, char trigger);
+/* Find a service by name. */
+struct service *service_find(const char *name);
+/* Bind one command to a service. */
+struct svccmd *service_bind_modcmd(struct service *service, struct modcmd *cmd, const char *name);
+
+/* Send help for a command to a user. */
+int svccmd_send_help(struct userNode *user, struct userNode *bot, struct svccmd *cmd);
+/* .. and if somebody doesn't have a modcmd handy .. */
+int svccmd_send_help_2(struct userNode *user, struct service *service, const char *topic);
+/* Check whether a user may invoke a command or not.  If they can,
+ * return non-zero.  If they cannot (and noisy is non-zero), tell them
+ * why not and return 0.
+ */
+int svccmd_can_invoke(struct userNode *user, struct userNode *bot, struct svccmd *cmd, struct chanNode *channel, int flags);
+/* Execute a command.  Returns non-zero on success. */
+int svccmd_invoke_argv(struct userNode *user, struct service *service, struct chanNode *channel, unsigned int argc, char *argv[], unsigned int server_qualified);
+/* Get notification when a command is being unbound.  This lets
+ * services which cache svccmd references remove them.
+ */
+typedef void (*svccmd_unbind_func_t)(struct svccmd *target);
+void reg_svccmd_unbind_func(svccmd_unbind_func_t handler);
+
+/* Initialize the module command subsystem. */
+void modcmd_init(void);
+/* Finalize the command mappings, read aliases, etc.  Do this after
+ * all other modules have registered their commands.
+ */
+void modcmd_finalize(void);
+
+#endif /* !defined(MODCMDS_H) */
diff --git a/src/modcmd.help b/src/modcmd.help
new file mode 100644 (file)
index 0000000..e3da2aa
--- /dev/null
@@ -0,0 +1,94 @@
+"bind" ("/msg $S BIND <service> <bindname> <command> [additional args..]",
+        "Binds (adds) a command to an existing service.  $bbindname$b is the name of the new command for the service.  $bcommand$b may be one of:",
+        "  CommandName              To refer to a command on the same service.",
+        "  ServiceNick.CommandName  To refer to a command bound on a different service.",
+        "  *ModuleName.CommandName  To bind a command directly from a module (note the asterisk before the module name).",
+        "  *ModuleName.*            To bind all commands from the named module.",
+        "For simplicity, you cannot bind to a command that is itself an alias.  Certain commands will not bound with the last form; you must bind them by name.",
+        "(A command binding is very similar to an alias, but only pays the speed penalty for alias expansion when there are additional arguments in the binding.)",
+        "$uSee also:$u unbind");
+"commands" "${index}";
+"god" ("/msg $C GOD [on|off]",
+        "Toggles security override, which grants you complete access to all channels. Please use carefully.");
+"help" ("/msg $S HELP [command]",
+        "Help will show you the information for the given command.",
+        "All help files will use the same syntax, with optional parameters listed in [] and required parameters listed in <>.",
+        "To see what commands are available for use with $S, type /msg $S help index. For help on any specific command or topic, type /msg $S help help command.");
+"readhelp" ("/msg $S READHELP <module>",
+        "Re-reads the module's help file from disk.",
+        "$uSee Also:$u help");
+"unbind" ("/msg $S UNBIND <service> <command>",
+        "Unbinds a command from a service, so that it is no longer accessible.",
+        "Some commands (such as $bauth$b and $bbind$b) will not let you unbind their last binding -- for obvious reasons.",
+        "$uSee Also:$u bind");
+"timecmd" ("/msg $S TIMECMD <command and args>",
+        "Reports how long it takes to run the specified command.");
+"command" ("/msg $S COMMAND <command>", 
+        "Shows the restrictions on who can use the named command (and how).");
+"modcmd" ("/msg $S MODCMD <command> <option> <newval> [additional option/value pairs..]",
+        "Changes the named option(s) for the specified command.  The command name may be prefixed with $bServiceNick.$b to specify another service's command (for example, $N.AUTH to refer to the auth command).",
+        "Supported options are:",
+        "  FLAGS          Comma-separated, +/- prefixed list of flags to add or remove.",
+        "  CHANNEL_ACCESS Minimum ChanServ access.",
+        "  OPER_ACCESS    Minimum OpServ access.",
+        "  ACCESS         Alias for OPER_ACCESS (if the value starts with a digit) or CHANNEL_ACCESS (if the value does not start with a digit).",
+        "  ACCOUNT_FLAGS  Account flags to require or deny (for example, +R-S)",
+        "  TEMPLATE       Command from which to inherit access restrictions",
+        "  WEIGHT         How much the command counts against the anti-flood policer",
+        "See the $bmodcmd flags$b entry for a list of supported flags.");
+"modcmd flags" ("The following flags are supported for commands:",
+        "  ACCEPTCHAN     Treat a normal channel name (if specified) as the context for the command",
+        "  ACCEPTPLUSCHAN Accept modeless channel names as well as normal channel names",
+        "  AUTHED         Require that the user be authed to use the command",
+        "  CHANNEL        Require that an existing channel be given as the context",
+        "  CHANUSER       Require that the user have $C access to the context",
+        "  DISABLED       Command is disabled",
+        "  HELPING        Require that the user have security override enabled",
+        "  IGNORE_CSUSPEND  Allow a +REGCHAN command to work even if the channel is suspended",
+        "  JOINABLE       Require that the user have $C access to the channel, be in the channel, or be network staff to use the command",
+        "  KEEPBOUND      Do not let the last instance of the command be removed",
+        "  LOGHOSTMASK    Log the user's ident, hostname and IP (as well as nick and account name)",
+        "  NOLOG          Do not log the command at all",
+        "  NETWORKHELPER  Allow network helpers to use the command",
+        "  OPER           Allow opers to use the command",
+        "  QUALIFIED      Require $b/msg Service@$s$b addressing when using the command",
+        "  REGCHAN        Require a registered channel to be give as the context",
+        "  SUPPORTHELPER  Allow support helpers to use the command",
+        "  TOY            Command is a toy (cannot be invoked on a channel from outside the channel)",
+        "Note: If any of SUPPORTHELPER, NETWORKHELPER, OPER, any of the specified flags is considered sufficient.  For example, NETWORKHELPER and OPER both specified means both network helpers and opers can use the command.");
+"joiner" ("/msg $S JOINER [subcmd ...]",
+        "Magically looks up subcommands and redirects to them.  Use the command by itself to see what subcommands are known.");
+"stats modules" ("/msg $S STATS MODULES [modulename]",
+        "With no module name argument, shows a list of loaded modules and brief statistics for each.",
+        "When a module name is given, shows commands exported by that module.",
+        "$uSee Also:$u stats services, command, modcmd, bind");
+"stats services" ("/msg $S STATS SERVICES [botnick]",
+        "With no bot nick argument, shows a list of the service bots using the unified command framework, and brief statistics for each.",
+        "When a bot nick is given, shows commands bound to that service.",
+        "$uSee Also:$u stats modules, command, modcmd, bind, unbind");
+"showcommands" ("/msg $S SHOWCOMMANDS [opserv-access] [channel-access]",
+        "Shows commands which you can execute (with their required access levels).  If you give a numeric $O access level or text $C access name, it further restricts output to only show commands which can be executed by users with that access.",
+        "$uSee Also:$u command");
+"helpfiles" ("/msg $S HELPFILES <service> [module list]",
+        "With only a service nick, shows the helpfiles used by that service.",
+        "With a list of modules, sets the order that the service will look up non-command help entries.",
+        "$uSee Also:$u bind, unbind");
+"service add" ("/msg $S SERVICE ADD <nick> <description>",
+        "Creates a new service bot.",
+        "$uSee Also:$u service rename, service trigger, service remove");
+"service rename" ("/msg $S SERVICE RENAME <oldnick> <newnick>",
+        "Renames a service bot.  Currently does not support the default services.",
+        "$uSee Also:$u service add, service trigger, service remove");
+"service trigger" ("/msg $S SERVICE TRIGGER <nick> [remove|<trigger>]",
+        "Sets or clears the trigger character that the named service uses to identify in-channel messages directed at it.",
+        "To remove a trigger, use the word $bremove$b (or $bnone$b) as the argument.  Otherwise, the first letter of the argument will be used as the new trigger.",
+        "If no argument is given, shows the current trigger for the service.",
+        "$uSee Also:$u service add, service rename, service remove");
+"service remove" ("/msg $S SERVICE REMOVE <nick> [<quit reason>]",
+        "Destroys a service.  If a default service is named, it will be recreated when srvx restarts.",
+        "$uSee Also:$u service add, service rename, service trigger");
+"set language" ("/msg $S set language <langname>",
+        "Sets the language that $S and other services will use when sending you messages.",
+        "If a particular message is not available in your language, it will use the closest match it has.",
+        "Supported languages are: ${languages}",
+        "$uSee Also:$u set");
diff --git a/src/modules.c b/src/modules.c
new file mode 100644 (file)
index 0000000..1794fa8
--- /dev/null
@@ -0,0 +1,135 @@
+/* modules.c - Compiled-in module support
+ * Copyright 2002-2003 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "log.h"
+#include "modules.h"
+
+enum init_state {
+    UNINIT,
+    WORKING,
+    BROKEN,
+    INITED,
+    DONE
+};
+
+struct cmodule {
+    const char *name;
+    int (*init_func)(void);
+    int (*finalize_func)(void);
+    const char **deps;
+    enum init_state state;
+};
+
+#define WITH_MODULE(x) extern int x##_init(void); extern int x##_finalize(void); extern const char *x##_module_deps[];
+#include "modules-list.h"
+#undef WITH_MODULE
+
+static struct cmodule cmodules[] = {
+#define WITH_MODULE(x) { #x, x##_init, x##_finalize, x##_module_deps, UNINIT },
+#include "modules-list.h"
+#undef WITH_MODULE
+    /* Placeholder at end of array */
+    { NULL, NULL, NULL, NULL, UNINIT }
+};
+
+static int
+modules_bsearch(const void *a, const void *b) {
+    const char *key = a;
+    const struct cmodule *cmod = b;
+    return irccasecmp(key, cmod->name);
+}
+
+static int
+modules_qsort(const void *a, const void *b) {
+    const struct cmodule *ca = a, *cb = b;
+    return irccasecmp(ca->name, cb->name);
+}
+
+static int
+module_init(struct cmodule *cmod, int final) {
+    unsigned int ii;
+    struct cmodule *dep;
+
+    switch (cmod->state) {
+    case UNINIT: break;
+    case INITED: if (!final) return 1; break;
+    case DONE:   return 1;
+    case BROKEN: return 0;
+    case WORKING:
+        log_module(MAIN_LOG, LOG_ERROR, "Tried to recursively enable code module %s.", cmod->name);
+        return 0;
+    }
+    cmod->state = WORKING;
+    for (ii=0; cmod->deps[ii]; ++ii) {
+        dep = bsearch(cmod->deps[ii], cmodules, ArrayLength(cmodules)-1, sizeof(cmodules[0]), modules_bsearch);
+        if (!dep) {
+            log_module(MAIN_LOG, LOG_ERROR, "Code module %s depends on unknown module %s.", cmod->name, cmod->deps[ii]);
+            cmod->state = BROKEN;
+            return 0;
+        }
+        if (!module_init(dep, final)) {
+            log_module(MAIN_LOG, LOG_ERROR, "Failed to initialize dependency %s of code module %s.", dep->name, cmod->name);
+            cmod->state = BROKEN;
+            return 0;
+        }
+    }
+    if (final) {
+        if (!cmod->finalize_func()) {
+            log_module(MAIN_LOG, LOG_ERROR, "Failed to finalize code module %s.", cmod->name);
+            cmod->state = BROKEN;
+            return 0;
+        }
+        cmod->state = DONE;
+        return 1;
+    } else {
+        if (!cmod->init_func()) {
+            log_module(MAIN_LOG, LOG_ERROR, "Failed to initialize code module %s.", cmod->name);
+            cmod->state = BROKEN;
+            return 0;
+        }
+        cmod->state = INITED;
+        return 1;
+    }
+}
+
+void
+modules_init(void) {
+    unsigned int ii;
+
+    qsort(cmodules, ArrayLength(cmodules)-1, sizeof(cmodules[0]), modules_qsort);
+    for (ii=0; cmodules[ii].name; ++ii) {
+        if (cmodules[ii].state != UNINIT) continue;
+        module_init(cmodules + ii, 0);
+        if (cmodules[ii].state != INITED) {
+            log_module(MAIN_LOG, LOG_WARNING, "Code module %s not properly initialized.", cmodules[ii].name);
+        }
+    }
+}
+
+void
+modules_finalize(void) {
+    unsigned int ii;
+
+    for (ii=0; cmodules[ii].name; ++ii) {
+        if (cmodules[ii].state != INITED) continue;
+        module_init(cmodules + ii, 1);
+        if (cmodules[ii].state != DONE) {
+            log_module(MAIN_LOG, LOG_WARNING, "Code module %s not properly finalized.", cmodules[ii].name);
+        }
+    }
+}
diff --git a/src/modules.h b/src/modules.h
new file mode 100644 (file)
index 0000000..a5770ac
--- /dev/null
@@ -0,0 +1,25 @@
+/* modules.h - Compiled-in module support
+ * Copyright 2002 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#if !defined(MODULES_H)
+#define MODULES_H
+
+void modules_init(void);
+void modules_finalize(void);
+
+#endif
diff --git a/src/nickserv.c b/src/nickserv.c
new file mode 100644 (file)
index 0000000..e62f737
--- /dev/null
@@ -0,0 +1,3671 @@
+/* nickserv.c - Nick/authentication service
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "chanserv.h"
+#include "conf.h"
+#include "global.h"
+#include "modcmd.h"
+#include "opserv.h" /* for gag_create(), opserv_bad_channel() */
+#include "saxdb.h"
+#include "sendmail.h"
+#include "timeq.h"
+
+#ifdef HAVE_REGEX_H
+#include <regex.h>
+#endif
+
+#define NICKSERV_CONF_NAME "services/nickserv"
+
+#define KEY_DISABLE_NICKS "disable_nicks"
+#define KEY_DEFAULT_HOSTMASK "default_hostmask"
+#define KEY_NICKS_PER_HANDLE "nicks_per_handle"
+#define KEY_NICKS_PER_ACCOUNT "nicks_per_account"
+#define KEY_PASSWORD_MIN_LENGTH "password_min_length"
+#define KEY_PASSWORD_MIN_DIGITS "password_min_digits"
+#define KEY_PASSWORD_MIN_UPPER "password_min_upper"
+#define KEY_PASSWORD_MIN_LOWER "password_min_lower"
+#define KEY_VALID_HANDLE_REGEX "valid_handle_regex"
+#define KEY_VALID_ACCOUNT_REGEX "valid_account_regex"
+#define KEY_VALID_NICK_REGEX "valid_nick_regex"
+#define KEY_DB_BACKUP_FREQ "db_backup_freq"
+#define KEY_MODOPER_LEVEL "modoper_level"
+#define KEY_SET_EPITHET_LEVEL "set_epithet_level"
+#define KEY_FLAG_LEVELS "flag_levels"
+#define KEY_HANDLE_EXPIRE_FREQ "handle_expire_freq"
+#define KEY_ACCOUNT_EXPIRE_FREQ "account_expire_freq"
+#define KEY_HANDLE_EXPIRE_DELAY        "handle_expire_delay"
+#define KEY_ACCOUNT_EXPIRE_DELAY "account_expire_delay"
+#define KEY_NOCHAN_HANDLE_EXPIRE_DELAY "nochan_handle_expire_delay"
+#define KEY_NOCHAN_ACCOUNT_EXPIRE_DELAY "nochan_account_expire_delay"
+#define KEY_DICT_FILE "dict_file"
+#define KEY_NICK "nick"
+#define KEY_LANGUAGE "language"
+#define KEY_AUTOGAG_ENABLED "autogag_enabled"
+#define KEY_AUTOGAG_DURATION "autogag_duration"
+#define KEY_AUTH_POLICER "auth_policer"
+#define KEY_EMAIL_VISIBLE_LEVEL "email_visible_level"
+#define KEY_EMAIL_ENABLED "email_enabled"
+#define KEY_EMAIL_REQUIRED "email_required"
+#define KEY_COOKIE_TIMEOUT "cookie_timeout"
+#define KEY_ACCOUNTS_PER_EMAIL "accounts_per_email"
+#define KEY_EMAIL_SEARCH_LEVEL "email_search_level"
+
+#define KEY_ID "id"
+#define KEY_PASSWD "passwd"
+#define KEY_NICKS "nicks"
+#define KEY_MASKS "masks"
+#define KEY_OPSERV_LEVEL "opserv_level"
+#define KEY_FLAGS "flags"
+#define KEY_REGISTER_ON "register"
+#define KEY_LAST_SEEN "lastseen"
+#define KEY_INFO "info"
+#define KEY_USERLIST_STYLE "user_style"
+#define KEY_SCREEN_WIDTH "screen_width"
+#define KEY_LAST_AUTHED_HOST "last_authed_host"
+#define KEY_LAST_QUIT_HOST "last_quit_host"
+#define KEY_EMAIL_ADDR "email_addr"
+#define KEY_COOKIE "cookie"
+#define KEY_COOKIE_DATA "data"
+#define KEY_COOKIE_TYPE "type"
+#define KEY_COOKIE_EXPIRES "expires"
+#define KEY_ACTIVATION "activation"
+#define KEY_PASSWORD_CHANGE "password change"
+#define KEY_EMAIL_CHANGE "email change"
+#define KEY_ALLOWAUTH "allowauth"
+#define KEY_EPITHET "epithet"
+#define KEY_TABLE_WIDTH "table_width"
+#define KEY_ANNOUNCEMENTS "announcements"
+#define KEY_MAXLOGINS "maxlogins"
+
+#define NICKSERV_VALID_CHARS   "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
+
+#define NICKSERV_FUNC(NAME) MODCMD_FUNC(NAME)
+#define OPTION_FUNC(NAME) int NAME(struct userNode *user, struct handle_info *hi, UNUSED_ARG(unsigned int override), unsigned int argc, char *argv[])
+typedef OPTION_FUNC(option_func_t);
+
+DEFINE_LIST(handle_info_list, struct handle_info*);
+
+#define NICKSERV_MIN_PARMS(N) do { \
+  if (argc < N) { \
+    reply("MSG_MISSING_PARAMS", argv[0]); \
+    svccmd_send_help(user, nickserv, cmd); \
+    return 0; \
+  } } while (0)
+
+struct userNode *nickserv;
+struct userList curr_helpers;
+const char *handle_flags = HANDLE_FLAGS;
+
+static struct module *nickserv_module;
+static struct service *nickserv_service;
+static struct log_type *NS_LOG;
+static dict_t nickserv_handle_dict; /* contains struct handle_info* */
+static dict_t nickserv_id_dict; /* contains struct handle_info* */
+static dict_t nickserv_nick_dict; /* contains struct nick_info* */
+static dict_t nickserv_opt_dict; /* contains option_func_t* */
+static dict_t nickserv_allow_auth_dict; /* contains struct handle_info* */
+static dict_t nickserv_email_dict; /* contains struct handle_info_list*, indexed by email addr */
+static char handle_inverse_flags[256];
+static unsigned int flag_access_levels[32];
+static const struct message_entry msgtab[] = {
+    { "NSMSG_HANDLE_EXISTS", "Account $b%s$b is already registered." },
+    { "NSMSG_PASSWORD_SHORT", "Your password must be at least %lu characters long." },
+    { "NSMSG_PASSWORD_ACCOUNT", "Your password may not be the same as your account name." },
+    { "NSMSG_PASSWORD_DICTIONARY", "Your password should not be the word \"password\", or any other dictionary word." },
+    { "NSMSG_PASSWORD_READABLE", "Your password must have at least %lu digit(s), %lu capital letter(s), and %lu lower-case letter(s)." },
+    { "NSMSG_PARTIAL_REGISTER", "Account has been registered to you; nick was already registered to someone else." },
+    { "NSMSG_OREGISTER_VICTIM", "%s has registered a new account for you (named %s)." },
+    { "NSMSG_OREGISTER_H_SUCCESS", "Account has been registered." },
+    { "NSMSG_REGISTER_H_SUCCESS", "Account has been registered to you." },
+    { "NSMSG_REGISTER_HN_SUCCESS", "Account and nick have been registered to you." },
+    { "NSMSG_REQUIRE_OPER", "You must be an $bIRC Operator$b to register the first account." },
+    { "NSMSG_ROOT_HANDLE", "Account %s has been granted $broot-level privileges$b." },
+    { "NSMSG_USE_COOKIE_REGISTER", "To activate your account, you must check your email for the \"cookie\" that has been mailed to it.  When you have it, use the $bcookie$b command to complete registration." },
+    { "NSMSG_USE_COOKIE_RESETPASS", "A cookie has been mailed to your account's email address.  You must check your email and use the $bcookie$b command to confirm the password change." },
+    { "NSMSG_USE_COOKIE_EMAIL_1", "A cookie has been mailed to the new address you requested.  To finish setting your email address, please check your email for the cookie and use the $bcookie$b command to verify." },
+    { "NSMSG_USE_COOKIE_EMAIL_2", "A cookie has been generated, and half mailed to each your old and new addresses.  To finish changing your email address, please check your email for the cookie and use the $bcookie$b command to verify." },
+    { "NSMSG_USE_COOKIE_AUTH", "A cookie has been generated and sent to your email address.  Once you have checked your email and received the cookie, auth using the $bcookie$b command." },
+    { "NSMSG_COOKIE_LIVE", "Account $b%s$b already has a cookie active.  Please either finish using that cookie, wait for it to expire, or auth to the account and use the $bdelcookie$b command." },
+    { "NSMSG_EMAIL_UNACTIVATED", "That email address already has an unused cookie outstanding.  Please use the cookie or wait for it to expire." },
+    { "NSMSG_NO_COOKIE", "Your account does not have any cookie issued right now." },
+    { "NSMSG_CANNOT_COOKIE", "You cannot use that kind of cookie when you are logged in." },
+    { "NSMSG_BAD_COOKIE", "That cookie is not the right one.  Please make sure you are copying it EXACTLY from the email; it is case-sensitive, so $bABC$b is different from $babc$b." },
+    { "NSMSG_HANDLE_ACTIVATED", "Your account is now activated (with the password you entered when you registered).  You are now authenticated to your account." },
+    { "NSMSG_PASSWORD_CHANGED", "You have successfully changed your password to what you requested with the $bresetpass$b command." },
+    { "NSMSG_EMAIL_PROHIBITED", "%s may not be used as an email address: %s" },
+    { "NSMSG_EMAIL_OVERUSED", "There are already the maximum number of accounts associated with that email address." },
+    { "NSMSG_EMAIL_SAME", "That is the email address already there; no need to change it." },
+    { "NSMSG_EMAIL_CHANGED", "You have successfully changed your email address." },
+    { "NSMSG_BAD_COOKIE_TYPE", "Your account had bad cookie type %d; sorry.  I am confused.  Please report this bug." },
+    { "NSMSG_MUST_TIME_OUT", "You must wait for cookies of that type to time out." },
+    { "NSMSG_ATE_COOKIE", "I ate the cookie for your account.  You may now have another." },
+    { "NSMSG_USE_RENAME", "You are already authenticated to account $b%s$b -- contact the support staff to rename your account." },
+    { "NSMSG_REGISTER_BAD_NICKMASK", "Could not recognize $b%s$b as either a current nick or a hostmask." },
+    { "NSMSG_NICK_NOT_REGISTERED", "Nick $b%s$b has not been registered to any account." },
+    { "NSMSG_HANDLE_NOT_FOUND", "Could not find your account -- did you register yet?" },
+    { "NSMSG_ALREADY_AUTHED", "You are already authed to account $b%s$b; you must reconnect to auth to a different account." },
+    { "NSMSG_USE_AUTHCOOKIE", "Your hostmask is not valid for account $b%s$b.  Please use the $bauthcookie$b command to grant yourself access.  (/msg $S authcookie %s)" },
+    { "NSMSG_HOSTMASK_INVALID", "Your hostmask is not valid for account $b%s$b." },
+    { "NSMSG_USER_IS_SERVICE", "$b%s$b is a network service; you can only use that command on real users." },
+    { "NSMSG_USER_PREV_AUTH", "$b%s$b is already authenticated." },
+    { "NSMSG_USER_PREV_STAMP", "$b%s$b has authenticated to an account once and cannot authenticate again." },
+    { "NSMSG_BAD_MAX_LOGINS", "MaxLogins must be at most %d." },
+    { "NSMSG_LANGUAGE_NOT_FOUND", "Language $b%s$b is not supported; $b%s$b was the closest available match." },
+    { "NSMSG_MAX_LOGINS", "Your account already has its limit of %d user(s) logged in." },
+    { "NSMSG_STAMPED_REGISTER", "You have already authenticated to an account once this session; you may not register a new account." },
+    { "NSMSG_STAMPED_AUTH", "You have already authenticated to an account once this session; you may not authenticate to another." },
+    { "NSMSG_STAMPED_RESETPASS", "You have already authenticated to an account once this session; you may not reset your password to authenticate again." },
+    { "NSMSG_STAMPED_AUTHCOOKIE",  "You have already authenticated to an account once this session; you may not use a cookie to authenticate to another account." },
+    { "NSMSG_HANDLEINFO_ON", "Account information for $b%s$b:" },
+    { "NSMSG_HANDLEINFO_ID", "  Account ID: %lu" },
+    { "NSMSG_HANDLEINFO_REGGED", "  Registered on: %s" },
+    { "NSMSG_HANDLEINFO_LASTSEEN", "  Last seen: %s" },
+    { "NSMSG_HANDLEINFO_LASTSEEN_NOW", "  Last seen: Right now!" },
+    { "NSMSG_HANDLEINFO_VACATION", "  On vacation." },
+    { "NSMSG_HANDLEINFO_EMAIL_ADDR", "  Email address: %s" },
+    { "NSMSG_HANDLEINFO_COOKIE_ACTIVATION", "  Cookie: There is currently an activation cookie issued for this account" },
+    { "NSMSG_HANDLEINFO_COOKIE_PASSWORD", "  Cookie: There is currently a password change cookie issued for this account" },
+    { "NSMSG_HANDLEINFO_COOKIE_EMAIL", "  Cookie: There is currently an email change cookie issued for this account" },
+    { "NSMSG_HANDLEINFO_COOKIE_ALLOWAUTH", "  Cookie: There is currently an allowauth cookie issued for this account" },
+    { "NSMSG_HANDLEINFO_COOKIE_UNKNOWN", "  Cookie: There is currently an unknown cookie issued for this account" },
+    { "NSMSG_HANDLEINFO_INFOLINE", "  Infoline: %s" },
+    { "NSMSG_HANDLEINFO_FLAGS", "  Flags: %s" },
+    { "NSMSG_HANDLEINFO_EPITHET", "  Epithet: %s" },
+    { "NSMSG_HANDLEINFO_LAST_HOST", "  Last quit hostmask: %s" },
+    { "NSMSG_HANDLEINFO_LAST_HOST_UNKNOWN", "  Last quit hostmask: Unknown" },
+    { "NSMSG_HANDLEINFO_NICKS", "  Nickname(s): %s" },
+    { "NSMSG_HANDLEINFO_MASKS", "  Hostmask(s): %s" },
+    { "NSMSG_HANDLEINFO_CHANNELS", "  Channel(s): %s" },
+    { "NSMSG_HANDLEINFO_CURRENT", "  Current nickname(s): %s" },
+    { "NSMSG_HANDLEINFO_DNR", "  Do-not-register (by %s): %s" },
+    { "NSMSG_USERINFO_AUTHED_AS", "$b%s$b is authenticated to account $b%s$b." },
+    { "NSMSG_USERINFO_NOT_AUTHED", "$b%s$b is not authenticated to any account." },
+    { "NSMSG_NICKINFO_OWNER", "Nick $b%s$b is owned by account $b%s$b." },
+    { "NSMSG_PASSWORD_INVALID", "Incorrect password; please try again." },
+    { "NSMSG_PLEASE_SET_EMAIL", "We now require email addresses for users.  Please use the $bset email$b command to set your email address!" },
+    { "NSMSG_WEAK_PASSWORD", "WARNING: You are using a password that is considered weak (easy to guess).  It is STRONGLY recommended you change it (now, if not sooner) by typing \"/msg $S@$s PASS oldpass newpass\" (with your current password and a new password)." },
+    { "NSMSG_HANDLE_SUSPENDED", "Your $b$N$b account has been suspended; you may not use it." },
+    { "NSMSG_AUTH_SUCCESS", "I recognize you." },
+    { "NSMSG_ALLOWAUTH_STAFF", "$b%s$b is a helper or oper; please use $bstaff$b after the account name to allowauth." },
+    { "NSMSG_AUTH_ALLOWED", "User $b%s$b may now authenticate to account $b%s$b." },
+    { "NSMSG_AUTH_ALLOWED_MSG", "You may now authenticate to account $b%s$b by typing $b/msg $N@$s auth %s password$b (using your password).  If you will be using this computer regularly, please type $b/msg $N addmask$b (AFTER you auth) to permanently add your hostmask." },
+    { "NSMSG_AUTH_ALLOWED_EMAIL", "You may also (after you auth) type $b/msg $N set email user@your.isp$b to set an email address.  This will let you use the $bauthcookie$b command to be authenticated in the future." },
+    { "NSMSG_AUTH_NORMAL_ONLY", "User $b%s$b may now only authenticate to accounts with matching hostmasks." },
+    { "NSMSG_AUTH_UNSPECIAL", "User $b%s$b did not have any special auth allowance." },
+    { "NSMSG_MUST_AUTH", "You must be authenticated first." },
+    { "NSMSG_TOO_MANY_NICKS", "You have already registered the maximum permitted number of nicks." },
+    { "NSMSG_NICK_EXISTS", "Nick $b%s$b already registered." },
+    { "NSMSG_REGNICK_SUCCESS", "Nick $b%s$b has been registered to you." },
+    { "NSMSG_OREGNICK_SUCCESS", "Nick $b%s$b has been registered to account $b%s$b." },
+    { "NSMSG_PASS_SUCCESS", "Password changed." },
+    { "NSMSG_MASK_INVALID", "$b%s$b is an invalid hostmask." },
+    { "NSMSG_ADDMASK_ALREADY", "$b%s$b is already a hostmask in your account." },
+    { "NSMSG_ADDMASK_SUCCESS", "Hostmask %s added." },
+    { "NSMSG_DELMASK_NOTLAST", "You may not delete your last hostmask." },
+    { "NSMSG_DELMASK_SUCCESS", "Hostmask %s deleted." },
+    { "NSMSG_DELMASK_NOT_FOUND", "Unable to find mask to be deleted." },
+    { "NSMSG_OPSERV_LEVEL_BAD", "You may not promote another oper above your level." },
+    { "NSMSG_USE_CMD_PASS", "Please use the PASS command to change your password." },
+    { "NSMSG_UNKNOWN_NICK", "I know nothing about nick $b%s$b." },
+    { "NSMSG_NOT_YOUR_NICK", "The nick $b%s$b is not registered to you." },
+    { "NSMSG_NICK_USER_YOU", "I will not let you kill yourself." },
+    { "NSMSG_UNREGNICK_SUCCESS", "Nick $b%s$b has been unregistered." },
+    { "NSMSG_UNREGISTER_SUCCESS", "Account $b%s$b has been unregistered." },
+    { "NSMSG_UNREGISTER_NICKS_SUCCESS", "Account $b%s$b and all its nicks have been unregistered." },
+    { "NSMSG_HANDLE_STATS", "There are %d nicks registered to your account." },
+    { "NSMSG_HANDLE_NONE", "You are not authenticated against any account." },
+    { "NSMSG_GLOBAL_STATS", "There are %d accounts and %d nicks registered globally." },
+    { "NSMSG_GLOBAL_STATS_NONICK", "There are %d accounts registered." },
+    { "NSMSG_CANNOT_GHOST_SELF", "You may not ghost-kill yourself." },
+    { "NSMSG_CANNOT_GHOST_USER", "$b%s$b is not authed to your account; you may not ghost-kill them." },
+    { "NSMSG_GHOST_KILLED", "$b%s$b has been killed as a ghost." },
+    { "NSMSG_ON_VACATION", "You are now on vacation.  Your account will be preserved until you authenticate again." },
+    { "NSMSG_NO_ACCESS", "Access denied." },
+    { "NSMSG_INVALID_FLAG", "$b%c$b is not a valid $N account flag." },
+    { "NSMSG_SET_FLAG", "Applied flags $b%s$b to %s's $N account." },
+    { "NSMSG_FLAG_PRIVILEGED", "You have insufficient access to set flag %c." },
+    { "NSMSG_DB_UNREADABLE", "Unable to read database file %s; check the log for more information." },
+    { "NSMSG_DB_MERGED", "$N merged DB from %s (in "FMT_TIME_T".%03lu seconds)." },
+    { "NSMSG_HANDLE_CHANGED", "$b%s$b's account name has been changed to $b%s$b." },
+    { "NSMSG_BAD_HANDLE", "Account $b%s$b not registered because it is in use by a network service, is too long, or contains invalid characters." },
+    { "NSMSG_BAD_NICK", "Nickname $b%s$b not registered because it is in use by a network service, is too long, or contains invalid characters." },
+    { "NSMSG_BAD_EMAIL_ADDR", "Please use a well-formed email address." },
+    { "NSMSG_FAIL_RENAME", "Account $b%s$b not renamed to $b%s$b because it is in use by a network services, or contains invalid characters." },
+    { "NSMSG_ACCOUNT_SEARCH_RESULTS", "The following accounts were found:" },
+    { "NSMSG_SEARCH_MATCH", "Match: %s" },
+    { "NSMSG_INVALID_ACTION", "%s is an invalid search action." },
+    { "NSMSG_CANNOT_MERGE_SELF", "You cannot merge account $b%s$b with itself." },
+    { "NSMSG_HANDLES_MERGED", "Merged account $b%s$b into $b%s$b." },
+    { "NSMSG_RECLAIM_WARN", "%s is a registered nick - you must auth to account %s or change your nick." },
+    { "NSMSG_RECLAIM_KILL", "Unauthenticated user of nick." },
+    { "NSMSG_RECLAIMED_NONE", "You cannot manually reclaim a nick." },
+    { "NSMSG_RECLAIMED_WARN", "Sent a request for %s to change their nick." },
+    { "NSMSG_RECLAIMED_SVSNICK", "Forcibly changed %s's nick." },
+    { "NSMSG_RECLAIMED_KILL",  "Disconnected %s from the network." },
+    { "NSMSG_CLONE_AUTH", "Warning: %s (%s@%s) authed to your account." },
+    { "NSMSG_SETTING_LIST", "$b$N account settings:$b" },
+    { "NSMSG_INVALID_OPTION", "$b%s$b is an invalid account setting." },
+    { "NSMSG_INVALID_ANNOUNCE", "$b%s$b is an announcements value." },
+    { "NSMSG_SET_INFO", "$bINFO:         $b%s" },
+    { "NSMSG_SET_WIDTH", "$bWIDTH:        $b%d" },
+    { "NSMSG_SET_TABLEWIDTH", "$bTABLEWIDTH:   $b%d" },
+    { "NSMSG_SET_COLOR", "$bCOLOR:        $b%s" },
+    { "NSMSG_SET_PRIVMSG", "$bPRIVMSG:      $b%s" },
+    { "NSMSG_SET_STYLE", "$bSTYLE:        $b%s" },
+    { "NSMSG_SET_ANNOUNCEMENTS", "$bANNOUNCEMENTS: $b%s" },
+    { "NSMSG_SET_PASSWORD", "$bPASSWORD:     $b%s" },
+    { "NSMSG_SET_FLAGS", "$bFLAGS:        $b%s" },
+    { "NSMSG_SET_EMAIL", "$bEMAIL:        $b%s" },
+    { "NSMSG_SET_MAXLOGINS", "$bMAXLOGINS:    $b%d" },
+    { "NSMSG_SET_LANGUAGE", "$bLANGUAGE:     $b%s" },
+    { "NSMSG_SET_LEVEL", "$bLEVEL:        $b%d" },
+    { "NSMSG_SET_EPITHET", "$bEPITHET:      $b%s" },
+    { "NSEMAIL_ACTIVATION_SUBJECT", "Account verification for %s" },
+    { "NSEMAIL_ACTIVATION_BODY", "This email has been sent to verify that this email address belongs to the person who tried to register an account on %1$s.  Your cookie is:\n    %2$s\nTo verify your email address and complete the account registration, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %2$s\nIf you did NOT request this account, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
+    { "NSEMAIL_PASSWORD_CHANGE_SUBJECT", "Password change verification on %s" },
+    { "NSEMAIL_PASSWORD_CHANGE_BODY", "This email has been sent to verify that you wish to change the password on your account %5$s.  Your cookie is %2$s.\nTo complete the password change, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %2$s\nIf you did NOT request your password to be changed, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
+    { "NSEMAIL_EMAIL_CHANGE_SUBJECT", "Email address change verification for %s" },
+    { "NSEMAIL_EMAIL_CHANGE_BODY_NEW", "This email has been sent to verify that your email address belongs to the same person as account %5$s on %1$s.  The SECOND HALF of your cookie is %2$.*6$s.\nTo verify your address as associated with this account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s ?????%2$.*6$s\n(Replace the ????? with the FIRST HALF of the cookie, as sent to your OLD email address.)\nIf you did NOT request this email address to be associated with this account, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
+    { "NSEMAIL_EMAIL_CHANGE_BODY_OLD", "This email has been sent to verify that you want to change your email for account %5$s on %1$s from this address to %7$s.  The FIRST HALF of your cookie is %2$.*6$s\nTo verify your new address as associated with this account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %2$.*6$s?????\n(Replace the ????? with the SECOND HALF of the cookie, as sent to your NEW email address.)\nIf you did NOT request this change of email address, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
+    { "NSEMAIL_EMAIL_VERIFY_SUBJECT", "Email address verification for %s" },
+    { "NSEMAIL_EMAIL_VERIFY_BODY", "This email has been sent to verify that this address belongs to the same person as %5$s on %1$s.  Your cookie is %2$s.\nTo verify your address as associated with this account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %1$s\nIf you did NOT request this email address to be associated with this account, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
+    { "NSEMAIL_ALLOWAUTH_SUBJECT", "Authentication allowed for %s" },
+    { "NSEMAIL_ALLOWAUTH_BODY", "This email has been sent to let you authenticate (auth) to account %5$s on %1$s.  Your cookie is %2$s.\nTo auth to that account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %1$s\nIf you did NOT request this authorization, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
+    { "CHECKPASS_YES", "Yes." },
+    { "CHECKPASS_NO", "No." },
+    { NULL, NULL }
+};
+
+enum reclaim_action {
+    RECLAIM_NONE,
+    RECLAIM_WARN,
+    RECLAIM_SVSNICK,
+    RECLAIM_KILL
+};
+static void nickserv_reclaim(struct userNode *user, struct nick_info *ni, enum reclaim_action action);
+static void nickserv_reclaim_p(void *data);
+
+static struct {
+    unsigned int disable_nicks : 1;
+    unsigned int valid_handle_regex_set : 1;
+    unsigned int valid_nick_regex_set : 1;
+    unsigned int autogag_enabled : 1;
+    unsigned int email_enabled : 1;
+    unsigned int email_required : 1;
+    unsigned int default_hostmask : 1;
+    unsigned int warn_nick_owned : 1;
+    unsigned int warn_clone_auth : 1;
+    unsigned long nicks_per_handle;
+    unsigned long password_min_length;
+    unsigned long password_min_digits;
+    unsigned long password_min_upper;
+    unsigned long password_min_lower;
+    unsigned long db_backup_frequency;
+    unsigned long handle_expire_frequency;
+    unsigned long autogag_duration;
+    unsigned long email_visible_level;
+    unsigned long cookie_timeout;
+    unsigned long handle_expire_delay;
+    unsigned long nochan_handle_expire_delay;
+    unsigned long modoper_level;
+    unsigned long set_epithet_level;
+    unsigned long handles_per_email;
+    unsigned long email_search_level;
+    const char *network_name;
+    const char *titlehost_suffix;
+    regex_t valid_handle_regex;
+    regex_t valid_nick_regex;
+    dict_t weak_password_dict;
+    struct policer_params *auth_policer_params;
+    enum reclaim_action reclaim_action;
+    enum reclaim_action auto_reclaim_action;
+    unsigned long auto_reclaim_delay;
+    unsigned char default_maxlogins;
+    unsigned char hard_maxlogins;
+} nickserv_conf;
+
+/* We have 2^32 unique account IDs to use. */
+unsigned long int highest_id = 0;
+
+static char *
+canonicalize_hostmask(char *mask)
+{
+    char *out = mask, *temp;
+    if ((temp = strchr(mask, '!'))) {
+       temp++;
+       while (*temp) *out++ = *temp++;
+       *out++ = 0;
+    }
+    return mask;
+}
+
+static struct handle_info *
+register_handle(const char *handle, const char *passwd, UNUSED_ARG(unsigned long id))
+{
+    struct handle_info *hi;
+
+#ifdef WITH_PROTOCOL_BAHAMUT
+    char id_base64[IDLEN + 1];
+    do
+    {
+        /* Assign a unique account ID to the account; note that 0 is
+           an invalid account ID. 1 is therefore the first account ID. */
+        if (!id) {
+            id = 1 + highest_id++;
+        } else {
+            /* Note: highest_id is and must always be the highest ID. */
+            if(id > highest_id) {
+                highest_id = id;
+            }
+        }
+        inttobase64(id_base64, id, IDLEN);
+
+        /* Make sure an account with the same ID doesn't exist. If a
+           duplicate is found, log some details and assign a new one.
+           This should be impossible, but it never hurts to expect it. */
+        if ((hi = dict_find(nickserv_id_dict, id_base64, NULL))) {
+            log_module(NS_LOG, LOG_WARNING, "Duplicated account ID %lu (%s) found belonging to %s while inserting %s.", id, id_base64, hi->handle, handle);
+            id = 0;
+        }
+    } while(!id);
+#endif
+
+    hi = calloc(1, sizeof(*hi));
+    hi->userlist_style = HI_DEFAULT_STYLE;
+    hi->announcements = '?';
+    hi->handle = strdup(handle);
+    safestrncpy(hi->passwd, passwd, sizeof(hi->passwd));
+    hi->infoline = NULL;
+    dict_insert(nickserv_handle_dict, hi->handle, hi);
+
+#ifdef WITH_PROTOCOL_BAHAMUT
+    hi->id = id;
+    dict_insert(nickserv_id_dict, strdup(id_base64), hi);
+#endif
+
+    return hi;
+}
+
+static void
+register_nick(const char *nick, struct handle_info *owner)
+{
+    struct nick_info *ni;
+    ni = malloc(sizeof(struct nick_info));
+    safestrncpy(ni->nick, nick, sizeof(ni->nick));
+    ni->owner = owner;
+    ni->next = owner->nicks;
+    owner->nicks = ni;
+    dict_insert(nickserv_nick_dict, ni->nick, ni);
+}
+
+static void
+free_nick_info(void *vni)
+{
+    struct nick_info *ni = vni;
+    free(ni);
+}
+
+static void
+delete_nick(struct nick_info *ni)
+{
+    struct nick_info *last, *next;
+    struct userNode *user;
+    /* Check to see if we should mark a user as unregistered. */
+    if ((user = GetUserH(ni->nick)) && IsReggedNick(user)) {
+        user->modes &= ~FLAGS_REGNICK;
+        irc_regnick(user);
+    }
+    /* Remove ni from the nick_info linked list. */
+    if (ni == ni->owner->nicks) {
+       ni->owner->nicks = ni->next;
+    } else {
+       last = ni->owner->nicks;
+       next = last->next;
+       while (next != ni) {
+           last = next;
+           next = last->next;
+       }
+       last->next = next->next;
+    }
+    dict_remove(nickserv_nick_dict, ni->nick);
+}
+
+static unreg_func_t *unreg_func_list;
+static unsigned int unreg_func_size = 0, unreg_func_used = 0;
+
+void
+reg_unreg_func(unreg_func_t func)
+{
+    if (unreg_func_used == unreg_func_size) {
+       if (unreg_func_size) {
+           unreg_func_size <<= 1;
+           unreg_func_list = realloc(unreg_func_list, unreg_func_size*sizeof(unreg_func_t));
+       } else {
+           unreg_func_size = 8;
+           unreg_func_list = malloc(unreg_func_size*sizeof(unreg_func_t));
+       }
+    }
+    unreg_func_list[unreg_func_used++] = func;
+}
+
+static void
+nickserv_free_cookie(void *data)
+{
+    struct handle_cookie *cookie = data;
+    if (cookie->hi) cookie->hi->cookie = NULL;
+    if (cookie->data) free(cookie->data);
+    free(cookie);
+}
+
+static void
+free_handle_info(void *vhi)
+{
+    struct handle_info *hi = vhi;
+
+#ifdef WITH_PROTOCOL_BAHAMUT
+    char id[IDLEN + 1];
+
+    inttobase64(id, hi->id, IDLEN);
+    dict_remove(nickserv_id_dict, id);
+#endif
+
+    free_string_list(hi->masks);
+    assert(!hi->users);
+
+    while (hi->nicks)
+        delete_nick(hi->nicks);
+    free(hi->infoline);
+    free(hi->epithet);
+    if (hi->cookie) {
+        timeq_del(hi->cookie->expires, nickserv_free_cookie, hi->cookie, 0);
+        nickserv_free_cookie(hi->cookie);
+    }
+    if (hi->email_addr) {
+        struct handle_info_list *hil = dict_find(nickserv_email_dict, hi->email_addr, NULL);
+        handle_info_list_remove(hil, hi);
+        if (!hil->used)
+            dict_remove(nickserv_email_dict, hi->email_addr);
+    }
+    free(hi);
+}
+
+static void set_user_handle_info(struct userNode *user, struct handle_info *hi, int stamp);
+
+static void
+nickserv_unregister_handle(struct handle_info *hi, struct userNode *notify)
+{
+    unsigned int n;
+
+    for (n=0; n<unreg_func_used; n++)
+        unreg_func_list[n](notify, hi);
+    while (hi->users)
+        set_user_handle_info(hi->users, NULL, 0);
+    if (notify) {
+        if (nickserv_conf.disable_nicks)
+            send_message(notify, nickserv, "NSMSG_UNREGISTER_SUCCESS", hi->handle);
+        else
+            send_message(notify, nickserv, "NSMSG_UNREGISTER_NICKS_SUCCESS", hi->handle);
+    }
+    dict_remove(nickserv_handle_dict, hi->handle);
+}
+
+struct handle_info*
+get_handle_info(const char *handle)
+{
+    return dict_find(nickserv_handle_dict, handle, 0);
+}
+
+struct nick_info*
+get_nick_info(const char *nick)
+{
+    return nickserv_conf.disable_nicks ? 0 : dict_find(nickserv_nick_dict, nick, 0);
+}
+
+struct modeNode *
+find_handle_in_channel(struct chanNode *channel, struct handle_info *handle, struct userNode *except)
+{
+    unsigned int nn;
+    struct modeNode *mn;
+
+    for (nn=0; nn<channel->members.used; ++nn) {
+        mn = channel->members.list[nn];
+        if ((mn->user != except) && (mn->user->handle_info == handle))
+            return mn;
+    }
+    return NULL;
+}
+
+int
+oper_has_access(struct userNode *user, struct userNode *bot, unsigned int min_level, unsigned int quiet) {
+    if (!user->handle_info) {
+        if (!quiet)
+            send_message(user, bot, "MSG_AUTHENTICATE");
+        return 0;
+    }
+
+    if (!IsOper(user) && (!IsHelping(user) || min_level)) {
+       if (!quiet)
+            send_message(user, bot, "NSMSG_NO_ACCESS");
+       return 0;
+    }
+
+    if (HANDLE_FLAGGED(user->handle_info, OPER_SUSPENDED)) {
+       if (!quiet)
+            send_message(user, bot, "MSG_OPER_SUSPENDED");
+       return 0;
+    }
+
+    if (user->handle_info->opserv_level < min_level) {
+       if (!quiet)
+            send_message(user, bot, "NSMSG_NO_ACCESS");
+       return 0;
+    }
+
+    return 1;
+}
+
+static int
+is_valid_handle(const char *handle)
+{
+    struct userNode *user;
+    /* cant register a juped nick/service nick as handle, to prevent confusion */
+    user = GetUserH(handle);
+    if (user && IsLocal(user))
+        return 0;
+    /* check against maximum length */
+    if (strlen(handle) > NICKSERV_HANDLE_LEN)
+       return 0;
+    /* for consistency, only allow account names that could be nicks */
+    if (!is_valid_nick(handle))
+        return 0;
+    /* disallow account names that look like bad words */
+    if (opserv_bad_channel(handle))
+        return 0;
+    /* test either regex or containing all valid chars */
+    if (nickserv_conf.valid_handle_regex_set) {
+        int err = regexec(&nickserv_conf.valid_handle_regex, handle, 0, 0, 0);
+        if (err) {
+            char buff[256];
+            buff[regerror(err, &nickserv_conf.valid_handle_regex, buff, sizeof(buff))] = 0;
+            log_module(NS_LOG, LOG_INFO, "regexec error: %s (%d)", buff, err);
+        }
+        return !err;
+    } else {
+        return !handle[strspn(handle, NICKSERV_VALID_CHARS)];
+    }
+}
+
+static int
+is_registerable_nick(const char *nick)
+{
+    /* make sure it could be used as an account name */
+    if (!is_valid_handle(nick))
+        return 0;
+    /* check length */
+    if (strlen(nick) > NICKLEN)
+        return 0;
+    /* test either regex or as valid handle */
+    if (nickserv_conf.valid_nick_regex_set) {
+        int err = regexec(&nickserv_conf.valid_nick_regex, nick, 0, 0, 0);
+        if (err) {
+            char buff[256];
+            buff[regerror(err, &nickserv_conf.valid_nick_regex, buff, sizeof(buff))] = 0;
+            log_module(NS_LOG, LOG_INFO, "regexec error: %s (%d)", buff, err);
+        }
+        return !err;
+    }
+    return 1;
+}
+
+static int
+is_valid_email_addr(const char *email)
+{
+    return strchr(email, '@') != NULL;
+}
+
+static const char *
+visible_email_addr(struct userNode *user, struct handle_info *hi)
+{
+    if (hi->email_addr) {
+        if (oper_has_access(user, nickserv, nickserv_conf.email_visible_level, 1)) {
+            return hi->email_addr;
+        } else {
+            return "Set.";
+        }
+    } else {
+        return "Not set.";
+    }
+}
+
+struct handle_info *
+smart_get_handle_info(struct userNode *service, struct userNode *user, const char *name)
+{
+    struct handle_info *hi;
+    struct userNode *target;
+
+    switch (*name) {
+    case '*':
+        if (!(hi = get_handle_info(++name))) {
+            send_message(user, service, "MSG_HANDLE_UNKNOWN", name);
+            return 0;
+        }
+        return hi;
+    default:
+        if (!(target = GetUserH(name))) {
+            send_message(user, service, "MSG_NICK_UNKNOWN", name);
+            return 0;
+        }
+        if (IsLocal(target)) {
+            send_message(user, service, "NSMSG_USER_IS_SERVICE", target->nick);
+            return 0;
+        }
+        if (!(hi = target->handle_info)) {
+            send_message(user, service, "MSG_USER_AUTHENTICATE", target->nick);
+            return 0;
+        }
+        return hi;
+    }
+}
+
+int
+oper_outranks(struct userNode *user, struct handle_info *hi) {
+    if (user->handle_info->opserv_level > hi->opserv_level)
+        return 1;
+    if (user->handle_info->opserv_level == hi->opserv_level) {
+        if ((user->handle_info->opserv_level == 1000)
+            || (user->handle_info == hi)
+            || ((user->handle_info->opserv_level == 0)
+                && !(HANDLE_FLAGGED(hi, SUPPORT_HELPER) || HANDLE_FLAGGED(hi, NETWORK_HELPER))
+                && HANDLE_FLAGGED(user->handle_info, HELPING))) {
+            return 1;
+        }
+    }
+    send_message(user, nickserv, "MSG_USER_OUTRANKED", hi->handle);
+    return 0;
+}
+
+static struct handle_info *
+get_victim_oper(struct userNode *user, const char *target)
+{
+    struct handle_info *hi;
+    if (!(hi = smart_get_handle_info(nickserv, user, target)))
+        return 0;
+    if (HANDLE_FLAGGED(user->handle_info, OPER_SUSPENDED)) {
+       send_message(user, nickserv, "MSG_OPER_SUSPENDED");
+       return 0;
+    }
+    return oper_outranks(user, hi) ? hi : NULL;
+}
+
+static int
+valid_user_for(struct userNode *user, struct handle_info *hi)
+{
+    unsigned int ii;
+
+    /* If no hostmasks on the account, allow it. */
+    if (!hi->masks->used)
+        return 1;
+    /* If any hostmask matches, allow it. */
+    for (ii=0; ii<hi->masks->used; ii++)
+        if (user_matches_glob(user, hi->masks->list[ii], 0))
+            return 1;
+    /* If they are allowauthed to this account, allow it (removing the aa). */
+    if (dict_find(nickserv_allow_auth_dict, user->nick, NULL) == hi) {
+       dict_remove(nickserv_allow_auth_dict, user->nick);
+       return 2;
+    }
+    /* The user is not allowed to use this account. */
+    return 0;
+}
+
+static int
+is_secure_password(const char *handle, const char *pass, struct userNode *user)
+{
+    unsigned int i, len;
+    unsigned int cnt_digits = 0, cnt_upper = 0, cnt_lower = 0;
+    len = strlen(pass);
+    if (len < nickserv_conf.password_min_length) {
+        if (user)
+            send_message(user, nickserv, "NSMSG_PASSWORD_SHORT", nickserv_conf.password_min_length);
+        return 0;
+    }
+    if (!irccasecmp(pass, handle)) {
+        if (user)
+            send_message(user, nickserv, "NSMSG_PASSWORD_ACCOUNT");
+        return 0;
+    }
+    dict_find(nickserv_conf.weak_password_dict, pass, &i);
+    if (i) {
+        if (user)
+            send_message(user, nickserv, "NSMSG_PASSWORD_DICTIONARY");
+        return 0;
+    }
+    for (i=0; i<len; i++) {
+       if (isdigit(pass[i]))
+            cnt_digits++;
+       if (isupper(pass[i]))
+            cnt_upper++;
+       if (islower(pass[i]))
+            cnt_lower++;
+    }
+    if ((cnt_lower < nickserv_conf.password_min_lower)
+       || (cnt_upper < nickserv_conf.password_min_upper)
+       || (cnt_digits < nickserv_conf.password_min_digits)) {
+        if (user)
+            send_message(user, nickserv, "NSMSG_PASSWORD_READABLE", nickserv_conf.password_min_digits, nickserv_conf.password_min_upper, nickserv_conf.password_min_lower);
+        return 0;
+    }
+    return 1;
+}
+
+static auth_func_t *auth_func_list;
+static unsigned int auth_func_size = 0, auth_func_used = 0;
+
+void
+reg_auth_func(auth_func_t func)
+{
+    if (auth_func_used == auth_func_size) {
+       if (auth_func_size) {
+           auth_func_size <<= 1;
+           auth_func_list = realloc(auth_func_list, auth_func_size*sizeof(auth_func_t));
+       } else {
+           auth_func_size = 8;
+           auth_func_list = malloc(auth_func_size*sizeof(auth_func_t));
+       }
+    }
+    auth_func_list[auth_func_used++] = func;
+}
+
+static handle_rename_func_t *rf_list;
+static unsigned int rf_list_size, rf_list_used;
+
+void
+reg_handle_rename_func(handle_rename_func_t func)
+{
+    if (rf_list_used == rf_list_size) {
+        if (rf_list_size) {
+            rf_list_size <<= 1;
+            rf_list = realloc(rf_list, rf_list_size*sizeof(rf_list[0]));
+        } else {
+            rf_list_size = 8;
+            rf_list = malloc(rf_list_size*sizeof(rf_list[0]));
+        }
+    }
+    rf_list[rf_list_used++] = func;
+}
+
+static void
+set_user_handle_info(struct userNode *user, struct handle_info *hi, int stamp)
+{
+    unsigned int n;
+    struct handle_info *old_info;
+
+    /* This can happen if somebody uses COOKIE while authed, or if
+     * they re-auth to their current handle (which is silly, but users
+     * are like that). */
+    if (user->handle_info == hi)
+        return;
+
+    if (user->handle_info) {
+       struct userNode *other;
+
+       if (IsHelper(user))
+            userList_remove(&curr_helpers, user);
+
+       /* remove from next_authed linked list */
+       if (user->handle_info->users == user) {
+           user->handle_info->users = user->next_authed;
+       } else {
+           for (other = user->handle_info->users;
+                other->next_authed != user;
+                other = other->next_authed) ;
+           other->next_authed = user->next_authed;
+       }
+        /* if nobody left on old handle, and they're not an oper, remove !god */
+        if (!user->handle_info->users && !user->handle_info->opserv_level)
+            HANDLE_CLEAR_FLAG(user->handle_info, HELPING);
+        /* record them as being last seen at this time */
+       user->handle_info->lastseen = now;
+        /* and record their hostmask */
+        snprintf(user->handle_info->last_quit_host, sizeof(user->handle_info->last_quit_host), "%s@%s", user->ident, user->hostname);
+    }
+    old_info = user->handle_info;
+    user->handle_info = hi;
+    if (hi && !hi->users && !hi->opserv_level)
+        HANDLE_CLEAR_FLAG(hi, HELPING);
+    for (n=0; n<auth_func_used; n++)
+        auth_func_list[n](user, old_info);
+    if (hi) {
+        struct nick_info *ni;
+
+        HANDLE_CLEAR_FLAG(hi, FROZEN);
+        if (nickserv_conf.warn_clone_auth) {
+            struct userNode *other;
+            for (other = hi->users; other; other = other->next_authed)
+                send_message(other, nickserv, "NSMSG_CLONE_AUTH", user->nick, user->ident, user->hostname);
+        }
+       user->next_authed = hi->users;
+       hi->users = user;
+       hi->lastseen = now;
+       if (IsHelper(user))
+            userList_append(&curr_helpers, user);
+
+        if (stamp) {
+#ifdef WITH_PROTOCOL_BAHAMUT
+            /* Stamp users with their account ID. */
+            char id[IDLEN + 1];
+            inttobase64(id, hi->id, IDLEN);
+#elif WITH_PROTOCOL_P10
+            /* Stamp users with their account name. */
+            char *id = hi->handle;
+#else
+            const char *id = "???";
+#endif
+            if (!nickserv_conf.disable_nicks) {
+                struct nick_info *ni;
+                for (ni = hi->nicks; ni; ni = ni->next) {
+                    if (!irccasecmp(user->nick, ni->nick)) {
+                        user->modes |= FLAGS_REGNICK;
+                        break;
+                    }
+                }
+            }
+            StampUser(user, id);
+        }
+
+        if ((ni = get_nick_info(user->nick)) && (ni->owner == hi))
+            timeq_del(0, nickserv_reclaim_p, user, TIMEQ_IGNORE_WHEN);
+    } else {
+        /* We cannot clear the user's account ID, unfortunately. */
+       user->next_authed = NULL;
+    }
+}
+
+static struct handle_info*
+nickserv_register(struct userNode *user, struct userNode *settee, const char *handle, const char *passwd, int no_auth)
+{
+    struct handle_info *hi;
+    struct nick_info *ni;
+    char crypted[MD5_CRYPT_LENGTH];
+
+    if ((hi = dict_find(nickserv_handle_dict, handle, NULL))) {
+       send_message(user, nickserv, "NSMSG_HANDLE_EXISTS", handle);
+       return 0;
+    }
+
+    if (!is_secure_password(handle, passwd, user))
+        return 0;
+
+    cryptpass(passwd, crypted);
+    hi = register_handle(handle, crypted, 0);
+    hi->masks = alloc_string_list(1);
+    hi->users = NULL;
+    hi->registered = now;
+    hi->lastseen = now;
+    hi->flags = HI_DEFAULT_FLAGS;
+    if (settee && !no_auth)
+        set_user_handle_info(settee, hi, 1);
+
+    if (user != settee)
+        send_message(user, nickserv, "NSMSG_OREGISTER_H_SUCCESS");
+    else if (nickserv_conf.disable_nicks)
+        send_message(user, nickserv, "NSMSG_REGISTER_H_SUCCESS");
+    else if ((ni = dict_find(nickserv_nick_dict, user->nick, NULL)))
+        send_message(user, nickserv, "NSMSG_PARTIAL_REGISTER");
+    else {
+        register_nick(user->nick, hi);
+        send_message(user, nickserv, "NSMSG_REGISTER_HN_SUCCESS");
+    }
+    if (settee && (user != settee))
+        send_message(settee, nickserv, "NSMSG_OREGISTER_VICTIM", user->nick, hi->handle);
+    return hi;
+}
+
+static void
+nickserv_bake_cookie(struct handle_cookie *cookie)
+{
+    cookie->hi->cookie = cookie;
+    timeq_add(cookie->expires, nickserv_free_cookie, cookie);
+}
+
+static void
+nickserv_make_cookie(struct userNode *user, struct handle_info *hi, enum cookie_type type, const char *cookie_data)
+{
+    struct handle_cookie *cookie;
+    char subject[128], body[4096], *misc;
+    const char *netname, *fmt;
+    int first_time = 0;
+
+    if (hi->cookie) {
+        send_message(user, nickserv, "NSMSG_COOKIE_LIVE", hi->handle);
+        return;
+    }
+
+    cookie = calloc(1, sizeof(*cookie));
+    cookie->hi = hi;
+    cookie->type = type;
+    cookie->data = cookie_data ? strdup(cookie_data) : NULL;
+    cookie->expires = now + nickserv_conf.cookie_timeout;
+    inttobase64(cookie->cookie, rand(), 5);
+    inttobase64(cookie->cookie+5, rand(), 5);
+
+    netname = nickserv_conf.network_name;
+    subject[0] = 0;
+
+    switch (cookie->type) {
+    case ACTIVATION:
+        hi->passwd[0] = 0; /* invalidate password */
+        send_message(user, nickserv, "NSMSG_USE_COOKIE_REGISTER");
+        fmt = user_find_message(user, "NSEMAIL_ACTIVATION_SUBJECT");
+        snprintf(subject, sizeof(subject), fmt, netname);
+        fmt = user_find_message(user, "NSEMAIL_ACTIVATION_BODY");
+        snprintf(body, sizeof(body), fmt, netname, cookie->cookie, nickserv->nick, self->name, hi->handle);
+        first_time = 1;
+        break;
+    case PASSWORD_CHANGE:
+        send_message(user, nickserv, "NSMSG_USE_COOKIE_RESETPASS");
+        fmt = user_find_message(user, "NSEMAIL_PASSWORD_CHANGE_SUBJECT");
+        snprintf(subject, sizeof(subject), fmt, netname);
+        fmt = user_find_message(user, "NSEMAIL_PASSWORD_CHANGE_BODY");
+        snprintf(body, sizeof(body), fmt, netname, cookie->cookie, nickserv->nick, self->name, hi->handle);
+        break;
+    case EMAIL_CHANGE:
+        misc = hi->email_addr;
+        hi->email_addr = cookie->data;
+        if (misc) {
+            send_message(user, nickserv, "NSMSG_USE_COOKIE_EMAIL_2");
+            fmt = user_find_message(user, "NSEMAIL_EMAIL_CHANGE_SUBJECT");
+            snprintf(subject, sizeof(subject), fmt, netname);
+            fmt = user_find_message(user, "NSEMAIL_EMAIL_CHANGE_BODY_NEW");
+            snprintf(body, sizeof(body), fmt, netname, cookie->cookie+COOKIELEN/2, nickserv->nick, self->name, hi->handle, COOKIELEN/2);
+            sendmail(nickserv, hi, subject, body, 1);
+            fmt = user_find_message(user, "NSEMAIL_EMAIL_CHANGE_BODY_OLD");
+            snprintf(body, sizeof(body), fmt, netname, cookie->cookie+COOKIELEN/2, nickserv->nick, self->name, hi->handle, COOKIELEN/2, hi->email_addr);
+        } else {
+            send_message(user, nickserv, "NSMSG_USE_COOKIE_EMAIL_1");
+            fmt = user_find_message(user, "NSEMAIL_EMAIL_VERIFY_SUBJECT");
+            snprintf(subject, sizeof(subject), fmt, netname);
+            fmt = user_find_message(user, "NSEMAIL_EMAIL_VERIFY_BODY");
+            snprintf(body, sizeof(body), fmt, netname, cookie->cookie, nickserv->nick, self->name, hi->handle);
+            sendmail(nickserv, hi, subject, body, 1);
+            subject[0] = 0;
+        }
+        hi->email_addr = misc;
+        break;
+    case ALLOWAUTH:
+        fmt = user_find_message(user, "NSEMAIL_ALLOWAUTH_SUBJECT");
+        snprintf(subject, sizeof(subject), fmt, netname);
+        fmt = user_find_message(user, "NSEMAIL_ALLOWAUTH_BODY");
+        snprintf(body, sizeof(body), fmt, netname, cookie->cookie, nickserv->nick, self->name, hi->handle);
+        break;
+    default:
+        log_module(NS_LOG, LOG_ERROR, "Bad cookie type %d in nickserv_make_cookie.", cookie->type);
+        break;
+    }
+    if (subject[0])
+        sendmail(nickserv, hi, subject, body, first_time);
+    nickserv_bake_cookie(cookie);
+}
+
+static void
+nickserv_eat_cookie(struct handle_cookie *cookie)
+{
+    cookie->hi->cookie = NULL;
+    timeq_del(cookie->expires, nickserv_free_cookie, cookie, 0);
+    nickserv_free_cookie(cookie);
+}
+
+static void
+nickserv_free_email_addr(void *data)
+{
+    handle_info_list_clean(data);
+    free(data);
+}
+
+static void
+nickserv_set_email_addr(struct handle_info *hi, const char *new_email_addr)
+{
+    struct handle_info_list *hil;
+    /* Remove from old handle_info_list ... */
+    if (hi->email_addr && (hil = dict_find(nickserv_email_dict, hi->email_addr, 0))) {
+        handle_info_list_remove(hil, hi);
+        if (!hil->used) dict_remove(nickserv_email_dict, hil->tag);
+        hi->email_addr = NULL;
+    }
+    /* Add to the new list.. */
+    if (new_email_addr) {
+        if (!(hil = dict_find(nickserv_email_dict, new_email_addr, 0))) {
+            hil = calloc(1, sizeof(*hil));
+            hil->tag = strdup(new_email_addr);
+            handle_info_list_init(hil);
+            dict_insert(nickserv_email_dict, hil->tag, hil);
+        }
+        handle_info_list_append(hil, hi);
+        hi->email_addr = hil->tag;
+    }
+}
+
+static NICKSERV_FUNC(cmd_register)
+{
+    struct handle_info *hi;
+    const char *email_addr, *password;
+    int no_auth;
+
+    if (!IsOper(user) && !dict_size(nickserv_handle_dict)) {
+       /* Require the first handle registered to belong to someone +o. */
+       reply("NSMSG_REQUIRE_OPER");
+       return 0;
+    }
+
+    if (user->handle_info) {
+        reply("NSMSG_USE_RENAME", user->handle_info->handle);
+        return 0;
+    }
+
+    if (IsStamped(user)) {
+        /* Unauthenticated users might still have been stamped
+           previously and could therefore have a hidden host;
+           do not allow them to register a new account. */
+        reply("NSMSG_STAMPED_REGISTER");
+        return 0;
+    }
+
+    NICKSERV_MIN_PARMS((unsigned)3 + nickserv_conf.email_required);
+
+    if (!is_valid_handle(argv[1])) {
+        reply("NSMSG_BAD_HANDLE", argv[1]);
+        return 0;
+    }
+
+    if ((argc >= 4) && nickserv_conf.email_enabled) {
+        struct handle_info_list *hil;
+        const char *str;
+
+        /* Remember email address. */
+        email_addr = argv[3];
+
+        /* Check that the email address looks valid.. */
+        if (!is_valid_email_addr(email_addr)) {
+            reply("NSMSG_BAD_EMAIL_ADDR");
+            return 0;
+        }
+
+        /* .. and that we are allowed to send to it. */
+        if ((str = sendmail_prohibited_address(email_addr))) {
+            reply("NSMSG_EMAIL_PROHIBITED", email_addr, str);
+            return 0;
+        }
+
+        /* If we do email verify, make sure we don't spam the address. */
+        if ((hil = dict_find(nickserv_email_dict, email_addr, NULL))) {
+            unsigned int nn;
+            for (nn=0; nn<hil->used; nn++) {
+                if (hil->list[nn]->cookie) {
+                    reply("NSMSG_EMAIL_UNACTIVATED");
+                    return 0;
+                }
+            }
+            if (hil->used >= nickserv_conf.handles_per_email) {
+                reply("NSMSG_EMAIL_OVERUSED");
+                return 0;
+            }
+        }
+
+        no_auth = 1;
+    } else {
+        email_addr = 0;
+        no_auth = 0;
+    }
+
+    password = argv[2];
+    argv[2] = "****";
+    if (!(hi = nickserv_register(user, user, argv[1], password, no_auth)))
+        return 0;
+    /* Add any masks they should get. */
+    if (nickserv_conf.default_hostmask) {
+        string_list_append(hi->masks, strdup("*@*"));
+    } else {
+        string_list_append(hi->masks, generate_hostmask(user, GENMASK_OMITNICK|GENMASK_NO_HIDING|GENMASK_ANY_IDENT));
+        if (user->ip.s_addr && user->hostname[strspn(user->hostname, "0123456789.")])
+            string_list_append(hi->masks, generate_hostmask(user, GENMASK_OMITNICK|GENMASK_BYIP|GENMASK_NO_HIDING|GENMASK_ANY_IDENT));
+    }
+
+    /* If they're the first to register, give them level 1000. */
+    if (dict_size(nickserv_handle_dict) == 1) {
+        hi->opserv_level = 1000;
+        reply("NSMSG_ROOT_HANDLE", argv[1]);
+    }
+
+    /* Set their email address. */
+    if (email_addr)
+        nickserv_set_email_addr(hi, email_addr);
+
+    /* If they need to do email verification, tell them. */
+    if (no_auth)
+        nickserv_make_cookie(user, hi, ACTIVATION, hi->passwd);
+
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_oregister)
+{
+    char *mask;
+    struct userNode *settee;
+    struct handle_info *hi;
+
+    NICKSERV_MIN_PARMS(4);
+
+    if (!is_valid_handle(argv[1])) {
+        reply("NSMSG_BAD_HANDLE", argv[1]);
+        return 0;
+    }
+
+    if (strchr(argv[3], '@')) {
+       mask = canonicalize_hostmask(strdup(argv[3]));
+       if (argc > 4) {
+           settee = GetUserH(argv[4]);
+           if (!settee) {
+               reply("MSG_NICK_UNKNOWN", argv[4]);
+                free(mask);
+               return 0;
+           }
+       } else {
+           settee = NULL;
+       }
+    } else if ((settee = GetUserH(argv[3]))) {
+       mask = generate_hostmask(settee, GENMASK_OMITNICK|GENMASK_NO_HIDING|GENMASK_ANY_IDENT);
+    } else {
+       reply("NSMSG_REGISTER_BAD_NICKMASK", argv[3]);
+       return 0;
+    }
+    if (settee && settee->handle_info) {
+        reply("NSMSG_USER_PREV_AUTH", settee->nick);
+        free(mask);
+        return 0;
+    }
+    if (!(hi = nickserv_register(user, settee, argv[1], argv[2], 0))) {
+        free(mask);
+        return 0;
+    }
+    string_list_append(hi->masks, mask);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_handleinfo)
+{
+    char buff[400];
+    unsigned int i, pos=0, herelen;
+    struct userNode *target, *next_un;
+    struct handle_info *hi;
+    const char *nsmsg_none;
+
+    if (argc < 2) {
+        if (!(hi = user->handle_info)) {
+            reply("NSMSG_MUST_AUTH");
+            return 0;
+        }
+    } else if (!(hi = modcmd_get_handle_info(user, argv[1]))) {
+        return 0;
+    }
+
+    nsmsg_none = handle_find_message(hi, "MSG_NONE");
+    reply("NSMSG_HANDLEINFO_ON", hi->handle);
+#ifdef WITH_PROTOCOL_BAHAMUT
+    reply("NSMSG_HANDLEINFO_ID", hi->id);
+#endif
+    reply("NSMSG_HANDLEINFO_REGGED", ctime(&hi->registered));
+
+    if (!hi->users) {
+       intervalString(buff, now - hi->lastseen);
+       reply("NSMSG_HANDLEINFO_LASTSEEN", buff);
+    } else {
+       reply("NSMSG_HANDLEINFO_LASTSEEN_NOW");
+    }
+
+    reply("NSMSG_HANDLEINFO_INFOLINE", (hi->infoline ? hi->infoline : nsmsg_none));
+    if (HANDLE_FLAGGED(hi, FROZEN))
+        reply("NSMSG_HANDLEINFO_VACATION");
+
+    if (oper_has_access(user, cmd->parent->bot, 0, 1)) {
+        struct do_not_register *dnr;
+        if ((dnr = chanserv_is_dnr(NULL, hi)))
+            reply("NSMSG_HANDLEINFO_DNR", dnr->setter, dnr->reason);
+        if (!oper_outranks(user, hi))
+            return 1;
+    } else if (hi != user->handle_info)
+        return 1;
+
+    if (nickserv_conf.email_enabled)
+        reply("NSMSG_HANDLEINFO_EMAIL_ADDR", visible_email_addr(user, hi));
+
+    if (hi->cookie) {
+        const char *type;
+        switch (hi->cookie->type) {
+        case ACTIVATION: type = "NSMSG_HANDLEINFO_COOKIE_ACTIVATION"; break;
+        case PASSWORD_CHANGE: type = "NSMSG_HANDLEINFO_COOKIE_PASSWORD"; break;
+        case EMAIL_CHANGE: type = "NSMSG_HANDLEINFO_COOKIE_EMAIL"; break;
+        case ALLOWAUTH: type = "NSMSG_HANDLEINFO_COOKIE_ALLOWAUTH"; break;
+        default: type = "NSMSG_HANDLEINFO_COOKIE_UNKNOWN"; break;
+        }
+        reply(type);
+    }
+
+    if (hi->flags) {
+       unsigned long flen = 1;
+       char flags[34]; /* 32 bits possible plus '+' and '\0' */
+       flags[0] = '+';
+       for (i=0, flen=1; handle_flags[i]; i++)
+           if (hi->flags & 1 << i)
+                flags[flen++] = handle_flags[i];
+       flags[flen] = 0;
+       reply("NSMSG_HANDLEINFO_FLAGS", flags);
+    } else {
+       reply("NSMSG_HANDLEINFO_FLAGS", nsmsg_none);
+    }
+
+    if (HANDLE_FLAGGED(hi, SUPPORT_HELPER)
+        || HANDLE_FLAGGED(hi, NETWORK_HELPER)
+        || (hi->opserv_level > 0)) {
+        reply("NSMSG_HANDLEINFO_EPITHET", (hi->epithet ? hi->epithet : nsmsg_none));
+    }
+
+    if (hi->last_quit_host[0])
+        reply("NSMSG_HANDLEINFO_LAST_HOST", hi->last_quit_host);
+    else
+        reply("NSMSG_HANDLEINFO_LAST_HOST_UNKNOWN");
+
+    if (nickserv_conf.disable_nicks) {
+       /* nicks disabled; don't show anything about registered nicks */
+    } else if (hi->nicks) {
+       struct nick_info *ni, *next_ni;
+       for (ni = hi->nicks; ni; ni = next_ni) {
+           herelen = strlen(ni->nick);
+           if (pos + herelen + 1 > ArrayLength(buff)) {
+               next_ni = ni;
+               goto print_nicks_buff;
+           } else {
+               next_ni = ni->next;
+           }
+           memcpy(buff+pos, ni->nick, herelen);
+           pos += herelen; buff[pos++] = ' ';
+           if (!next_ni) {
+             print_nicks_buff:
+               buff[pos-1] = 0;
+               reply("NSMSG_HANDLEINFO_NICKS", buff);
+               pos = 0;
+           }
+       }
+    } else {
+       reply("NSMSG_HANDLEINFO_NICKS", nsmsg_none);
+    }
+
+    if (hi->masks->used) {
+        for (i=0; i < hi->masks->used; i++) {
+            herelen = strlen(hi->masks->list[i]);
+            if (pos + herelen + 1 > ArrayLength(buff)) {
+                i--;
+                goto print_mask_buff;
+            }
+            memcpy(buff+pos, hi->masks->list[i], herelen);
+            pos += herelen; buff[pos++] = ' ';
+            if (i+1 == hi->masks->used) {
+              print_mask_buff:
+                buff[pos-1] = 0;
+                reply("NSMSG_HANDLEINFO_MASKS", buff);
+                pos = 0;
+            }
+        }
+    } else {
+        reply("NSMSG_HANDLEINFO_MASKS", nsmsg_none);
+    }
+
+    if (hi->channels) {
+       struct userData *channel, *next;
+       char *name;
+
+       for (channel = hi->channels; channel; channel = next) {
+           next = channel->u_next;
+            name = channel->channel->channel->name;
+           herelen = strlen(name);
+           if (pos + herelen + 7 > ArrayLength(buff)) {
+               next = channel;
+                goto print_chans_buff;
+           }
+            if (IsUserSuspended(channel))
+                buff[pos++] = '-';
+            pos += sprintf(buff+pos, "%d:%s ", channel->access, name);
+           if (next == NULL) {
+             print_chans_buff:
+               buff[pos-1] = 0;
+               reply("NSMSG_HANDLEINFO_CHANNELS", buff);
+               pos = 0;
+           }
+       }
+    } else {
+       reply("NSMSG_HANDLEINFO_CHANNELS", nsmsg_none);
+    }
+
+    for (target = hi->users; target; target = next_un) {
+       herelen = strlen(target->nick);
+       if (pos + herelen + 1 > ArrayLength(buff)) {
+           next_un = target;
+           goto print_cnick_buff;
+       } else {
+           next_un = target->next_authed;
+       }
+       memcpy(buff+pos, target->nick, herelen);
+       pos += herelen; buff[pos++] = ' ';
+       if (!next_un) {
+         print_cnick_buff:
+           buff[pos-1] = 0;
+           reply("NSMSG_HANDLEINFO_CURRENT", buff);
+           pos = 0;
+       }
+    }
+
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_userinfo)
+{
+    struct userNode *target;
+
+    NICKSERV_MIN_PARMS(2);
+    if (!(target = GetUserH(argv[1]))) {
+       reply("MSG_NICK_UNKNOWN", argv[1]);
+       return 0;
+    }
+    if (target->handle_info)
+       reply("NSMSG_USERINFO_AUTHED_AS", target->nick, target->handle_info->handle);
+    else
+       reply("NSMSG_USERINFO_NOT_AUTHED", target->nick);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_nickinfo)
+{
+    struct nick_info *ni;
+
+    NICKSERV_MIN_PARMS(2);
+    if (!(ni = get_nick_info(argv[1]))) {
+       reply("MSG_NICK_UNKNOWN", argv[1]);
+       return 0;
+    }
+    reply("NSMSG_NICKINFO_OWNER", ni->nick, ni->owner->handle);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_rename_handle)
+{
+    struct handle_info *hi;
+    char msgbuf[MAXLEN], *old_handle;
+    unsigned int nn;
+
+    NICKSERV_MIN_PARMS(3);
+    if (!(hi = get_victim_oper(user, argv[1])))
+        return 0;
+    if (!is_valid_handle(argv[2])) {
+        reply("NSMSG_FAIL_RENAME", argv[1], argv[2]);
+        return 0;
+    }
+    if (get_handle_info(argv[2])) {
+        reply("NSMSG_HANDLE_EXISTS", argv[2]);
+        return 0;
+    }
+
+    dict_remove2(nickserv_handle_dict, old_handle = hi->handle, 1);
+    hi->handle = strdup(argv[2]);
+    dict_insert(nickserv_handle_dict, hi->handle, hi);
+    for (nn=0; nn<rf_list_used; nn++)
+        rf_list[nn](hi, old_handle);
+    snprintf(msgbuf, sizeof(msgbuf), "%s renamed account %s to %s.", user->handle_info->handle, old_handle, hi->handle);
+    reply("NSMSG_HANDLE_CHANGED", old_handle, hi->handle);
+    global_message(MESSAGE_RECIPIENT_STAFF, msgbuf);
+    free(old_handle);
+    return 1;
+}
+
+static failpw_func_t *failpw_func_list;
+static unsigned int failpw_func_size = 0, failpw_func_used = 0;
+
+void
+reg_failpw_func(failpw_func_t func)
+{
+    if (failpw_func_used == failpw_func_size) {
+        if (failpw_func_size) {
+            failpw_func_size <<= 1;
+            failpw_func_list = realloc(failpw_func_list, failpw_func_size*sizeof(failpw_func_t));
+        } else {
+            failpw_func_size = 8;
+            failpw_func_list = malloc(failpw_func_size*sizeof(failpw_func_t));
+        }
+    }
+    failpw_func_list[failpw_func_used++] = func;
+}
+
+static NICKSERV_FUNC(cmd_auth)
+{
+    int pw_arg, used, maxlogins;
+    struct handle_info *hi;
+    const char *passwd;
+    struct userNode *other;
+
+    if (user->handle_info) {
+        reply("NSMSG_ALREADY_AUTHED", user->handle_info->handle);
+        return 0;
+    }
+    if (IsStamped(user)) {
+        /* Unauthenticated users might still have been stamped
+           previously and could therefore have a hidden host;
+           do not allow them to authenticate. */
+        reply("NSMSG_STAMPED_AUTH");
+        return 0;
+    }
+    if (argc == 3) {
+        hi = dict_find(nickserv_handle_dict, argv[1], NULL);
+        pw_arg = 2;
+    } else if (argc == 2) {
+        if (nickserv_conf.disable_nicks) {
+            if (!(hi = get_handle_info(user->nick))) {
+                reply("NSMSG_HANDLE_NOT_FOUND");
+                return 0;
+            }
+        } else {
+            /* try to look up their handle from their nick */
+            struct nick_info *ni;
+            ni = get_nick_info(user->nick);
+            if (!ni) {
+                reply("NSMSG_NICK_NOT_REGISTERED", user->nick);
+                return 0;
+            }
+            hi = ni->owner;
+        }
+        pw_arg = 1;
+    } else {
+        reply("MSG_MISSING_PARAMS", argv[0]);
+        svccmd_send_help(user, nickserv, cmd);
+        return 0;
+    }
+    if (!hi) {
+        reply("NSMSG_HANDLE_NOT_FOUND");
+        return 0;
+    }
+    passwd = argv[pw_arg];
+    if (!valid_user_for(user, hi)) {
+        if (hi->email_addr && nickserv_conf.email_enabled)
+            reply("NSMSG_USE_AUTHCOOKIE", hi->handle, hi->handle);
+        else
+            reply("NSMSG_HOSTMASK_INVALID", hi->handle);
+        argv[pw_arg] = "BADMASK";
+        return 1;
+    }
+    if (!checkpass(passwd, hi->passwd)) {
+        unsigned int n;
+        reply("NSMSG_PASSWORD_INVALID");
+        argv[pw_arg] = "BADPASS";
+        for (n=0; n<failpw_func_used; n++) failpw_func_list[n](user, hi);
+        if (nickserv_conf.autogag_enabled) {
+            if (!user->auth_policer.params) {
+                user->auth_policer.last_req = now;
+                user->auth_policer.params = nickserv_conf.auth_policer_params;
+            }
+            if (!policer_conforms(&user->auth_policer, now, 1.0)) {
+                char *hostmask;
+                hostmask = generate_hostmask(user, GENMASK_STRICT_HOST|GENMASK_BYIP|GENMASK_NO_HIDING);
+                log_module(NS_LOG, LOG_INFO, "%s auto-gagged for repeated password guessing.", hostmask);
+                gag_create(hostmask, nickserv->nick, "Repeated password guessing.", now+nickserv_conf.autogag_duration);
+                free(hostmask);
+                argv[pw_arg] = "GAGGED";
+            }
+        }
+        return 1;
+    }
+    if (HANDLE_FLAGGED(hi, SUSPENDED)) {
+        reply("NSMSG_HANDLE_SUSPENDED");
+        argv[pw_arg] = "SUSPENDED";
+        return 1;
+    }
+    maxlogins = hi->maxlogins ? hi->maxlogins : nickserv_conf.default_maxlogins;
+    for (used = 0, other = hi->users; other; other = other->next_authed) {
+        if (++used >= maxlogins) {
+            reply("NSMSG_MAX_LOGINS", maxlogins);
+            argv[pw_arg] = "MAXLOGINS";
+            return 1;
+        }
+    }
+
+    if (nickserv_conf.email_required && !hi->email_addr)
+        reply("NSMSG_PLEASE_SET_EMAIL");
+    if (!is_secure_password(hi->handle, passwd, NULL))
+        reply("NSMSG_WEAK_PASSWORD");
+    if (hi->passwd[0] != '$')
+        cryptpass(passwd, hi->passwd);
+
+    reply("NSMSG_AUTH_SUCCESS");
+    argv[pw_arg] = "****";
+    set_user_handle_info(user, hi, 1);
+    return 1;
+}
+
+static allowauth_func_t *allowauth_func_list;
+static unsigned int allowauth_func_size = 0, allowauth_func_used = 0;
+
+void
+reg_allowauth_func(allowauth_func_t func)
+{
+    if (allowauth_func_used == allowauth_func_size) {
+        if (allowauth_func_size) {
+            allowauth_func_size <<= 1;
+            allowauth_func_list = realloc(allowauth_func_list, allowauth_func_size*sizeof(allowauth_func_t));
+        } else {
+            allowauth_func_size = 8;
+            allowauth_func_list = malloc(allowauth_func_size*sizeof(allowauth_func_t));
+        }
+    }
+    allowauth_func_list[allowauth_func_used++] = func;
+}
+
+static NICKSERV_FUNC(cmd_allowauth)
+{
+    struct userNode *target;
+    struct handle_info *hi;
+    unsigned int n;
+
+    NICKSERV_MIN_PARMS(2);
+    if (!(target = GetUserH(argv[1]))) {
+        reply("MSG_NICK_UNKNOWN", argv[1]);
+        return 0;
+    }
+    if (target->handle_info) {
+        reply("NSMSG_USER_PREV_AUTH", target->nick);
+        return 0;
+    }
+    if (IsStamped(target)) {
+        /* Unauthenticated users might still have been stamped
+           previously and could therefore have a hidden host;
+           do not allow them to authenticate to an account. */
+        send_message(target, nickserv, "NSMSG_USER_PREV_STAMP", target->nick);
+        return 0;
+    }
+    if (argc == 2)
+        hi = NULL;
+    else if (!(hi = get_handle_info(argv[2]))) {
+        reply("MSG_HANDLE_UNKNOWN", argv[2]);
+        return 0;
+    }
+    if (hi) {
+        if (hi->opserv_level > user->handle_info->opserv_level) {
+            reply("MSG_USER_OUTRANKED", hi->handle);
+            return 0;
+        }
+        if (((hi->flags & (HI_FLAG_SUPPORT_HELPER|HI_FLAG_NETWORK_HELPER))
+             || (hi->opserv_level > 0))
+            && ((argc < 4) || irccasecmp(argv[3], "staff"))) {
+            reply("NSMSG_ALLOWAUTH_STAFF", hi->handle);
+            return 0;
+        }
+        dict_insert(nickserv_allow_auth_dict, target->nick, hi);
+        reply("NSMSG_AUTH_ALLOWED", target->nick, hi->handle);
+        send_message(target, nickserv, "NSMSG_AUTH_ALLOWED_MSG", hi->handle, hi->handle);
+        if (nickserv_conf.email_enabled)
+            send_message(target, nickserv, "NSMSG_AUTH_ALLOWED_EMAIL");
+    } else {
+        if (dict_remove(nickserv_allow_auth_dict, target->nick))
+            reply("NSMSG_AUTH_NORMAL_ONLY", target->nick);
+        else
+            reply("NSMSG_AUTH_UNSPECIAL", target->nick);
+    }
+    for (n=0; n<allowauth_func_used; n++)
+        allowauth_func_list[n](user, target, hi);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_authcookie)
+{
+    struct handle_info *hi;
+
+    NICKSERV_MIN_PARMS(2);
+    if (user->handle_info) {
+        reply("NSMSG_ALREADY_AUTHED", user->handle_info->handle);
+        return 0;
+    }
+    if (IsStamped(user)) {
+        /* Unauthenticated users might still have been stamped
+           previously and could therefore have a hidden host;
+           do not allow them to authenticate to an account. */
+        reply("NSMSG_STAMPED_AUTHCOOKIE");
+        return 0;
+    }
+    if (!(hi = get_handle_info(argv[1]))) {
+        reply("MSG_HANDLE_UNKNOWN", argv[1]);
+        return 0;
+    }
+    if (!hi->email_addr) {
+        reply("MSG_SET_EMAIL_ADDR");
+        return 0;
+    }
+    nickserv_make_cookie(user, hi, ALLOWAUTH, NULL);
+    reply("NSMSG_USE_COOKIE_AUTH");
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_delcookie)
+{
+    struct handle_info *hi;
+
+    hi = user->handle_info;
+    if (!hi->cookie) {
+        reply("NSMSG_NO_COOKIE");
+        return 0;
+    }
+    switch (hi->cookie->type) {
+    case ACTIVATION:
+    case EMAIL_CHANGE:
+        reply("NSMSG_MUST_TIME_OUT");
+        break;
+    default:
+        nickserv_eat_cookie(hi->cookie);
+        reply("NSMSG_ATE_COOKIE");
+        break;
+    }
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_resetpass)
+{
+    struct handle_info *hi;
+    char crypted[MD5_CRYPT_LENGTH];
+
+    NICKSERV_MIN_PARMS(3);
+    if (user->handle_info) {
+        reply("NSMSG_ALREADY_AUTHED", user->handle_info->handle);
+        return 0;
+    }
+    if (IsStamped(user)) {
+        /* Unauthenticated users might still have been stamped
+           previously and could therefore have a hidden host;
+           do not allow them to activate an account. */
+        reply("NSMSG_STAMPED_RESETPASS");
+        return 0;
+    }
+    if (!(hi = get_handle_info(argv[1]))) {
+        reply("MSG_HANDLE_UNKNOWN", argv[1]);
+        return 0;
+    }
+    if (!hi->email_addr) {
+        reply("MSG_SET_EMAIL_ADDR");
+        return 0;
+    }
+    cryptpass(argv[2], crypted);
+    argv[2] = "****";
+    nickserv_make_cookie(user, hi, PASSWORD_CHANGE, crypted);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_cookie)
+{
+    struct handle_info *hi;
+    const char *cookie;
+
+    if ((argc == 2) && (hi = user->handle_info) && hi->cookie && (hi->cookie->type == EMAIL_CHANGE)) {
+        cookie = argv[1];
+    } else {
+        NICKSERV_MIN_PARMS(3);
+        if (!(hi = get_handle_info(argv[1]))) {
+            reply("MSG_HANDLE_UNKNOWN", argv[1]);
+            return 0;
+        }
+        cookie = argv[2];
+    }
+
+    if (HANDLE_FLAGGED(hi, SUSPENDED)) {
+        reply("NSMSG_HANDLE_SUSPENDED");
+        return 0;
+    }
+
+    if (!hi->cookie) {
+        reply("NSMSG_NO_COOKIE");
+        return 0;
+    }
+
+    /* Check validity of operation before comparing cookie to
+     * prohibit guessing by authed users. */
+    if (user->handle_info
+        && (hi->cookie->type != EMAIL_CHANGE)
+        && (hi->cookie->type != PASSWORD_CHANGE)) {
+        reply("NSMSG_CANNOT_COOKIE");
+        return 0;
+    }
+
+    if (strcmp(cookie, hi->cookie->cookie)) {
+        reply("NSMSG_BAD_COOKIE");
+        return 0;
+    }
+
+    switch (hi->cookie->type) {
+    case ACTIVATION:
+        safestrncpy(hi->passwd, hi->cookie->data, sizeof(hi->passwd));
+        set_user_handle_info(user, hi, 1);
+        reply("NSMSG_HANDLE_ACTIVATED");
+        break;
+    case PASSWORD_CHANGE:
+        set_user_handle_info(user, hi, 1);
+        safestrncpy(hi->passwd, hi->cookie->data, sizeof(hi->passwd));
+        reply("NSMSG_PASSWORD_CHANGED");
+        break;
+    case EMAIL_CHANGE:
+        nickserv_set_email_addr(hi, hi->cookie->data);
+        reply("NSMSG_EMAIL_CHANGED");
+        break;
+    case ALLOWAUTH:
+        set_user_handle_info(user, hi, 1);
+        reply("NSMSG_AUTH_SUCCESS");
+        break;
+    default:
+        reply("NSMSG_BAD_COOKIE_TYPE", hi->cookie->type);
+        log_module(NS_LOG, LOG_ERROR, "Bad cookie type %d for account %s.", hi->cookie->type, hi->handle);
+        break;
+    }
+
+    nickserv_eat_cookie(hi->cookie);
+
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_oregnick) {
+    const char *nick;
+    struct handle_info *target;
+    struct nick_info *ni;
+
+    NICKSERV_MIN_PARMS(3);
+    if (!(target = modcmd_get_handle_info(user, argv[1])))
+        return 0;
+    nick = argv[2];
+    if (!is_registerable_nick(nick)) {
+        reply("NSMSG_BAD_NICK", nick);
+        return 0;
+    }
+    ni = dict_find(nickserv_nick_dict, nick, NULL);
+    if (ni) {
+       reply("NSMSG_NICK_EXISTS", nick);
+       return 0;
+    }
+    register_nick(nick, target);
+    reply("NSMSG_OREGNICK_SUCCESS", nick, target->handle);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_regnick) {
+    unsigned n;
+    struct nick_info *ni;
+
+    if (!is_registerable_nick(user->nick)) {
+        reply("NSMSG_BAD_NICK", user->nick);
+        return 0;
+    }
+    /* count their nicks, see if it's too many */
+    for (n=0,ni=user->handle_info->nicks; ni; n++,ni=ni->next) ;
+    if (n >= nickserv_conf.nicks_per_handle) {
+        reply("NSMSG_TOO_MANY_NICKS");
+        return 0;
+    }
+    ni = dict_find(nickserv_nick_dict, user->nick, NULL);
+    if (ni) {
+       reply("NSMSG_NICK_EXISTS", user->nick);
+       return 0;
+    }
+    register_nick(user->nick, user->handle_info);
+    reply("NSMSG_REGNICK_SUCCESS", user->nick);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_pass)
+{
+    struct handle_info *hi;
+    const char *old_pass, *new_pass;
+
+    NICKSERV_MIN_PARMS(3);
+    hi = user->handle_info;
+    old_pass = argv[1];
+    new_pass = argv[2];
+    argv[2] = "****";
+    if (!is_secure_password(hi->handle, new_pass, user)) return 0;
+    if (!checkpass(old_pass, hi->passwd)) {
+        argv[1] = "BADPASS";
+       reply("NSMSG_PASSWORD_INVALID");
+       return 0;
+    }
+    cryptpass(new_pass, hi->passwd);
+    argv[1] = "****";
+    reply("NSMSG_PASS_SUCCESS");
+    return 1;
+}
+
+static int
+nickserv_addmask(struct userNode *user, struct handle_info *hi, const char *mask)
+{
+    unsigned int i;
+    char *new_mask = canonicalize_hostmask(strdup(mask));
+    for (i=0; i<hi->masks->used; i++) {
+        if (!irccasecmp(new_mask, hi->masks->list[i])) {
+            send_message(user, nickserv, "NSMSG_ADDMASK_ALREADY", new_mask);
+            free(new_mask);
+            return 0;
+        }
+    }
+    string_list_append(hi->masks, new_mask);
+    send_message(user, nickserv, "NSMSG_ADDMASK_SUCCESS", new_mask);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_addmask)
+{
+    if (argc < 2) {
+        char *mask = generate_hostmask(user, GENMASK_OMITNICK|GENMASK_NO_HIDING|GENMASK_ANY_IDENT);
+        int res = nickserv_addmask(user, user->handle_info, mask);
+        free(mask);
+        return res;
+    } else {
+        if (!is_gline(argv[1])) {
+            reply("NSMSG_MASK_INVALID", argv[1]);
+            return 0;
+        }
+        return nickserv_addmask(user, user->handle_info, argv[1]);
+    }
+}
+
+static NICKSERV_FUNC(cmd_oaddmask)
+{
+    struct handle_info *hi;
+
+    NICKSERV_MIN_PARMS(3);
+    if (!(hi = get_victim_oper(user, argv[1])))
+        return 0;
+    return nickserv_addmask(user, hi, argv[2]);
+}
+
+static int
+nickserv_delmask(struct userNode *user, struct handle_info *hi, const char *del_mask)
+{
+    unsigned int i;
+    for (i=0; i<hi->masks->used; i++) {
+       if (!strcmp(del_mask, hi->masks->list[i])) {
+           char *old_mask = hi->masks->list[i];
+           if (hi->masks->used == 1) {
+               send_message(user, nickserv, "NSMSG_DELMASK_NOTLAST");
+               return 0;
+           }
+           hi->masks->list[i] = hi->masks->list[--hi->masks->used];
+           send_message(user, nickserv, "NSMSG_DELMASK_SUCCESS", old_mask);
+           free(old_mask);
+           return 1;
+       }
+    }
+    send_message(user, nickserv, "NSMSG_DELMASK_NOT_FOUND");
+    return 0;
+}
+
+static NICKSERV_FUNC(cmd_delmask)
+{
+    NICKSERV_MIN_PARMS(2);
+    return nickserv_delmask(user, user->handle_info, argv[1]);
+}
+
+static NICKSERV_FUNC(cmd_odelmask)
+{
+    struct handle_info *hi;
+    NICKSERV_MIN_PARMS(3);
+    if (!(hi = get_victim_oper(user, argv[1])))
+        return 0;
+    return nickserv_delmask(user, hi, argv[2]);
+}
+
+int
+nickserv_modify_handle_flags(struct userNode *user, struct userNode *bot, const char *str, unsigned long *padded, unsigned long *premoved) {
+    unsigned int nn, add = 1, pos;
+    unsigned long added, removed, flag;
+
+    for (added=removed=nn=0; str[nn]; nn++) {
+       switch (str[nn]) {
+       case '+': add = 1; break;
+       case '-': add = 0; break;
+       default:
+           if (!(pos = handle_inverse_flags[(unsigned char)str[nn]])) {
+               send_message(user, bot, "NSMSG_INVALID_FLAG", str[nn]);
+               return 0;
+           }
+            if (user && (user->handle_info->opserv_level < flag_access_levels[pos-1])) {
+                /* cheesy avoidance of looking up the flag name.. */
+                send_message(user, bot, "NSMSG_FLAG_PRIVILEGED", str[nn]);
+                return 0;
+            }
+            flag = 1 << (pos - 1);
+           if (add)
+                added |= flag, removed &= ~flag;
+           else
+                removed |= flag, added &= ~flag;
+           break;
+       }
+    }
+    *padded = added;
+    *premoved = removed;
+    return 1;
+}
+
+static int
+nickserv_apply_flags(struct userNode *user, struct handle_info *hi, const char *flags)
+{
+    unsigned long before, after, added, removed;
+    struct userNode *uNode;
+
+    before = hi->flags & (HI_FLAG_SUPPORT_HELPER|HI_FLAG_NETWORK_HELPER);
+    if (!nickserv_modify_handle_flags(user, nickserv, flags, &added, &removed))
+        return 0;
+    hi->flags = (hi->flags | added) & ~removed;
+    after = hi->flags & (HI_FLAG_SUPPORT_HELPER|HI_FLAG_NETWORK_HELPER);
+
+    /* Strip helping flag if they're only a support helper and not
+     * currently in #support. */
+    if (HANDLE_FLAGGED(hi, HELPING) && (after == HI_FLAG_SUPPORT_HELPER)) {
+        struct channelList *schannels;
+        unsigned int ii;
+        schannels = chanserv_support_channels();
+        for (uNode = hi->users; uNode; uNode = uNode->next_authed) {
+            for (ii = 0; ii < schannels->used; ++ii)
+                if (GetUserMode(schannels->list[ii], uNode))
+                    break;
+            if (ii < schannels->used)
+                break;
+        }
+        if (!uNode)
+            HANDLE_CLEAR_FLAG(hi, HELPING);
+    }
+
+    if (after && !before) {
+        /* Add user to current helper list. */
+        for (uNode = hi->users; uNode; uNode = uNode->next_authed)
+            userList_append(&curr_helpers, uNode);
+    } else if (!after && before) {
+        /* Remove user from current helper list. */
+        for (uNode = hi->users; uNode; uNode = uNode->next_authed)
+            userList_remove(&curr_helpers, uNode);
+    }
+
+    return 1;
+}
+
+static void
+set_list(struct userNode *user, struct handle_info *hi, int override)
+{
+    option_func_t *opt;
+    unsigned int i;
+    char *set_display[] = {
+        "INFO", "WIDTH", "TABLEWIDTH", "COLOR", "PRIVMSG", "STYLE",
+        "EMAIL", "ANNOUNCEMENTS", "MAXLOGINS"
+    };
+
+    send_message(user, nickserv, "NSMSG_SETTING_LIST");
+
+    /* Do this so options are presented in a consistent order. */
+    for (i = 0; i < ArrayLength(set_display); ++i)
+       if ((opt = dict_find(nickserv_opt_dict, set_display[i], NULL)))
+           opt(user, hi, override, 0, NULL);
+}
+
+static NICKSERV_FUNC(cmd_set)
+{
+    struct handle_info *hi;
+    option_func_t *opt;
+
+    hi = user->handle_info;
+    if (argc < 2) {
+       set_list(user, hi, 0);
+       return 1;
+    }
+    if (!(opt = dict_find(nickserv_opt_dict, argv[1], NULL))) {
+       reply("NSMSG_INVALID_OPTION", argv[1]);
+        return 0;
+    }
+    return opt(user, hi, 0, argc-1, argv+1);
+}
+
+static NICKSERV_FUNC(cmd_oset)
+{
+    struct handle_info *hi;
+    option_func_t *opt;
+
+    NICKSERV_MIN_PARMS(2);
+
+    if (!(hi = get_victim_oper(user, argv[1])))
+        return 0;
+
+    if (argc < 3) {
+       set_list(user, hi, 0);
+       return 1;
+    }
+
+    if (!(opt = dict_find(nickserv_opt_dict, argv[2], NULL))) {
+       reply("NSMSG_INVALID_OPTION", argv[2]);
+        return 0;
+    }
+
+    return opt(user, hi, 1, argc-2, argv+2);
+}
+
+static OPTION_FUNC(opt_info)
+{
+    const char *info;
+    if (argc > 1) {
+       if ((argv[1][0] == '*') && (argv[1][1] == 0)) {
+            free(hi->infoline);
+            hi->infoline = NULL;
+       } else {
+           hi->infoline = strdup(unsplit_string(argv+1, argc-1, NULL));
+       }
+    }
+
+    info = hi->infoline ? hi->infoline : user_find_message(user, "MSG_NONE");
+    send_message(user, nickserv, "NSMSG_SET_INFO", info);
+    return 1;
+}
+
+static OPTION_FUNC(opt_width)
+{
+    if (argc > 1) {
+       unsigned int new_width = strtoul(argv[1], NULL, 0);
+       hi->screen_width = new_width;
+    }
+
+    if ((hi->screen_width > 0) && (hi->screen_width < MIN_LINE_SIZE))
+        hi->screen_width = MIN_LINE_SIZE;
+    else if (hi->screen_width > MAX_LINE_SIZE)
+        hi->screen_width = MAX_LINE_SIZE;
+
+    send_message(user, nickserv, "NSMSG_SET_WIDTH", hi->screen_width);
+    return 1;
+}
+
+static OPTION_FUNC(opt_tablewidth)
+{
+    if (argc > 1) {
+       unsigned int new_width = strtoul(argv[1], NULL, 0);
+       hi->table_width = new_width;
+    }
+
+    if ((hi->table_width > 0) && (hi->table_width < MIN_LINE_SIZE))
+        hi->table_width = MIN_LINE_SIZE;
+    else if (hi->screen_width > MAX_LINE_SIZE)
+        hi->table_width = MAX_LINE_SIZE;
+
+    send_message(user, nickserv, "NSMSG_SET_TABLEWIDTH", hi->table_width);
+    return 1;
+}
+
+static OPTION_FUNC(opt_color)
+{
+    if (argc > 1) {
+       if (enabled_string(argv[1]))
+           HANDLE_SET_FLAG(hi, MIRC_COLOR);
+        else if (disabled_string(argv[1]))
+           HANDLE_CLEAR_FLAG(hi, MIRC_COLOR);
+       else {
+           send_message(user, nickserv, "MSG_INVALID_BINARY", argv[1]);
+           return 0;
+       }
+    }
+
+    send_message(user, nickserv, "NSMSG_SET_COLOR", user_find_message(user, HANDLE_FLAGGED(hi, MIRC_COLOR) ? "MSG_ON" : "MSG_OFF"));
+    return 1;
+}
+
+static OPTION_FUNC(opt_privmsg)
+{
+    if (argc > 1) {
+       if (enabled_string(argv[1]))
+           HANDLE_SET_FLAG(hi, USE_PRIVMSG);
+        else if (disabled_string(argv[1]))
+           HANDLE_CLEAR_FLAG(hi, USE_PRIVMSG);
+       else {
+           send_message(user, nickserv, "MSG_INVALID_BINARY", argv[1]);
+           return 0;
+       }
+    }
+
+    send_message(user, nickserv, "NSMSG_SET_PRIVMSG", user_find_message(user, HANDLE_FLAGGED(hi, USE_PRIVMSG) ? "MSG_ON" : "MSG_OFF"));
+    return 1;
+}
+
+static OPTION_FUNC(opt_style)
+{
+    char *style;
+
+    if (argc > 1) {
+       if (!irccasecmp(argv[1], "Zoot"))
+           hi->userlist_style = HI_STYLE_ZOOT;
+       else if (!irccasecmp(argv[1], "def"))
+           hi->userlist_style = HI_STYLE_DEF;
+    }
+
+    switch (hi->userlist_style) {
+    case HI_STYLE_DEF:
+       style = "def";
+       break;
+    case HI_STYLE_ZOOT:
+    default:
+       style = "Zoot";
+    }
+
+    send_message(user, nickserv, "NSMSG_SET_STYLE", style);
+    return 1;
+}
+
+static OPTION_FUNC(opt_announcements)
+{
+    const char *choice;
+
+    if (argc > 1) {
+        if (enabled_string(argv[1]))
+            hi->announcements = 'y';
+        else if (disabled_string(argv[1]))
+            hi->announcements = 'n';
+        else if (!strcmp(argv[1], "?") || !irccasecmp(argv[1], "default"))
+            hi->announcements = '?';
+        else {
+            send_message(user, nickserv, "NSMSG_INVALID_ANNOUNCE", argv[1]);
+            return 0;
+        }
+    }
+
+    switch (hi->announcements) {
+    case 'y': choice = user_find_message(user, "MSG_ON"); break;
+    case 'n': choice = user_find_message(user, "MSG_ON"); break;
+    case '?': choice = "default"; break;
+    default: choice = "unknown"; break;
+    }
+    send_message(user, nickserv, "NSMSG_SET_ANNOUNCEMENTS", choice);
+    return 1;
+}
+
+static OPTION_FUNC(opt_password)
+{
+    if (!override) {
+       send_message(user, nickserv, "NSMSG_USE_CMD_PASS");
+       return 0;
+    }
+
+    if (argc > 1)
+       cryptpass(argv[1], hi->passwd);
+
+    send_message(user, nickserv, "NSMSG_SET_PASSWORD", "***");
+    return 1;
+}
+
+static OPTION_FUNC(opt_flags)
+{
+    char flags[33];
+    unsigned int ii, flen;
+
+    if (!override) {
+       send_message(user, nickserv, "MSG_SETTING_PRIVILEGED", argv[0]);
+       return 0;
+    }
+
+    if (argc > 1)
+       nickserv_apply_flags(user, hi, argv[1]);
+
+    for (ii = flen = 0; handle_flags[ii]; ii++)
+        if (hi->flags & (1 << ii))
+            flags[flen++] = handle_flags[ii];
+    flags[flen] = '\0';
+    if (hi->flags)
+        send_message(user, nickserv, "NSMSG_SET_FLAGS", flags);
+    else
+        send_message(user, nickserv, "NSMSG_SET_FLAGS", user_find_message(user, "MSG_NONE"));
+    return 1;
+}
+
+static OPTION_FUNC(opt_email)
+{
+    if (argc > 1) {
+        const char *str;
+        if (!is_valid_email_addr(argv[1])) {
+            send_message(user, nickserv, "NSMSG_BAD_EMAIL_ADDR");
+            return 0;
+        }
+        if ((str = sendmail_prohibited_address(argv[1]))) {
+            send_message(user, nickserv, "NSMSG_EMAIL_PROHIBITED", argv[1], str);
+            return 0;
+        }
+        if (hi->email_addr && !irccasecmp(hi->email_addr, argv[1]))
+            send_message(user, nickserv, "NSMSG_EMAIL_SAME");
+        else if (!override)
+                nickserv_make_cookie(user, hi, EMAIL_CHANGE, argv[1]);
+        else {
+            nickserv_set_email_addr(hi, argv[1]);
+            if (hi->cookie)
+                nickserv_eat_cookie(hi->cookie);
+            send_message(user, nickserv, "NSMSG_SET_EMAIL", visible_email_addr(user, hi));
+        }
+    } else
+        send_message(user, nickserv, "NSMSG_SET_EMAIL", visible_email_addr(user, hi));
+    return 1;
+}
+
+static OPTION_FUNC(opt_maxlogins)
+{
+    char maxlogins;
+    if (argc > 1) {
+        maxlogins = strtoul(argv[1], NULL, 0);
+        if ((maxlogins > nickserv_conf.hard_maxlogins) && !override) {
+            send_message(user, nickserv, "NSMSG_BAD_MAX_LOGINS", nickserv_conf.hard_maxlogins);
+            return 0;
+        }
+        hi->maxlogins = maxlogins;
+    }
+    maxlogins = hi->maxlogins ? hi->maxlogins : nickserv_conf.default_maxlogins;
+    send_message(user, nickserv, "NSMSG_SET_MAXLOGINS", maxlogins);
+    return 1;
+}
+
+static OPTION_FUNC(opt_language)
+{
+    struct language *lang;
+    if (argc > 1) {
+        lang = language_find(argv[1]);
+        if (irccasecmp(lang->name, argv[1]))
+            send_message(user, nickserv, "NSMSG_LANGUAGE_NOT_FOUND", argv[1], lang->name);
+        hi->language = lang;
+    }
+    send_message(user, nickserv, "NSMSG_SET_LANGUAGE", hi->language->name);
+    return 1;
+}
+
+int
+oper_try_set_access(struct userNode *user, struct userNode *bot, struct handle_info *target, unsigned int new_level) {
+    if (!oper_has_access(user, bot, nickserv_conf.modoper_level, 0))
+        return 0;
+    if ((user->handle_info->opserv_level < target->opserv_level)
+        || ((user->handle_info->opserv_level == target->opserv_level)
+            && (user->handle_info->opserv_level < 1000))) {
+        send_message(user, bot, "MSG_USER_OUTRANKED", target->handle);
+        return 0;
+    }
+    if ((user->handle_info->opserv_level < new_level)
+        || ((user->handle_info->opserv_level == new_level)
+            && (user->handle_info->opserv_level < 1000))) {
+        send_message(user, bot, "NSMSG_OPSERV_LEVEL_BAD");
+        return 0;
+    }
+    if (user->handle_info == target) {
+        send_message(user, bot, "MSG_STUPID_ACCESS_CHANGE");
+        return 0;
+    }
+    if (target->opserv_level == new_level)
+        return 0;
+    log_module(NS_LOG, LOG_INFO, "Account %s setting oper level for account %s to %d (from %d).",
+        user->handle_info->handle, target->handle, new_level, target->opserv_level);
+    target->opserv_level = new_level;
+    return 1;
+}
+
+static OPTION_FUNC(opt_level)
+{
+    int res;
+
+    if (!override) {
+       send_message(user, nickserv, "MSG_SETTING_PRIVILEGED", argv[0]);
+       return 0;
+    }
+
+    res = (argc > 1) ? oper_try_set_access(user, nickserv, hi, strtoul(argv[1], NULL, 0)) : 0;
+    send_message(user, nickserv, "NSMSG_SET_LEVEL", hi->opserv_level);
+    return res;
+}
+
+static OPTION_FUNC(opt_epithet)
+{
+    if (!override) {
+        send_message(user, nickserv, "MSG_SETTING_PRIVILEGED", argv[0]);
+        return 0;
+    }
+
+    if ((argc > 1) && oper_has_access(user, nickserv, nickserv_conf.set_epithet_level, 0)) {
+        char *epithet = unsplit_string(argv+1, argc-1, NULL);
+        if (hi->epithet)
+            free(hi->epithet);
+        if ((epithet[0] == '*') && !epithet[1])
+            hi->epithet = NULL;
+        else
+            hi->epithet = strdup(epithet);
+    }
+
+    if (hi->epithet)
+        send_message(user, nickserv, "NSMSG_SET_EPITHET", hi->epithet);
+    else
+        send_message(user, nickserv, "NSMSG_SET_EPITHET", user_find_message(user, "MSG_NONE"));
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_reclaim)
+{
+    struct handle_info *hi;
+    struct nick_info *ni;
+    struct userNode *victim;
+
+    NICKSERV_MIN_PARMS(2);
+    hi = user->handle_info;
+    ni = dict_find(nickserv_nick_dict, argv[1], 0);
+    if (!ni) {
+        reply("NSMSG_UNKNOWN_NICK", argv[1]);
+        return 0;
+    }
+    if (ni->owner != user->handle_info) {
+        reply("NSMSG_NOT_YOUR_NICK", ni->nick);
+        return 0;
+    }
+    victim = GetUserH(ni->nick);
+    if (!victim) {
+        reply("MSG_NICK_UNKNOWN", ni->nick);
+        return 0;
+    }
+    if (victim == user) {
+        reply("NSMSG_NICK_USER_YOU");
+        return 0;
+    }
+    nickserv_reclaim(victim, ni, nickserv_conf.reclaim_action);
+    switch (nickserv_conf.reclaim_action) {
+    case RECLAIM_NONE: reply("NSMSG_RECLAIMED_NONE"); break;
+    case RECLAIM_WARN: reply("NSMSG_RECLAIMED_WARN", victim->nick); break;
+    case RECLAIM_SVSNICK: reply("NSMSG_RECLAIMED_SVSNICK", victim->nick); break;
+    case RECLAIM_KILL: reply("NSMSG_RECLAIMED_KILL", victim->nick); break;
+    }
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_unregnick)
+{
+    const char *nick;
+    struct handle_info *hi;
+    struct nick_info *ni;
+
+    hi = user->handle_info;
+    nick = (argc < 2) ? user->nick : (const char*)argv[1];
+    ni = dict_find(nickserv_nick_dict, nick, NULL);
+    if (!ni) {
+       reply("NSMSG_UNKNOWN_NICK", nick);
+       return 0;
+    }
+    if (hi != ni->owner) {
+       reply("NSMSG_NOT_YOUR_NICK", nick);
+       return 0;
+    }
+    reply("NSMSG_UNREGNICK_SUCCESS", ni->nick);
+    delete_nick(ni);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_ounregnick)
+{
+    struct nick_info *ni;
+
+    NICKSERV_MIN_PARMS(2);
+    if (!(ni = get_nick_info(argv[1]))) {
+       reply("NSMSG_NICK_NOT_REGISTERED", argv[1]);
+       return 0;
+    }
+    if (ni->owner->opserv_level >= user->handle_info->opserv_level) {
+       reply("MSG_USER_OUTRANKED", ni->nick);
+       return 0;
+    }
+    reply("NSMSG_UNREGNICK_SUCCESS", ni->nick);
+    delete_nick(ni);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_unregister)
+{
+    struct handle_info *hi;
+    char *passwd;
+
+    NICKSERV_MIN_PARMS(2);
+    hi = user->handle_info;
+    passwd = argv[1];
+    argv[1] = "****";
+    if (checkpass(passwd, hi->passwd)) {
+        nickserv_unregister_handle(hi, user);
+        return 1;
+    } else {
+       log_module(NS_LOG, LOG_INFO, "Account '%s' tried to unregister with the wrong password.", hi->handle);
+       reply("NSMSG_PASSWORD_INVALID");
+        return 0;
+    }
+}
+
+static NICKSERV_FUNC(cmd_ounregister)
+{
+    struct handle_info *hi;
+
+    NICKSERV_MIN_PARMS(2);
+    if (!(hi = get_victim_oper(user, argv[1])))
+        return 0;
+    nickserv_unregister_handle(hi, user);
+    return 0;
+}
+
+static NICKSERV_FUNC(cmd_status)
+{
+    if (nickserv_conf.disable_nicks) {
+        reply("NSMSG_GLOBAL_STATS_NONICK",
+                        dict_size(nickserv_handle_dict));
+    } else {
+        if (user->handle_info) {
+            int cnt=0;
+            struct nick_info *ni;
+            for (ni=user->handle_info->nicks; ni; ni=ni->next) cnt++;
+            reply("NSMSG_HANDLE_STATS", cnt);
+        } else {
+            reply("NSMSG_HANDLE_NONE");
+        }
+        reply("NSMSG_GLOBAL_STATS",
+              dict_size(nickserv_handle_dict),
+              dict_size(nickserv_nick_dict));
+    }
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_ghost)
+{
+    struct userNode *target;
+    char reason[MAXLEN];
+
+    NICKSERV_MIN_PARMS(2);
+    if (!(target = GetUserH(argv[1]))) {
+        reply("MSG_NICK_UNKNOWN", argv[1]);
+        return 0;
+    }
+    if (target == user) {
+        reply("NSMSG_CANNOT_GHOST_SELF");
+        return 0;
+    }
+    if (!target->handle_info || (target->handle_info != user->handle_info)) {
+        reply("NSMSG_CANNOT_GHOST_USER", target->nick);
+        return 0;
+    }
+    snprintf(reason, sizeof(reason), "Ghost kill on account %s (requested by %s).", target->handle_info->handle, user->nick);
+    DelUser(target, nickserv, 1, reason);
+    reply("NSMSG_GHOST_KILLED", argv[1]);
+    return 1;
+}
+
+static NICKSERV_FUNC(cmd_vacation)
+{
+    HANDLE_SET_FLAG(user->handle_info, FROZEN);
+    reply("NSMSG_ON_VACATION");
+    return 1;
+}
+
+static int
+nickserv_saxdb_write(struct saxdb_context *ctx) {
+    dict_iterator_t it;
+    struct handle_info *hi;
+    char flags[33];
+
+    for (it = dict_first(nickserv_handle_dict); it; it = iter_next(it)) {
+        hi = iter_data(it);
+#ifdef WITH_PROTOCOL_BAHAMUT
+        assert(hi->id);
+#endif
+        saxdb_start_record(ctx, iter_key(it), 0);
+        if (hi->announcements != '?') {
+            flags[0] = hi->announcements;
+            flags[1] = 0;
+            saxdb_write_string(ctx, KEY_ANNOUNCEMENTS, flags);
+        }
+        if (hi->cookie) {
+            struct handle_cookie *cookie = hi->cookie;
+            char *type;
+
+            switch (cookie->type) {
+            case ACTIVATION: type = KEY_ACTIVATION; break;
+            case PASSWORD_CHANGE: type = KEY_PASSWORD_CHANGE; break;
+            case EMAIL_CHANGE: type = KEY_EMAIL_CHANGE; break;
+            case ALLOWAUTH: type = KEY_ALLOWAUTH; break;
+            default: type = NULL; break;
+            }
+            if (type) {
+                saxdb_start_record(ctx, KEY_COOKIE, 0);
+                saxdb_write_string(ctx, KEY_COOKIE_TYPE, type);
+                saxdb_write_int(ctx, KEY_COOKIE_EXPIRES, cookie->expires);
+                if (cookie->data)
+                    saxdb_write_string(ctx, KEY_COOKIE_DATA, cookie->data);
+                saxdb_write_string(ctx, KEY_COOKIE, cookie->cookie);
+                saxdb_end_record(ctx);
+            }
+        }
+        if (hi->email_addr)
+            saxdb_write_string(ctx, KEY_EMAIL_ADDR, hi->email_addr);
+        if (hi->epithet)
+            saxdb_write_string(ctx, KEY_EPITHET, hi->epithet);
+        if (hi->flags) {
+            int ii, flen;
+
+            for (ii=flen=0; handle_flags[ii]; ++ii)
+                if (hi->flags & (1 << ii))
+                    flags[flen++] = handle_flags[ii];
+            flags[flen] = 0;
+            saxdb_write_string(ctx, KEY_FLAGS, flags);
+        }
+#ifdef WITH_PROTOCOL_BAHAMUT
+        saxdb_write_int(ctx, KEY_ID, hi->id);
+#endif
+        if (hi->infoline)
+            saxdb_write_string(ctx, KEY_INFO, hi->infoline);
+        if (hi->last_quit_host[0])
+            saxdb_write_string(ctx, KEY_LAST_QUIT_HOST, hi->last_quit_host);
+        saxdb_write_int(ctx, KEY_LAST_SEEN, hi->lastseen);
+        if (hi->masks->used)
+            saxdb_write_string_list(ctx, KEY_MASKS, hi->masks);
+        if (hi->maxlogins)
+            saxdb_write_int(ctx, KEY_MAXLOGINS, hi->maxlogins);
+        if (hi->nicks) {
+            struct string_list *slist;
+            struct nick_info *ni;
+
+            slist = alloc_string_list(nickserv_conf.nicks_per_handle);
+            for (ni = hi->nicks; ni; ni = ni->next) string_list_append(slist, ni->nick);
+            saxdb_write_string_list(ctx, KEY_NICKS, slist);
+            free(slist->list);
+            free(slist);
+        }
+        if (hi->opserv_level)
+            saxdb_write_int(ctx, KEY_OPSERV_LEVEL, hi->opserv_level);
+        if (hi->language && (hi->language != lang_C))
+            saxdb_write_string(ctx, KEY_LANGUAGE, hi->language->name);
+        saxdb_write_string(ctx, KEY_PASSWD, hi->passwd);
+        saxdb_write_int(ctx, KEY_REGISTER_ON, hi->registered);
+        if (hi->screen_width)
+            saxdb_write_int(ctx, KEY_SCREEN_WIDTH, hi->screen_width);
+        if (hi->table_width)
+            saxdb_write_int(ctx, KEY_TABLE_WIDTH, hi->table_width);
+        flags[0] = hi->userlist_style;
+        flags[1] = 0;
+        saxdb_write_string(ctx, KEY_USERLIST_STYLE, flags);
+        saxdb_end_record(ctx);
+    }
+    return 0;
+}
+
+static handle_merge_func_t *handle_merge_func_list;
+static unsigned int handle_merge_func_size = 0, handle_merge_func_used = 0;
+
+void
+reg_handle_merge_func(handle_merge_func_t func)
+{
+    if (handle_merge_func_used == handle_merge_func_size) {
+        if (handle_merge_func_size) {
+            handle_merge_func_size <<= 1;
+            handle_merge_func_list = realloc(handle_merge_func_list, handle_merge_func_size*sizeof(handle_merge_func_t));
+        } else {
+            handle_merge_func_size = 8;
+            handle_merge_func_list = malloc(handle_merge_func_size*sizeof(handle_merge_func_t));
+        }
+    }
+    handle_merge_func_list[handle_merge_func_used++] = func;
+}
+
+static NICKSERV_FUNC(cmd_merge)
+{
+    struct handle_info *hi_from, *hi_to;
+    struct userNode *last_user;
+    struct userData *cList, *cListNext;
+    unsigned int ii, jj, n;
+    char buffer[MAXLEN];
+
+    NICKSERV_MIN_PARMS(3);
+
+    if (!(hi_from = get_victim_oper(user, argv[1])))
+        return 0;
+    if (!(hi_to = get_victim_oper(user, argv[2])))
+        return 0;
+    if (hi_to == hi_from) {
+        reply("NSMSG_CANNOT_MERGE_SELF", hi_to->handle);
+        return 0;
+    }
+
+    for (n=0; n<handle_merge_func_used; n++)
+        handle_merge_func_list[n](user, hi_to, hi_from);
+
+    /* Append "from" handle's nicks to "to" handle's nick list. */
+    if (hi_to->nicks) {
+        struct nick_info *last_ni;
+        for (last_ni=hi_to->nicks; last_ni->next; last_ni=last_ni->next) ;
+        last_ni->next = hi_from->nicks;
+    }
+    while (hi_from->nicks) {
+        hi_from->nicks->owner = hi_to;
+        hi_from->nicks = hi_from->nicks->next;
+    }
+
+    /* Merge the hostmasks. */
+    for (ii=0; ii<hi_from->masks->used; ii++) {
+        char *mask = hi_from->masks->list[ii];
+        for (jj=0; jj<hi_to->masks->used; jj++)
+            if (match_ircglobs(hi_to->masks->list[jj], mask))
+                break;
+        if (jj==hi_to->masks->used) /* Nothing from the "to" handle covered this mask, so add it. */
+            string_list_append(hi_to->masks, strdup(mask));
+    }
+
+    /* Merge the lists of authed users. */
+    if (hi_to->users) {
+        for (last_user=hi_to->users; last_user->next_authed; last_user=last_user->next_authed) ;
+        last_user->next_authed = hi_from->users;
+    } else {
+        hi_to->users = hi_from->users;
+    }
+    /* Repoint the old "from" handle's users. */
+    for (last_user=hi_from->users; last_user; last_user=last_user->next_authed) {
+        last_user->handle_info = hi_to;
+    }
+    hi_from->users = NULL;
+
+    /* Merge channel userlists. */
+    for (cList=hi_from->channels; cList; cList=cListNext) {
+        struct userData *cList2;
+        cListNext = cList->u_next;
+        for (cList2=hi_to->channels; cList2; cList2=cList2->u_next)
+            if (cList->channel == cList2->channel)
+                break;
+        log_module(NS_LOG, LOG_DEBUG, "Merging %s->%s@%s: before %p->%p->%-p, %p->%p->%p",
+                   hi_from->handle, hi_to->handle, cList->channel->channel->name,
+                   cList->u_prev, cList, cList->u_next,
+                   (cList2?cList2->u_prev:0), cList2, (cList2?cList2->u_next:0));
+        if (cList2 && (cList2->access >= cList->access)) {
+            /* keep cList2 in hi_to; remove cList from hi_from */
+            log_module(NS_LOG, LOG_DEBUG, "Deleting %p", cList);
+            del_channel_user(cList, 1);
+        } else {
+            if (cList2) {
+                /* remove the lower-ranking cList2 from hi_to */
+                log_module(NS_LOG, LOG_DEBUG, "Deleting %p", cList2);
+                del_channel_user(cList2, 1);
+            }
+            /* cList needs to be moved from hi_from to hi_to */
+            cList->handle = hi_to;
+            /* Remove from linked list for hi_from */
+            assert(!cList->u_prev);
+            hi_from->channels = cList->u_next;
+            if (cList->u_next)
+                cList->u_next->u_prev = cList->u_prev;
+            /* Add to linked list for hi_to */
+            cList->u_prev = NULL;
+            cList->u_next = hi_to->channels;
+            if (hi_to->channels)
+                hi_to->channels->u_prev = cList;
+            hi_to->channels = cList;
+            log_module(NS_LOG, LOG_DEBUG, "Now %p->%p->%p",
+                       cList->u_prev, cList, cList->u_next);
+        }
+    }
+
+    /* Do they get an OpServ level promotion? */
+    if (hi_from->opserv_level > hi_to->opserv_level)
+        hi_to->opserv_level = hi_from->opserv_level;
+
+    /* What about last seen time? */
+    if (hi_from->lastseen > hi_to->lastseen)
+        hi_to->lastseen = hi_from->lastseen;
+
+    /* Notify of success. */
+    sprintf(buffer, "%s (%s) merged account %s into %s.", user->nick, user->handle_info->handle, hi_from->handle, hi_to->handle);
+    reply("NSMSG_HANDLES_MERGED", hi_from->handle, hi_to->handle);
+    global_message(MESSAGE_RECIPIENT_STAFF, buffer);
+
+    /* Unregister the "from" handle. */
+    nickserv_unregister_handle(hi_from, NULL);
+
+    return 1;
+}
+
+struct nickserv_discrim {
+    unsigned int limit, min_level, max_level;
+    unsigned long flags_on, flags_off;
+    time_t min_registered, max_registered;
+    time_t lastseen;
+    enum { SUBSET, EXACT, SUPERSET } hostmask_type;
+    const char *nickmask;
+    const char *hostmask;
+    const char *handlemask;
+    const char *emailmask;
+};
+
+typedef void (*discrim_search_func)(struct userNode *source, struct handle_info *hi);
+
+struct discrim_apply_info {
+    struct nickserv_discrim *discrim;
+    discrim_search_func func;
+    struct userNode *source;
+    unsigned int matched;
+};
+
+static struct nickserv_discrim *
+nickserv_discrim_create(struct userNode *user, unsigned int argc, char *argv[])
+{
+    unsigned int i;
+    struct nickserv_discrim *discrim;
+
+    discrim = malloc(sizeof(*discrim));
+    memset(discrim, 0, sizeof(*discrim));
+    discrim->min_level = 0;
+    discrim->max_level = ~0;
+    discrim->limit = 50;
+    discrim->min_registered = 0;
+    discrim->max_registered = INT_MAX;
+    discrim->lastseen = now;
+
+    for (i=0; i<argc; i++) {
+        if (i == argc - 1) {
+            send_message(user, nickserv, "MSG_MISSING_PARAMS", argv[i]);
+            goto fail;
+        }
+        if (!irccasecmp(argv[i], "limit")) {
+            discrim->limit = atoi(argv[++i]);
+        } else if (!irccasecmp(argv[i], "flags")) {
+            nickserv_modify_handle_flags(user, nickserv, argv[++i], &discrim->flags_on, &discrim->flags_off);
+        } else if (!irccasecmp(argv[i], "registered")) {
+            const char *cmp = argv[++i];
+            if (cmp[0] == '<') {
+                if (cmp[1] == '=') {
+                    discrim->min_registered = now - ParseInterval(cmp+2);
+                } else {
+                    discrim->min_registered = now - ParseInterval(cmp+1) + 1;
+                }
+            } else if (cmp[0] == '=') {
+                discrim->min_registered = discrim->max_registered = now - ParseInterval(cmp+1);
+            } else if (cmp[0] == '>') {
+                if (cmp[1] == '=') {
+                    discrim->max_registered = now - ParseInterval(cmp+2);
+                } else {
+                    discrim->max_registered = now - ParseInterval(cmp+1) - 1;
+                }
+            } else {
+                send_message(user, nickserv, "MSG_INVALID_CRITERIA", cmp);
+            }
+        } else if (!irccasecmp(argv[i], "seen")) {
+            discrim->lastseen = now - ParseInterval(argv[++i]);
+        } else if (!nickserv_conf.disable_nicks && !irccasecmp(argv[i], "nickmask")) {
+            discrim->nickmask = argv[++i];
+        } else if (!irccasecmp(argv[i], "hostmask")) {
+            i++;
+            if (!irccasecmp(argv[i], "exact")) {
+                if (i == argc - 1) {
+                    send_message(user, nickserv, "MSG_MISSING_PARAMS", argv[i]);
+                    goto fail;
+                }
+                discrim->hostmask_type = EXACT;
+            } else if (!irccasecmp(argv[i], "subset")) {
+                if (i == argc - 1) {
+                    send_message(user, nickserv, "MSG_MISSING_PARAMS", argv[i]);
+                    goto fail;
+                }
+                discrim->hostmask_type = SUBSET;
+            } else if (!irccasecmp(argv[i], "superset")) {
+                if (i == argc - 1) {
+                    send_message(user, nickserv, "MSG_MISSING_PARAMS", argv[i]);
+                    goto fail;
+                }
+                discrim->hostmask_type = SUPERSET;
+            } else {
+                i--;
+                discrim->hostmask_type = SUPERSET;
+            }
+            discrim->hostmask = argv[++i];
+        } else if (!irccasecmp(argv[i], "handlemask") || !irccasecmp(argv[i], "accountmask")) {
+            if (!irccasecmp(argv[++i], "*")) {
+                discrim->handlemask = 0;
+            } else {
+                discrim->handlemask = argv[i];
+            }
+        } else if (!irccasecmp(argv[i], "email")) {
+            if (user->handle_info->opserv_level < nickserv_conf.email_search_level) {
+                send_message(user, nickserv, "MSG_NO_SEARCH_ACCESS", "email");
+                goto fail;
+            } else if (!irccasecmp(argv[++i], "*")) {
+                discrim->emailmask = 0;
+            } else {
+                discrim->emailmask = argv[i];
+            }
+        } else if (!irccasecmp(argv[i], "access")) {
+            const char *cmp = argv[++i];
+            if (cmp[0] == '<') {
+                if (discrim->min_level == 0) discrim->min_level = 1;
+                if (cmp[1] == '=') {
+                    discrim->max_level = strtoul(cmp+2, NULL, 0);
+                } else {
+                    discrim->max_level = strtoul(cmp+1, NULL, 0) - 1;
+                }
+            } else if (cmp[0] == '=') {
+                discrim->min_level = discrim->max_level = strtoul(cmp+1, NULL, 0);
+            } else if (cmp[0] == '>') {
+                if (cmp[1] == '=') {
+                    discrim->min_level = strtoul(cmp+2, NULL, 0);
+                } else {
+                    discrim->min_level = strtoul(cmp+1, NULL, 0) + 1;
+                }
+            } else {
+                send_message(user, nickserv, "MSG_INVALID_CRITERIA", cmp);
+            }
+        } else {
+            send_message(user, nickserv, "MSG_INVALID_CRITERIA", argv[i]);
+            goto fail;
+        }
+    }
+    return discrim;
+  fail:
+    free(discrim);
+    return NULL;
+}
+
+static int
+nickserv_discrim_match(struct nickserv_discrim *discrim, struct handle_info *hi)
+{
+    if (((discrim->flags_on & hi->flags) != discrim->flags_on)
+        || (discrim->flags_off & hi->flags)
+        || (discrim->min_registered > hi->registered)
+        || (discrim->max_registered < hi->registered)
+        || (discrim->lastseen < (hi->users?now:hi->lastseen))
+        || (discrim->handlemask && !match_ircglob(hi->handle, discrim->handlemask))
+        || (discrim->emailmask && (!hi->email_addr || !match_ircglob(hi->email_addr, discrim->emailmask)))
+        || (discrim->min_level > hi->opserv_level)
+        || (discrim->max_level < hi->opserv_level)) {
+        return 0;
+    }
+    if (discrim->hostmask) {
+        unsigned int i;
+        for (i=0; i<hi->masks->used; i++) {
+            const char *mask = hi->masks->list[i];
+            if ((discrim->hostmask_type == SUBSET)
+                && (match_ircglobs(discrim->hostmask, mask))) break;
+            else if ((discrim->hostmask_type == EXACT)
+                     && !irccasecmp(discrim->hostmask, mask)) break;
+            else if ((discrim->hostmask_type == SUPERSET)
+                     && (match_ircglobs(mask, discrim->hostmask))) break;
+        }
+        if (i==hi->masks->used) return 0;
+    }
+    if (discrim->nickmask) {
+        struct nick_info *nick = hi->nicks;
+        while (nick) {
+            if (match_ircglob(nick->nick, discrim->nickmask)) break;
+            nick = nick->next;
+        }
+        if (!nick) return 0;
+    }
+    return 1;
+}
+
+static unsigned int
+nickserv_discrim_search(struct nickserv_discrim *discrim, discrim_search_func dsf, struct userNode *source)
+{
+    dict_iterator_t it, next;
+    unsigned int matched;
+
+    for (it = dict_first(nickserv_handle_dict), matched = 0;
+         it && (matched < discrim->limit);
+         it = next) {
+        next = iter_next(it);
+        if (nickserv_discrim_match(discrim, iter_data(it))) {
+            dsf(source, iter_data(it));
+            matched++;
+        }
+    }
+    return matched;
+}
+
+static void
+search_print_func(struct userNode *source, struct handle_info *match)
+{
+    send_message(source, nickserv, "NSMSG_SEARCH_MATCH", match->handle);
+}
+
+static void
+search_count_func(UNUSED_ARG(struct userNode *source), UNUSED_ARG(struct handle_info *match))
+{
+}
+
+static void
+search_unregister_func (struct userNode *source, struct handle_info *match)
+{
+    if (oper_has_access(source, nickserv, match->opserv_level, 0))
+        nickserv_unregister_handle(match, source);
+}
+
+static int
+nickserv_sort_accounts_by_access(const void *a, const void *b)
+{
+    const struct handle_info *hi_a = *(const struct handle_info**)a;
+    const struct handle_info *hi_b = *(const struct handle_info**)b;
+    if (hi_a->opserv_level != hi_b->opserv_level)
+        return hi_b->opserv_level - hi_a->opserv_level;
+    return irccasecmp(hi_a->handle, hi_b->handle);
+}
+
+void
+nickserv_show_oper_accounts(struct userNode *user, struct svccmd *cmd)
+{
+    struct handle_info_list hil;
+    struct helpfile_table tbl;
+    unsigned int ii;
+    dict_iterator_t it;
+    const char **ary;
+
+    memset(&hil, 0, sizeof(hil));
+    for (it = dict_first(nickserv_handle_dict); it; it = iter_next(it)) {
+        struct handle_info *hi = iter_data(it);
+        if (hi->opserv_level)
+            handle_info_list_append(&hil, hi);
+    }
+    qsort(hil.list, hil.used, sizeof(hil.list[0]), nickserv_sort_accounts_by_access);
+    tbl.length = hil.used + 1;
+    tbl.width = 2;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = malloc(tbl.length * sizeof(tbl.contents[0]));
+    tbl.contents[0] = ary = malloc(tbl.width * sizeof(ary[0]));
+    ary[0] = "Account";
+    ary[1] = "Level";
+    for (ii = 0; ii < hil.used; ) {
+        ary = malloc(tbl.width * sizeof(ary[0]));
+        ary[0] = hil.list[ii]->handle;
+        ary[1] = strtab(hil.list[ii]->opserv_level);
+        tbl.contents[++ii] = ary;
+    }
+    table_send(cmd->parent->bot, user->nick, 0, NULL, tbl);
+    reply("MSG_MATCH_COUNT", hil.used);
+    for (ii = 0; ii < hil.used; )
+        free(tbl.contents[++ii]);
+    free(tbl.contents);
+    free(hil.list);
+}
+
+static NICKSERV_FUNC(cmd_search)
+{
+    struct nickserv_discrim *discrim;
+    discrim_search_func action;
+    struct svccmd *subcmd;
+    unsigned int matches;
+    char buf[MAXLEN];
+
+    NICKSERV_MIN_PARMS(3);
+    sprintf(buf, "search %s", argv[1]);
+    subcmd = dict_find(nickserv_service->commands, buf, NULL);
+    if (!irccasecmp(argv[1], "print"))
+        action = search_print_func;
+    else if (!irccasecmp(argv[1], "count"))
+        action = search_count_func;
+    else if (!irccasecmp(argv[1], "unregister"))
+        action = search_unregister_func;
+    else {
+        reply("NSMSG_INVALID_ACTION", argv[1]);
+        return 0;
+    }
+
+    if (subcmd && !svccmd_can_invoke(user, nickserv, subcmd, NULL, SVCCMD_NOISY))
+        return 0;
+
+    discrim = nickserv_discrim_create(user, argc-2, argv+2);
+    if (!discrim)
+        return 0;
+
+    if (action == search_print_func)
+        reply("NSMSG_ACCOUNT_SEARCH_RESULTS");
+    else if (action == search_count_func)
+        discrim->limit = INT_MAX;
+
+    matches = nickserv_discrim_search(discrim, action, user);
+
+    if (matches)
+        reply("MSG_MATCH_COUNT", matches);
+    else
+        reply("MSG_NO_MATCHES");
+
+    free(discrim);
+    return 0;
+}
+
+static MODCMD_FUNC(cmd_checkpass)
+{
+    struct handle_info *hi;
+
+    NICKSERV_MIN_PARMS(3);
+    if (!(hi = get_handle_info(argv[1]))) {
+        reply("MSG_HANDLE_UNKNOWN", argv[1]);
+        return 0;
+    }
+    if (checkpass(argv[2], hi->passwd))
+        reply("CHECKPASS_YES");
+    else
+        reply("CHECKPASS_NO");
+    argv[2] = "****";
+    return 1;
+}
+
+static void
+nickserv_db_read_handle(const char *handle, dict_t obj)
+{
+    const char *str;
+    struct string_list *masks, *slist;
+    struct handle_info *hi;
+    struct userNode *authed_users;
+    unsigned long int id;
+    unsigned int ii;
+    dict_t subdb;
+
+    str = database_get_data(obj, KEY_ID, RECDB_QSTRING);
+    id = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(obj, KEY_PASSWD, RECDB_QSTRING);
+    if (!str) {
+        log_module(NS_LOG, LOG_WARNING, "did not find a password for %s -- skipping user.", handle);
+        return;
+    }
+    if ((hi = get_handle_info(handle))) {
+        authed_users = hi->users;
+        hi->users = NULL;
+        dict_remove(nickserv_handle_dict, hi->handle);
+    } else {
+        authed_users = NULL;
+    }
+    hi = register_handle(handle, str, id);
+    if (authed_users) {
+        hi->users = authed_users;
+        while (authed_users) {
+            authed_users->handle_info = hi;
+            authed_users = authed_users->next_authed;
+        }
+    }
+    masks = database_get_data(obj, KEY_MASKS, RECDB_STRING_LIST);
+    hi->masks = masks ? string_list_copy(masks) : alloc_string_list(1);
+    str = database_get_data(obj, KEY_MAXLOGINS, RECDB_QSTRING);
+    hi->maxlogins = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(obj, KEY_LANGUAGE, RECDB_QSTRING);
+    hi->language = language_find(str ? str : "C");
+    str = database_get_data(obj, KEY_OPSERV_LEVEL, RECDB_QSTRING);
+    hi->opserv_level = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(obj, KEY_INFO, RECDB_QSTRING);
+    if (str) hi->infoline = strdup(str);
+    str = database_get_data(obj, KEY_REGISTER_ON, RECDB_QSTRING);
+    hi->registered = str ? (time_t)strtoul(str, NULL, 0) : now;
+    str = database_get_data(obj, KEY_LAST_SEEN, RECDB_QSTRING);
+    hi->lastseen = str ? (time_t)strtoul(str, NULL, 0) : hi->registered;
+    /* We want to read the nicks even if disable_nicks is set.  This is so
+     * that we don't lose the nick data entirely. */
+    slist = database_get_data(obj, KEY_NICKS, RECDB_STRING_LIST);
+    if (slist) {
+        for (ii=0; ii<slist->used; ii++)
+            register_nick(slist->list[ii], hi);
+    }
+    str = database_get_data(obj, KEY_FLAGS, RECDB_QSTRING);
+    if (str) {
+        for (ii=0; str[ii]; ii++)
+            hi->flags |= 1 << (handle_inverse_flags[(unsigned char)str[ii]] - 1);
+    }
+    str = database_get_data(obj, KEY_USERLIST_STYLE, RECDB_QSTRING);
+    hi->userlist_style = str ? str[0] : HI_STYLE_ZOOT;
+    str = database_get_data(obj, KEY_ANNOUNCEMENTS, RECDB_QSTRING);
+    hi->announcements = str ? str[0] : '?';
+    str = database_get_data(obj, KEY_SCREEN_WIDTH, RECDB_QSTRING);
+    hi->screen_width = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(obj, KEY_TABLE_WIDTH, RECDB_QSTRING);
+    hi->table_width = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(obj, KEY_LAST_QUIT_HOST, RECDB_QSTRING);
+    if (!str) str = database_get_data(obj, KEY_LAST_AUTHED_HOST, RECDB_QSTRING);
+    if (str) safestrncpy(hi->last_quit_host, str, sizeof(hi->last_quit_host));
+    str = database_get_data(obj, KEY_EMAIL_ADDR, RECDB_QSTRING);
+    if (str) nickserv_set_email_addr(hi, str);
+    str = database_get_data(obj, KEY_EPITHET, RECDB_QSTRING);
+    if (str) hi->epithet = strdup(str);
+    subdb = database_get_data(obj, KEY_COOKIE, RECDB_OBJECT);
+    if (subdb) {
+        const char *data, *type, *expires, *cookie_str;
+        struct handle_cookie *cookie;
+
+        cookie = calloc(1, sizeof(*cookie));
+        type = database_get_data(subdb, KEY_COOKIE_TYPE, RECDB_QSTRING);
+        data = database_get_data(subdb, KEY_COOKIE_DATA, RECDB_QSTRING);
+        expires = database_get_data(subdb, KEY_COOKIE_EXPIRES, RECDB_QSTRING);
+        cookie_str = database_get_data(subdb, KEY_COOKIE, RECDB_QSTRING);
+        if (!type || !expires || !cookie_str) {
+            log_module(NS_LOG, LOG_ERROR, "Missing field(s) from cookie for account %s; dropping cookie.", hi->handle);
+            goto cookie_out;
+        }
+        if (!irccasecmp(type, KEY_ACTIVATION))
+            cookie->type = ACTIVATION;
+        else if (!irccasecmp(type, KEY_PASSWORD_CHANGE))
+            cookie->type = PASSWORD_CHANGE;
+        else if (!irccasecmp(type, KEY_EMAIL_CHANGE))
+            cookie->type = EMAIL_CHANGE;
+        else if (!irccasecmp(type, KEY_ALLOWAUTH))
+            cookie->type = ALLOWAUTH;
+        else {
+            log_module(NS_LOG, LOG_ERROR, "Invalid cookie type %s for account %s; dropping cookie.", type, handle);
+            goto cookie_out;
+        }
+        cookie->expires = strtoul(expires, NULL, 0);
+        if (cookie->expires < now)
+            goto cookie_out;
+        if (data)
+            cookie->data = strdup(data);
+        safestrncpy(cookie->cookie, cookie_str, sizeof(cookie->cookie));
+        cookie->hi = hi;
+      cookie_out:
+        if (cookie->hi)
+            nickserv_bake_cookie(cookie);
+        else
+            nickserv_free_cookie(cookie);
+    }
+}
+
+static int
+nickserv_saxdb_read(dict_t db) {
+    dict_iterator_t it;
+    struct record_data *rd;
+
+    for (it=dict_first(db); it; it=iter_next(it)) {
+        rd = iter_data(it);
+        nickserv_db_read_handle(iter_key(it), rd->d.object);
+    }
+    return 0;
+}
+
+static NICKSERV_FUNC(cmd_mergedb)
+{
+    struct timeval start, stop;
+    dict_t db;
+
+    NICKSERV_MIN_PARMS(2);
+    gettimeofday(&start, NULL);
+    if (!(db = parse_database(argv[1]))) {
+        reply("NSMSG_DB_UNREADABLE", argv[1]);
+        return 0;
+    }
+    nickserv_saxdb_read(db);
+    free_database(db);
+    gettimeofday(&stop, NULL);
+    stop.tv_sec -= start.tv_sec;
+    stop.tv_usec -= start.tv_usec;
+    if (stop.tv_usec < 0) {
+       stop.tv_sec -= 1;
+       stop.tv_usec += 1000000;
+    }
+    reply("NSMSG_DB_MERGED", argv[1], stop.tv_sec, stop.tv_usec/1000);
+    return 1;
+}
+
+static void
+expire_handles(UNUSED_ARG(void *data))
+{
+    dict_iterator_t it, next;
+    time_t expiry;
+    struct handle_info *hi;
+
+    for (it=dict_first(nickserv_handle_dict); it; it=next) {
+        next = iter_next(it);
+        hi = iter_data(it);
+        if ((hi->opserv_level > 0)
+            || hi->users
+            || HANDLE_FLAGGED(hi, FROZEN)
+            || HANDLE_FLAGGED(hi, NODELETE)) {
+            continue;
+        }
+        expiry = hi->channels ? nickserv_conf.handle_expire_delay : nickserv_conf.nochan_handle_expire_delay;
+        if ((now - hi->lastseen) > expiry) {
+            log_module(NS_LOG, LOG_INFO, "Expiring account %s for inactivity.", hi->handle);
+            nickserv_unregister_handle(hi, NULL);
+        }
+    }
+
+    if (nickserv_conf.handle_expire_frequency)
+        timeq_add(now + nickserv_conf.handle_expire_frequency, expire_handles, NULL);
+}
+
+static void
+nickserv_load_dict(const char *fname)
+{
+    FILE *file;
+    char line[128];
+    if (!(file = fopen(fname, "r"))) {
+        log_module(NS_LOG, LOG_ERROR, "Unable to open dictionary file %s: %s", fname, strerror(errno));
+        return;
+    }
+    while (!feof(file)) {
+        fgets(line, sizeof(line), file);
+        if (!line[0])
+            continue;
+        if (line[strlen(line)-1] == '\n')
+            line[strlen(line)-1] = 0;
+        dict_insert(nickserv_conf.weak_password_dict, strdup(line), NULL);
+    }
+    fclose(file);
+    log_module(NS_LOG, LOG_INFO, "Loaded %d words into weak password dictionary.", dict_size(nickserv_conf.weak_password_dict));
+}
+
+static enum reclaim_action
+reclaim_action_from_string(const char *str) {
+    if (!str)
+        return RECLAIM_NONE;
+    else if (!irccasecmp(str, "warn"))
+        return RECLAIM_WARN;
+    else if (!irccasecmp(str, "svsnick"))
+        return RECLAIM_SVSNICK;
+    else if (!irccasecmp(str, "kill"))
+        return RECLAIM_KILL;
+    else
+        return RECLAIM_NONE;
+}
+
+static void
+nickserv_conf_read(void)
+{
+    dict_t conf_node, child;
+    const char *str;
+    dict_iterator_t it;
+
+    if (!(conf_node = conf_get_data(NICKSERV_CONF_NAME, RECDB_OBJECT))) {
+       log_module(NS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", NICKSERV_CONF_NAME);
+       return;
+    }
+    str = database_get_data(conf_node, KEY_VALID_HANDLE_REGEX, RECDB_QSTRING);
+    if (!str) str = database_get_data(conf_node, KEY_VALID_ACCOUNT_REGEX, RECDB_QSTRING);
+    if (nickserv_conf.valid_handle_regex_set) regfree(&nickserv_conf.valid_handle_regex);
+    if (str) {
+        int err = regcomp(&nickserv_conf.valid_handle_regex, str, REG_EXTENDED|REG_ICASE|REG_NOSUB);
+        nickserv_conf.valid_handle_regex_set = !err;
+        if (err) log_module(NS_LOG, LOG_ERROR, "Bad valid_account_regex (error %d)", err);
+    } else {
+        nickserv_conf.valid_handle_regex_set = 0;
+    }
+    str = database_get_data(conf_node, KEY_VALID_NICK_REGEX, RECDB_QSTRING);
+    if (nickserv_conf.valid_nick_regex_set) regfree(&nickserv_conf.valid_nick_regex);
+    if (str) {
+        int err = regcomp(&nickserv_conf.valid_nick_regex, str, REG_EXTENDED|REG_ICASE|REG_NOSUB);
+        nickserv_conf.valid_nick_regex_set = !err;
+        if (err) log_module(NS_LOG, LOG_ERROR, "Bad valid_nick_regex (error %d)", err);
+    } else {
+        nickserv_conf.valid_nick_regex_set = 0;
+    }
+    str = database_get_data(conf_node, KEY_NICKS_PER_HANDLE, RECDB_QSTRING);
+    if (!str) str = database_get_data(conf_node, KEY_NICKS_PER_ACCOUNT, RECDB_QSTRING);
+    nickserv_conf.nicks_per_handle = str ? strtoul(str, NULL, 0) : 4;
+    str = database_get_data(conf_node, KEY_DISABLE_NICKS, RECDB_QSTRING);
+    nickserv_conf.disable_nicks = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(conf_node, KEY_DEFAULT_HOSTMASK, RECDB_QSTRING);
+    nickserv_conf.default_hostmask = str ? !disabled_string(str) : 0;
+    str = database_get_data(conf_node, KEY_PASSWORD_MIN_LENGTH, RECDB_QSTRING);
+    nickserv_conf.password_min_length = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(conf_node, KEY_PASSWORD_MIN_DIGITS, RECDB_QSTRING);
+    nickserv_conf.password_min_digits = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(conf_node, KEY_PASSWORD_MIN_UPPER, RECDB_QSTRING);
+    nickserv_conf.password_min_upper = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(conf_node, KEY_PASSWORD_MIN_LOWER, RECDB_QSTRING);
+    nickserv_conf.password_min_lower = str ? strtoul(str, NULL, 0) : 0;
+    str = database_get_data(conf_node, KEY_DB_BACKUP_FREQ, RECDB_QSTRING);
+    nickserv_conf.db_backup_frequency = str ? ParseInterval(str) : 7200;
+    str = database_get_data(conf_node, KEY_MODOPER_LEVEL, RECDB_QSTRING);
+    nickserv_conf.modoper_level = str ? strtoul(str, NULL, 0) : 900;
+    str = database_get_data(conf_node, KEY_SET_EPITHET_LEVEL, RECDB_QSTRING);
+    nickserv_conf.set_epithet_level = str ? strtoul(str, NULL, 0) : 1;
+    str = database_get_data(conf_node, KEY_HANDLE_EXPIRE_FREQ, RECDB_QSTRING);
+    if (!str) str = database_get_data(conf_node, KEY_ACCOUNT_EXPIRE_FREQ, RECDB_QSTRING);
+    nickserv_conf.handle_expire_frequency = str ? ParseInterval(str) : 86400;
+    str = database_get_data(conf_node, KEY_HANDLE_EXPIRE_DELAY, RECDB_QSTRING);
+    if (!str) str = database_get_data(conf_node, KEY_ACCOUNT_EXPIRE_DELAY, RECDB_QSTRING);
+    nickserv_conf.handle_expire_delay = str ? ParseInterval(str) : 86400*30;
+    str = database_get_data(conf_node, KEY_NOCHAN_HANDLE_EXPIRE_DELAY, RECDB_QSTRING);
+    if (!str) str = database_get_data(conf_node, KEY_NOCHAN_ACCOUNT_EXPIRE_DELAY, RECDB_QSTRING);
+    nickserv_conf.nochan_handle_expire_delay = str ? ParseInterval(str) : 86400*15;
+    str = database_get_data(conf_node, "warn_clone_auth", RECDB_QSTRING);
+    nickserv_conf.warn_clone_auth = str ? !disabled_string(str) : 1;
+    str = database_get_data(conf_node, "default_maxlogins", RECDB_QSTRING);
+    nickserv_conf.default_maxlogins = str ? strtoul(str, NULL, 0) : 2;
+    str = database_get_data(conf_node, "hard_maxlogins", RECDB_QSTRING);
+    nickserv_conf.hard_maxlogins = str ? strtoul(str, NULL, 0) : 10;
+    if (!nickserv_conf.disable_nicks) {
+        str = database_get_data(conf_node, "reclaim_action", RECDB_QSTRING);
+        nickserv_conf.reclaim_action = str ? reclaim_action_from_string(str) : RECLAIM_NONE;
+        str = database_get_data(conf_node, "warn_nick_owned", RECDB_QSTRING);
+        nickserv_conf.warn_nick_owned = str ? enabled_string(str) : 0;
+        str = database_get_data(conf_node, "auto_reclaim_action", RECDB_QSTRING);
+        nickserv_conf.auto_reclaim_action = str ? reclaim_action_from_string(str) : RECLAIM_NONE;
+        str = database_get_data(conf_node, "auto_reclaim_delay", RECDB_QSTRING);
+        nickserv_conf.auto_reclaim_delay = str ? ParseInterval(str) : 0;
+    }
+    child = database_get_data(conf_node, KEY_FLAG_LEVELS, RECDB_OBJECT);
+    for (it=dict_first(child); it; it=iter_next(it)) {
+        const char *key = iter_key(it), *value;
+        unsigned char flag;
+        int pos;
+
+        if (!strncasecmp(key, "uc_", 3))
+            flag = toupper(key[3]);
+        else if (!strncasecmp(key, "lc_", 3))
+            flag = tolower(key[3]);
+        else
+            flag = key[0];
+
+        if ((pos = handle_inverse_flags[flag])) {
+            value = GET_RECORD_QSTRING((struct record_data*)iter_data(it));
+            flag_access_levels[pos - 1] = strtoul(value, NULL, 0);
+        }
+    }
+    if (nickserv_conf.weak_password_dict)
+        dict_delete(nickserv_conf.weak_password_dict);
+    nickserv_conf.weak_password_dict = dict_new();
+    dict_set_free_keys(nickserv_conf.weak_password_dict, free);
+    dict_insert(nickserv_conf.weak_password_dict, strdup("password"), NULL);
+    dict_insert(nickserv_conf.weak_password_dict, strdup("<password>"), NULL);
+    str = database_get_data(conf_node, KEY_DICT_FILE, RECDB_QSTRING);
+    if (str)
+        nickserv_load_dict(str);
+    str = database_get_data(conf_node, KEY_NICK, RECDB_QSTRING);
+    if (str)
+        NickChange(nickserv, str, 0);
+    str = database_get_data(conf_node, KEY_AUTOGAG_ENABLED, RECDB_QSTRING);
+    nickserv_conf.autogag_enabled = str ? strtoul(str, NULL, 0) : 1;
+    str = database_get_data(conf_node, KEY_AUTOGAG_DURATION, RECDB_QSTRING);
+    nickserv_conf.autogag_duration = str ? ParseInterval(str) : 1800;
+    str = database_get_data(conf_node, KEY_EMAIL_VISIBLE_LEVEL, RECDB_QSTRING);
+    nickserv_conf.email_visible_level = str ? strtoul(str, NULL, 0) : 800;
+    str = database_get_data(conf_node, KEY_EMAIL_ENABLED, RECDB_QSTRING);
+    nickserv_conf.email_enabled = str ? enabled_string(str) : 0;
+    str = database_get_data(conf_node, KEY_COOKIE_TIMEOUT, RECDB_QSTRING);
+    nickserv_conf.cookie_timeout = str ? ParseInterval(str) : 24*3600;
+    str = database_get_data(conf_node, KEY_EMAIL_REQUIRED, RECDB_QSTRING);
+    nickserv_conf.email_required = (nickserv_conf.email_enabled && str) ? enabled_string(str) : 0;
+    str = database_get_data(conf_node, KEY_ACCOUNTS_PER_EMAIL, RECDB_QSTRING);
+    nickserv_conf.handles_per_email = str ? strtoul(str, NULL, 0) : 1;
+    str = database_get_data(conf_node, KEY_EMAIL_SEARCH_LEVEL, RECDB_QSTRING);
+    nickserv_conf.email_search_level = str ? strtoul(str, NULL, 0) : 600;
+    str = conf_get_data("server/network", RECDB_QSTRING);
+    nickserv_conf.network_name = str ? str : "some IRC network";
+    if (!nickserv_conf.auth_policer_params) {
+        nickserv_conf.auth_policer_params = policer_params_new();
+        policer_params_set(nickserv_conf.auth_policer_params, "size", "5");
+        policer_params_set(nickserv_conf.auth_policer_params, "drain-rate", "0.05");
+    }
+    child = database_get_data(conf_node, KEY_AUTH_POLICER, RECDB_OBJECT);
+    for (it=dict_first(child); it; it=iter_next(it))
+        set_policer_param(iter_key(it), iter_data(it), nickserv_conf.auth_policer_params);
+}
+
+static void
+nickserv_reclaim(struct userNode *user, struct nick_info *ni, enum reclaim_action action) {
+    char newnick[NICKLEN+1];
+
+    assert(user);
+    assert(ni);
+    switch (action) {
+    case RECLAIM_NONE:
+        /* do nothing */
+        break;
+    case RECLAIM_WARN:
+        send_message(user, nickserv, "NSMSG_RECLAIM_WARN", ni->nick, ni->owner->handle);
+        break;
+    case RECLAIM_SVSNICK:
+        do {
+            snprintf(newnick, sizeof(newnick), "Guest%d", rand()%10000);
+        } while (GetUserH(newnick));
+        irc_svsnick(nickserv, user, newnick);
+        break;
+    case RECLAIM_KILL:
+        irc_kill(nickserv, user, "NSMSG_RECLAIM_KILL");
+        break;
+    }
+}
+
+static void
+nickserv_reclaim_p(void *data) {
+    struct userNode *user = data;
+    struct nick_info *ni = get_nick_info(user->nick);
+    if (ni)
+        nickserv_reclaim(user, ni, nickserv_conf.auto_reclaim_action);
+}
+
+static int
+check_user_nick(struct userNode *user) {
+    struct nick_info *ni;
+    user->modes &= ~FLAGS_REGNICK;
+    if (!(ni = get_nick_info(user->nick)))
+        return 0;
+    if (user->handle_info == ni->owner) {
+        user->modes |= FLAGS_REGNICK;
+        irc_regnick(user);
+        return 0;
+    }
+    if (nickserv_conf.warn_nick_owned)
+        send_message(user, nickserv, "NSMSG_RECLAIM_WARN", ni->nick, ni->owner->handle);
+    if (nickserv_conf.auto_reclaim_action == RECLAIM_NONE)
+        return 0;
+    if (nickserv_conf.auto_reclaim_delay)
+        timeq_add(now + nickserv_conf.auto_reclaim_delay, nickserv_reclaim_p, user);
+    else
+        nickserv_reclaim(user, ni, nickserv_conf.auto_reclaim_action);
+    return 0;
+}
+
+int
+handle_new_user(struct userNode *user)
+{
+    return check_user_nick(user);
+}
+
+void
+handle_account(struct userNode *user, const char *stamp)
+{
+    struct handle_info *hi;
+
+#ifdef WITH_PROTOCOL_P10
+    hi = dict_find(nickserv_handle_dict, stamp, NULL);
+#else
+    hi = dict_find(nickserv_id_dict, stamp, NULL);
+#endif
+
+    if (hi) {
+        if (HANDLE_FLAGGED(hi, SUSPENDED)) {
+            return;
+        }
+        set_user_handle_info(user, hi, 0);
+    } else {
+        log_module(MAIN_LOG, LOG_WARNING, "%s had unknown account stamp %s.", user->nick, stamp);
+    }
+}
+
+void
+handle_nick_change(struct userNode *user, const char *old_nick)
+{
+    struct handle_info *hi;
+
+    if ((hi = dict_find(nickserv_allow_auth_dict, old_nick, 0))) {
+        dict_remove(nickserv_allow_auth_dict, old_nick);
+        dict_insert(nickserv_allow_auth_dict, user->nick, hi);
+    }
+    timeq_del(0, nickserv_reclaim_p, user, TIMEQ_IGNORE_WHEN);
+    check_user_nick(user);
+}
+
+void
+nickserv_remove_user(struct userNode *user, UNUSED_ARG(struct userNode *killer), UNUSED_ARG(const char *why))
+{
+    dict_remove(nickserv_allow_auth_dict, user->nick);
+    timeq_del(0, nickserv_reclaim_p, user, TIMEQ_IGNORE_WHEN);
+    set_user_handle_info(user, NULL, 0);
+}
+
+static struct modcmd *
+nickserv_define_func(const char *name, modcmd_func_t func, int min_level, int must_auth, int must_be_qualified)
+{
+    if (min_level > 0) {
+        char buf[16];
+        sprintf(buf, "%u", min_level);
+        if (must_be_qualified) {
+            return modcmd_register(nickserv_module, name, func, 1, (must_auth ? MODCMD_REQUIRE_AUTHED : 0), "level", buf, "flags", "+qualified,+loghostmask", NULL);
+        } else {
+            return modcmd_register(nickserv_module, name, func, 1, (must_auth ? MODCMD_REQUIRE_AUTHED : 0), "level", buf, NULL);
+        }
+    } else if (min_level == 0) {
+        if (must_be_qualified) {
+            return modcmd_register(nickserv_module, name, func, 1, (must_auth ? MODCMD_REQUIRE_AUTHED : 0), "flags", "+helping", NULL);
+        } else {
+            return modcmd_register(nickserv_module, name, func, 1, (must_auth ? MODCMD_REQUIRE_AUTHED : 0), "flags", "+helping", NULL);
+        }
+    } else {
+        if (must_be_qualified) {
+            return modcmd_register(nickserv_module, name, func, 1, (must_auth ? MODCMD_REQUIRE_AUTHED : 0), "flags", "+qualified,+loghostmask", NULL);
+        } else {
+            return modcmd_register(nickserv_module, name, func, 1, (must_auth ? MODCMD_REQUIRE_AUTHED : 0), NULL);
+        }
+    }
+}
+
+static void
+nickserv_db_cleanup(void)
+{
+    unreg_del_user_func(nickserv_remove_user);
+    userList_clean(&curr_helpers);
+    policer_params_delete(nickserv_conf.auth_policer_params);
+    dict_delete(nickserv_handle_dict);
+    dict_delete(nickserv_nick_dict);
+    dict_delete(nickserv_opt_dict);
+    dict_delete(nickserv_allow_auth_dict);
+    dict_delete(nickserv_email_dict);
+    dict_delete(nickserv_id_dict);
+    dict_delete(nickserv_conf.weak_password_dict);
+    free(auth_func_list);
+    free(unreg_func_list);
+    free(rf_list);
+    free(allowauth_func_list);
+    free(handle_merge_func_list);
+    free(failpw_func_list);
+    if (nickserv_conf.valid_handle_regex_set)
+        regfree(&nickserv_conf.valid_handle_regex);
+    if (nickserv_conf.valid_nick_regex_set)
+        regfree(&nickserv_conf.valid_nick_regex);
+}
+
+void
+init_nickserv(const char *nick)
+{
+    unsigned int i;
+    nickserv = AddService(nick, "Nick Services");
+    NS_LOG = log_register_type("NickServ", "file:nickserv.log");
+    reg_new_user_func(handle_new_user);
+    reg_nick_change_func(handle_nick_change);
+    reg_del_user_func(nickserv_remove_user);
+    reg_account_func(handle_account);
+
+    /* set up handle_inverse_flags */
+    memset(handle_inverse_flags, 0, sizeof(handle_inverse_flags));
+    for (i=0; handle_flags[i]; i++) {
+        handle_inverse_flags[(unsigned char)handle_flags[i]] = i + 1;
+        flag_access_levels[i] = 0;
+    }
+
+    conf_register_reload(nickserv_conf_read);
+    nickserv_opt_dict = dict_new();
+    nickserv_email_dict = dict_new();
+    dict_set_free_keys(nickserv_email_dict, free);
+    dict_set_free_data(nickserv_email_dict, nickserv_free_email_addr);
+
+    nickserv_module = module_register("NickServ", NS_LOG, "nickserv.help", NULL);
+    modcmd_register(nickserv_module, "AUTH", cmd_auth, 2, MODCMD_KEEP_BOUND, "flags", "+qualified,+loghostmask", NULL);
+    nickserv_define_func("ALLOWAUTH", cmd_allowauth, 0, 1, 0);
+    nickserv_define_func("REGISTER", cmd_register, -1, 0, 1);
+    nickserv_define_func("OREGISTER", cmd_oregister, 0, 1, 0);
+    nickserv_define_func("UNREGISTER", cmd_unregister, -1, 1, 1);
+    nickserv_define_func("OUNREGISTER", cmd_ounregister, 0, 1, 0);
+    nickserv_define_func("ADDMASK", cmd_addmask, -1, 1, 0);
+    nickserv_define_func("OADDMASK", cmd_oaddmask, 0, 1, 0);
+    nickserv_define_func("DELMASK", cmd_delmask, -1, 1, 0);
+    nickserv_define_func("ODELMASK", cmd_odelmask, 0, 1, 0);
+    nickserv_define_func("PASS", cmd_pass, -1, 1, 1);
+    nickserv_define_func("SET", cmd_set, -1, 1, 0);
+    nickserv_define_func("OSET", cmd_oset, 0, 1, 0);
+    nickserv_define_func("ACCOUNTINFO", cmd_handleinfo, -1, 0, 0);
+    nickserv_define_func("USERINFO", cmd_userinfo, -1, 1, 0);
+    nickserv_define_func("RENAME", cmd_rename_handle, -1, 1, 0);
+    nickserv_define_func("VACATION", cmd_vacation, -1, 1, 0);
+    nickserv_define_func("MERGE", cmd_merge, 0, 1, 0);
+    if (!nickserv_conf.disable_nicks) {
+       /* nick management commands */
+       nickserv_define_func("REGNICK", cmd_regnick, -1, 1, 0);
+       nickserv_define_func("OREGNICK", cmd_oregnick, 0, 1, 0);
+       nickserv_define_func("UNREGNICK", cmd_unregnick, -1, 1, 0);
+       nickserv_define_func("OUNREGNICK", cmd_ounregnick, 0, 1, 0);
+       nickserv_define_func("NICKINFO", cmd_nickinfo, -1, 1, 0);
+        nickserv_define_func("RECLAIM", cmd_reclaim, -1, 1, 0);
+    }
+    if (nickserv_conf.email_enabled) {
+        nickserv_define_func("AUTHCOOKIE", cmd_authcookie, -1, 0, 0);
+        nickserv_define_func("RESETPASS", cmd_resetpass, -1, 0, 1);
+        nickserv_define_func("COOKIE", cmd_cookie, -1, 0, 1);
+        nickserv_define_func("DELCOOKIE", cmd_delcookie, -1, 1, 0);
+        dict_insert(nickserv_opt_dict, "EMAIL", opt_email);
+    }
+    nickserv_define_func("GHOST", cmd_ghost, -1, 1, 0);
+    /* miscellaneous commands */
+    nickserv_define_func("STATUS", cmd_status, -1, 0, 0);
+    nickserv_define_func("SEARCH", cmd_search, 100, 1, 0);
+    nickserv_define_func("SEARCH UNREGISTER", NULL, 800, 1, 0);
+    nickserv_define_func("MERGEDB", cmd_mergedb, 999, 1, 0);
+    nickserv_define_func("CHECKPASS", cmd_checkpass, 601, 1, 0);
+    /* other options */
+    dict_insert(nickserv_opt_dict, "INFO", opt_info);
+    dict_insert(nickserv_opt_dict, "WIDTH", opt_width);
+    dict_insert(nickserv_opt_dict, "TABLEWIDTH", opt_tablewidth);
+    dict_insert(nickserv_opt_dict, "COLOR", opt_color);
+    dict_insert(nickserv_opt_dict, "PRIVMSG", opt_privmsg);
+    dict_insert(nickserv_opt_dict, "STYLE", opt_style);
+    dict_insert(nickserv_opt_dict, "PASS", opt_password);
+    dict_insert(nickserv_opt_dict, "PASSWORD", opt_password);
+    dict_insert(nickserv_opt_dict, "FLAGS", opt_flags);
+    dict_insert(nickserv_opt_dict, "ACCESS", opt_level);
+    dict_insert(nickserv_opt_dict, "LEVEL", opt_level);
+    dict_insert(nickserv_opt_dict, "EPITHET", opt_epithet);
+    dict_insert(nickserv_opt_dict, "ANNOUNCEMENTS", opt_announcements);
+    dict_insert(nickserv_opt_dict, "MAXLOGINS", opt_maxlogins);
+    dict_insert(nickserv_opt_dict, "LANGUAGE", opt_language);
+
+    nickserv_handle_dict = dict_new();
+    dict_set_free_keys(nickserv_handle_dict, free);
+    dict_set_free_data(nickserv_handle_dict, free_handle_info);
+
+    nickserv_id_dict = dict_new();
+    dict_set_free_keys(nickserv_id_dict, free);
+
+    nickserv_nick_dict = dict_new();
+    dict_set_free_data(nickserv_nick_dict, free_nick_info);
+
+    nickserv_allow_auth_dict = dict_new();
+
+    userList_init(&curr_helpers);
+
+    nickserv_service = service_register(nickserv, 0);
+    saxdb_register("NickServ", nickserv_saxdb_read, nickserv_saxdb_write);
+    reg_exit_func(nickserv_db_cleanup);
+    if(nickserv_conf.handle_expire_frequency)
+        timeq_add(now + nickserv_conf.handle_expire_frequency, expire_handles, NULL);
+    message_register_table(msgtab);
+}
diff --git a/src/nickserv.h b/src/nickserv.h
new file mode 100644 (file)
index 0000000..0a5862b
--- /dev/null
@@ -0,0 +1,156 @@
+/* nickserv.h - Nick/authentiction service
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef _nickserv_h
+#define _nickserv_h
+
+#include "hash.h"   /* for NICKLEN, etc., and common.h */
+struct svccmd;
+
+#define NICKSERV_HANDLE_LEN NICKLEN
+#define COOKIELEN 10
+
+/* HI_FLAG_* go into handle_info.flags */
+#define HI_FLAG_OPER_SUSPENDED 0x00000001
+#define HI_FLAG_USE_PRIVMSG    0x00000002
+#define HI_FLAG_SUPPORT_HELPER 0x00000004
+#define HI_FLAG_HELPING        0x00000008
+#define HI_FLAG_SUSPENDED      0x00000010
+#define HI_FLAG_MIRC_COLOR     0x00000020
+#define HI_FLAG_FROZEN         0x00000040
+#define HI_FLAG_NODELETE       0x00000080
+#define HI_FLAG_NETWORK_HELPER 0x00000100
+#define HI_FLAG_BOT            0x00000200
+/* Flag characters for the above.  First char is LSB, etc. */
+#define HANDLE_FLAGS "SphgscfnHb"
+
+/* HI_STYLE_* go into handle_info.userlist_style */
+#define HI_STYLE_DEF          'd'
+#define HI_STYLE_ZOOT         'Z'
+
+#define HI_DEFAULT_FLAGS       (HI_FLAG_MIRC_COLOR)
+#define HI_DEFAULT_STYLE       HI_STYLE_DEF
+
+#define HANDLE_FLAGGED(hi, tok) ((hi)->flags & HI_FLAG_##tok)
+#define HANDLE_SET_FLAG(hi, tok) ((hi)->flags |= HI_FLAG_##tok)
+#define HANDLE_TOGGLE_FLAG(hi, tok) ((hi)->flags ^= HI_FLAG_##tok)
+#define HANDLE_CLEAR_FLAG(hi, tok) ((hi)->flags &= ~HI_FLAG_##tok)
+
+#define IsSupportHelper(user) (user->handle_info && HANDLE_FLAGGED(user->handle_info, SUPPORT_HELPER))
+#define IsNetworkHelper(user) (user->handle_info && HANDLE_FLAGGED(user->handle_info, NETWORK_HELPER))
+#define IsHelper(user) (IsSupportHelper(user) || IsNetworkHelper(user))
+#define IsHelping(user) (user->handle_info && HANDLE_FLAGGED(user->handle_info, HELPING))
+#define IsStaff(user) (IsOper(user) || IsSupportHelper(user) || IsNetworkHelper(user))
+#define IsBot(user) (user->handle_info && HANDLE_FLAGGED(user->handle_info, BOT))
+
+enum cookie_type {
+    ACTIVATION,
+    PASSWORD_CHANGE,
+    EMAIL_CHANGE,
+    ALLOWAUTH
+};
+
+struct handle_cookie {
+    struct handle_info *hi;
+    char *data;
+    enum cookie_type type;
+    time_t expires;
+    char cookie[COOKIELEN+1];
+};
+
+struct handle_info {
+    struct nick_info *nicks;
+    struct string_list *masks;
+    struct userNode *users;
+    struct userData *channels;
+    struct handle_cookie *cookie;
+    struct language *language;
+    char *email_addr;
+    char *epithet;
+    char *infoline;
+    char *handle;
+#ifdef WITH_PROTOCOL_BAHAMUT
+    unsigned long id;
+#endif
+    time_t registered;
+    time_t lastseen;
+    unsigned short flags;
+    unsigned short opserv_level;
+    unsigned short screen_width;
+    unsigned short table_width;
+    unsigned char userlist_style;
+    unsigned char announcements;
+    unsigned char maxlogins;
+    char passwd[MD5_CRYPT_LENGTH+1];
+    char last_quit_host[USERLEN+HOSTLEN+2];
+};
+
+struct nick_info {
+    struct handle_info *owner;
+    struct nick_info *next; /* next nick owned by same handle */
+    char nick[NICKLEN+1];
+};
+
+struct handle_info_list {
+    unsigned int used, size;
+    struct handle_info **list;
+    char *tag; /* e.g. email address */
+};
+
+extern const char *handle_flags;
+
+void init_nickserv(const char *nick);
+struct handle_info *get_handle_info(const char *handle);
+struct handle_info *smart_get_handle_info(struct userNode *service, struct userNode *user, const char *name);
+int oper_try_set_access(struct userNode *user, struct userNode *bot, struct handle_info *target, unsigned int new_level);
+int oper_outranks(struct userNode *user, struct handle_info *hi);
+struct nick_info *get_nick_info(const char *nick);
+struct modeNode *find_handle_in_channel(struct chanNode *channel, struct handle_info *handle, struct userNode *except);
+int nickserv_modify_handle_flags(struct userNode *user, struct userNode *bot, const char *str, unsigned long *add, unsigned long *remove);
+int oper_has_access(struct userNode *user, struct userNode *bot, unsigned int min_level, unsigned int quiet);
+void nickserv_show_oper_accounts(struct userNode *user, struct svccmd *cmd);
+
+/* auth_funcs are called when a user gets a new handle_info.  They are
+ * called *after* user->handle_info has been updated.  */
+typedef void (*auth_func_t)(struct userNode *user, struct handle_info *old_handle);
+void reg_auth_func(auth_func_t func);
+
+/* Called just after a handle is renamed. */
+typedef void (*handle_rename_func_t)(struct handle_info *handle, const char *old_handle);
+void reg_handle_rename_func(handle_rename_func_t func);
+
+/* unreg_funcs are called right before a handle is unregistered.
+ * `user' is the person who caused the handle to be unregistered (either a
+ * client authed to the handle, or an oper). */
+typedef void (*unreg_func_t)(struct userNode *user, struct handle_info *handle);
+void reg_unreg_func(unreg_func_t func);
+
+/* Called just before a handle is merged */
+typedef void (*handle_merge_func_t)(struct userNode *user, struct handle_info *handle_to, struct handle_info *handle_from);
+void reg_handle_merge_func(handle_merge_func_t);
+
+/* Called after an allowauth. handle is null if allowauth authorization was
+ * removed */
+typedef void (*allowauth_func_t)(struct userNode *user, struct userNode *target, struct handle_info *handle);
+void reg_allowauth_func(allowauth_func_t func);
+
+/* Called when an auth attempt fails because of a bad password */
+typedef void (*failpw_func_t)(struct userNode *user, struct handle_info *handle);
+void reg_failpw_func(failpw_func_t func);
+
+#endif
diff --git a/src/nickserv.help.m4 b/src/nickserv.help.m4
new file mode 100644 (file)
index 0000000..9010624
--- /dev/null
@@ -0,0 +1,274 @@
+changequote({,})
+"<INDEX>" ("$b$N Help$b",
+        "$b$N$b is a nickname and authentication service, intended to serve as a central authentication point for all other network services. $b$C$b, $b$O$b, and $b$G$b all depend on $b$N$b to verify that users are valid. The other component allows for ownership of a nickname, but is not necessarily enabled.",
+       "$b$N$b command categories:",
+       "  ACCOUNT    Account management.",
+ifdef({/services/nickserv/disable_nicks},
+{        "  NOT NICKSERV   A note on what this service does and does not do.",},
+{        "  NICK       Nick management.",})
+ifdef({/services/nickserv/email_enabled},
+{        "  EMAIL      Email maintenance commands",})
+       "  OTHERS     Other functions.",
+        "  COMMANDS   A list of all available commands.");
+
+"HANDLE" ("The term $uhandle$u from earlier versions was confusing to many new users.  Therefore, it has been changed to $uaccount$u.");
+
+"ACCOUNT" ("Accounts are the way that $b$C$b identifies you for access to channels.  They are slightly similar to IRC nicks, but only have meaning to the services bots.  Until you authenticate to $b$N$b on an account, you can only use the $bREGISTER$b and $bAUTH$b commands.",
+        "Account management commands are:",
+        "  REGISTER   Register a new account.",
+        "  AUTH       Authenticate yourself to $b$N$b using an existing account.",
+        "  PASS       Change your account's password.",
+        "  ADDMASK    Add a hostmask to your account.",
+        "  DELMASK    Remove a hostmask from your account.",
+        "  SET        Set per-account options.",
+        "  UNREGISTER Unregister an account.",
+        "  RENAME     Renames an account",
+ifdef({/services/nickserv/enable_ghost},
+{        "  GHOST      Disconnects your old clients",})
+        "  ACCOUNT FLAGS Definition for each account flag");
+
+ifdef({/services/nickserv/disable_nicks},
+{"NOT NICKSERV" ("$bNOT NICKSERV$b",
+        "This $b$N$b is not a standard NickServ.",
+        "Most NickServs provide \"nick ownership\", and will either issue a /KILL or a forced nick change if you try to use a registered nick without providing the password.",
+        "This $b$N$b will not do this.  It only allows you to register an $baccount$b, which identifies users to $b$C$b.  In a way, it is a virtual nick.  When you authenticate to $b$N$b, it does not care what your IRC nick is -- only account you are logged in as.",
+        "$b$N$b can tell you what account a user is authenticated to using the $bUSERINFO$b command.  Any problems with account registration or $b$N$b should be directed to the normal support channel.");
+
+"OUNREGISTER" ("/msg $N OUNREGISTER <nick|*account>",
+        "Un-registers the specified account.",
+        "You may use *Account instead of Nick as the name argument; the * makes $N use the name of an account directly (useful if the user is not online).",
+        "$uSee Also:$u oregister");
+
+"UNREGISTER" ("/msg $N@$s UNREGISTER <password>",
+        "Un-registers the account you are authenticated as.",
+        "$uSee Also:$u register");},
+{"NICK" ("You may register IRC nicknames to be associated with your accounts, and will be able to request a KILL for anyone using a nickname registered to you.",
+       "Nick management commands are:",
+        "  NICKINFO   Find out who has registered a nick.",
+        "  REGNICK    Register a nickname.",
+        "  UNREGNICK  Unregister a nickname.",
+        "  RECLAIM    Reclaim a nick registered to you.");
+
+"NICKINFO" ("/msg $N NICKINFO <nick>",
+        "Displays information on the nick specified.",
+        "$uSee Also:$u accountinfo, userinfo");
+
+"REGNICK" ("/msg $N REGNICK ",
+        "Registers your current nick to the account you are authenticated to.",
+        "$uSee Also:$u register, unregister, unregnick");
+
+"OUNREGISTER" ("/msg $N OUNREGISTER <nick|*account>",
+        "Un-registers the specified account, and any nicks that have been registered to that account.",
+        "You may use *Account instead of Nick as the name argument; the * makes $N use the name of an account directly (useful if the user is not online).",
+        "$uSee Also:$u oregister, oregnick, ounregnick");
+
+"OREGNICK" ("/msg $N OREGNICK [<nick|*account> <nick>]",
+        "Registers specified nick to the specified account. If nick and account are not specified, then $boregnick$b registers your current nick to the account you are authenticated to.",
+        "You may use *Account instead of Nick as the name argument; the * makes $N use the name of an account directly (useful if the user is not online).",
+        "$uSee Also:$u oregister, ounregister, ounregnick");
+
+"OUNREGNICK" ("/msg $N OUNREGNICK <nick>",
+        "Un-registers a nick that was previously registered to an account.",
+        "$uSee Also:$u oregister, oregnick, ounregister");
+
+"UNREGISTER" ("/msg $N@$s UNREGISTER <password>",
+        "Un-registers the account you are authenticated with, and any nicks that have been registered to that account.",
+        "$uSee Also:$u register, regnick, unregnick");
+
+"UNREGNICK" ("/msg $N UNREGNICK [nick]",
+        "Un-registers a nick that was previously registered to your account.  If you do not specify a nick, your current nick will be un-registered.",
+        "$uSee Also:$u register, regnick, unregister");
+
+"RECLAIM" ("/msg $N RECLAIM <nick>",
+        "Reclaims the specified nick. You must be authenticated to the account that registered the nick.",
+        "Depending on configuration, this may do nothing, may ask the user nicely, may force a nick change on them, or may /KILL (disconnect) the target user.");})
+
+ifdef({/services/nickserv/email_enabled},
+{"EMAIL" ("Email-based maintenance commands and topics are:",
+        "  AUTHCOOKIE Email a cookie to allow you to authenticate (auth) without a matching hostmask.",
+        "  RESETPASS  Request a password change if you forgot your old password.",
+        "  COOKIE     Complete an email-based maintenance action.",
+        "  DELCOOKIE  For AUTHCOOKIE or RESETPASS, cancel the requested cookie.",
+        "  EMAIL POLICY  This network's policy on account email addresses.");
+
+"AUTHCOOKIE" ("/msg $N AUTHCOOKIE <account>",
+        "Requests that $N send you email with a cookie that allows you to auth to your account if you do not have a matching hostmask.  (For example, if your ISP changed your IP or hostname.)",
+        "Once you receive the cookie in email, you can use the $bcookie$b command to log in.",
+        "$uSee Also:$u cookie, delcookie");
+
+"RESETPASS" ("/msg $N@$s RESETPASS <account> <newpassword>",
+        "Requests that $N send you email with a cookie that will change your password (in case you have forgotten it).  Once you receive the cookie in email, use the $bcookie$b command to actually change your password.",
+        "$bYour password will not be changed, and you will not be able to use it to log in, until you confirm the change using the $ucookie$u command.$b",
+        "$uSee Also:$u cookie, delcookie");
+
+"DELCOOKIE" ("/msg $N DELCOOKIE",
+        "Requests that $N cancel your authcookie or resetpass cookie.",
+        "(Since set-email cookies and registration cookies send email to unverified addresses, to prevent mail flooding, they cannot be cancelled.)",
+        "$uSee Also:$u authcookie, resetpass, cookie");
+
+"COOKIE" ("/msg $N@$s COOKIE <account> <cookie>",
+        "Completes the maintenance action (for example, activating an account or changing your password) for which a cookie was issued.  The cookie will then be forgotten.",
+        "$uSee Also:$u authcookie, resetpass, set, delcookie");
+
+"EMAIL POLICY" ("$bEMAIL POLICY",
+        "FooNET has utmost respect for the privacy of its users.  We will submit your email address to as many spam databases as we can find, and we will even post it on our web site.",
+        "(No, not really.  It looks like somebody forgot to edit nickserv.help or nickserv.help.m4 to remove this entry.  Make sure they edit the mail section of srvx.conf while they are at it.)");})
+
+"OTHERS" ("Other commands are:",
+        "  USERINFO    Displays the account a user is authenticated to.",
+        "  ACCOUNTINFO Displays information about an account.",
+        "  VERSION     $b$N$b version information.",
+        "  STATUS      $b$N$b status.",
+        "  SEARCH      Search for accounts by various criteria.",
+        "  MERGE       Merge one account into another.",
+        "  MERGEDB     Load a database into memory.",
+        "  HELP        Get help on $b$N$b.");
+
+"ADDMASK" ("/msg $N ADDMASK [user@host]",
+        "Adds the specified user@host to the account you are authenticated to with $b$N$b.  If no mask is given, it uses your current mask.",
+        "$uSee Also:$u auth, delmask");
+"ALLOWAUTH" ("/msg $N ALLOWAUTH <nick> [account] [STAFF]",
+        "Allows the specified nick to $bauth$b to the specified account. $bAllowauth$b does NOT add the hostmask of that nick to the specified account.",
+        "If no account is given, it will cancel the allowauth for the user (assuming the user has an allowauth).",
+        "If the account is marked as a helper or oper, the STAFF qualifier must be given afterwards.  This reduces social engineering attacks.",
+        "$uSee Also:$u addmask, auth");
+"AUTH" ("/msg $N@$s AUTH [account] <password>",
+        "Authenticates yourself with $b$N$b to the specified account. You must use $bauth$b before you have any access to network services, including channels that are registered with $b$C$b.",
+        "If you omit the account, it uses your current nick as your account name.",
+ifdef({/services/nickserv/email_enabled},
+{        "$uSee Also:$u pass, resetpass, authcookie"},
+{        "$uSee Also:$u pass"})
+);
+"DELMASK" ("/msg $N DELMASK <user@host>",
+        "Removes a hostmask from the account you are authenticated on.",
+        "An account must have at least one hostmask; you cannot remove the last mask for an account.",
+        "$uSee Also:$u addmask");
+"ACCOUNTINFO" ("/msg $N ACCOUNTINFO <nick|*account>",
+        "Displays infomation on the specified account, including the date the account was registered, the last time that person was seen, the account's $b$N$b info, its flags, its hostmask(s), its channels, and the account's current nickname.",
+        "You may use *Account instead of Nick as the name argument; the * makes $N use the name of an account directly (useful if the user is not online).",
+ifdef({/services/nickserv/disable_nicks},
+{        "$uSee Also:$u userinfo, account flags"},
+{        "$uSee Also:$u nickinfo, userinfo, account flags"}));
+"ACCOUNT FLAGS" ("$bACCOUNT FLAGS$b",
+        "The following flags on accounts are defined:",
+        "$bS$b  $O access suspended",
+        "$bp$b  Use PRIVMSG for messages rather than NOTICE",
+        "$bh$b  User is a support helper (must be in support channel to override security)",
+        "$bH$b  User is a network helper (can toggle security override)",
+        "$bg$b  God mode (security override for IRC staff)",
+        "$bs$b  Account suspended",
+        "$bc$b  Use mIRC color codes in responses",
+        "$bf$b  Account frozen/on vacation (will not be unregistered for inactivity; cleared when account is authenticated against)",
+        "$bn$b  No-delete (will never be unregistered for inactivity)",
+        "$uSee Also:$u accountinfo, set");
+"OADDMASK" ("/msg $N OADDMASK <nick|*account> <user@host>",
+        "Adds a hostmask to the specified account.",
+        "You may use *Account instead of Nick as the name argument; the * makes $N use the name of an account directly (useful if the user is not online).",
+        "$uSee Also:$u odelmask");
+"ODELMASK" ("/msg $N ODELMASK <nick|*account> <user@host>",
+        "Removes a hostmask from the specified account.",
+        "An account must have at least one hostmask; you cannot remove the last mask for an account.",
+        "You may use *Account instead of Nick as the name argument; the * makes $N use the name of an account directly (useful if the user is not online).",
+        "$uSee Also:$u oaddmask");
+"OREGISTER" ("/msg $N@$s OREGISTER <account> <password> <user@host|nick>",
+        "Registers an account with $b$N$b using the specified account, password, and user@host. If then nick of an online user is specified, then that user's user@host is used.",
+ifdef({/services/nickserv/disable_nicks},
+{        "$uSee Also:$u ounregister"},
+{        "$uSee Also:$u oregnick, ounregister, ounregnick"}));
+"OSET" ("/msg $N OSET <nick|*account> [<setting> <value>]",
+        "Changes an account's settings for srvx. In addition to the normal $bset$b settings, you may set:",
+        "$bPASSWORD$b: Sets user's password.",
+        "$bFLAGS$b: Changes account flags for user.",
+        "$bLEVEL$b: Sets $O access level.",
+        "$bEPITHET$b: The description $C shows for the user's access.",
+        "You may use *Account instead of Nick as the name argument; the * makes $N use the name of an account directly (useful if the user is not online).",
+ifdef({/services/nickserv/disable_nicks},
+{        "$uSee Also:$u accountinfo, account flags, set, userinfo"},
+{        "$uSee Also:$u accountinfo, account flags, nickinfo, set, userinfo"}));
+"PASS" ("/msg $N@$s PASS <oldpass> <newpass>",
+        "Changes your $b$N$b password.",
+        "$uSee Also:$u auth");
+"REGISTER" (
+ifdef({/services/nickserv/email_enabled},
+{ifdef({/services/nickserv/email_required},
+{        "/msg $N@$s REGISTER <account> <password> <email>",},
+{        "/msg $N@$s REGISTER <account> <password> [email]",})},
+{        "/msg $N@$s REGISTER <account> <password>",})
+        "Registers a specified account with $b$N$b, adding your current user@host to your new account. You will be required to know the password you specify with $bregister$b in order to be able to use $bauth$b to authenticate to your account.",
+ifdef({/services/nickserv/email_enabled},
+{ifdef({/services/nickserv/email_required},
+{        "An email will be sent to the email address you give containing a cookie that will let you activate your account.  Once you have that cookie, you must use the $bcookie$b command to be able to use your account.",},
+{        "If you specify an email address, an email will be sent to it containing a cookie that will let you activate your account.  Once you have that cookie, you must use the $bcookie$b command to be able to use your account.",})})
+        "NOTE: It is strongly recommended that you use the long form ($N@$s) rather than just nick ($N) for this command, to protect against impersonators on other networks.",
+ifdef({/services/nickserv/disable_nicks},
+{        "$uSee Also:$u auth, unregister"},
+{        "$uSee Also:$u auth, regnick, unregister, unregnick"}));
+"SET" ("/msg $N SET [<setting> [value]]",
+        "Changes your account settings for srvx. Settings are:",
+        "$bANNOUNCEMENTS$b: Indicates whether you wish to receive community announcements via the $G service.",
+        "$bCOLOR$b: If set, $b$N$b and $b$C$b will use $bbold$b and $uunderlines$u in text they send you.",
+ifdef({/services/nickserv/email_enabled},
+{        "$bEMAIL$b: Sets (or changes) your email address.",})
+        "$bINFO$b:  Your infoline for $b$N$b (which can be viewed with the $baccountinfo$b command).",
+        "$bLANGUAGE$b: Your preferred language for private messages from the services.",
+        "$bPRIVMSG$b: If set, $b$N$b and $b$C$b will send text to you using PRIVMSGs rather than NOTICEs.",
+        "$bSTYLE$b: The style you want srvx to use for channel userlists it sends you. $bSTYLE$b can be either $bDef$b (default) or $bZoot$b.",
+        "$bTABLEWIDTH$b: Sets the width for wrapping table-formatted text. (Use 0 for the default.)",
+        "$bWIDTH$b: The width you want srvx to wrap text it sends you.  (Use 0 for the default.)",
+        "$bMAXLOGINS$b: The number of users that can log into your account at once.  (Use 0 for the default.)",
+        "$bset$b with no arguments returns your current settings.",
+ifdef({/services/nickserv/disable_nicks},
+{        "$uSee Also:$u accountinfo, userinfo"},
+{        "$uSee Also:$u accountinfo, nickinfo, userinfo"}));
+"STATUS" ("/msg $N STATUS",
+ifdef({/services/nickserv/disable_nicks},
+{        "Displays information about the status of $b$N$b, including the total number of accounts in its database."},
+{        "Displays information about the status of $b$N$b, including the total number of accounts and nicks that are registered in its database, and how many nicks are registered to your account (if you are authenticated to one)."}));
+"USERINFO" ("/msg $N USERINFO <nick>",
+        "Shows what account the nick specified is authenticated to.",
+        "$uSee Also:$u auth, accountinfo");
+"VERSION" ("/msg $N VERSION",
+        "Sends you the srvx version and some additional version information that is specific to $b$N$b.");
+"GHOST" ("/msg $N GHOST <nick>",
+        "This disconnects an old client that is authed to your account.  This is $bnot$b the same thing as nick ownership; the user $bmust$b be authenticated to the same account you are.",
+        "$uSee Also:$u auth");
+"RENAME" ("/msg $N RENAME <nick|*old-account> <new-account>",
+        "Renames an account.",
+        "This command is only accessible to helpers and IRC operators.",
+        "$uSee Also:$u merge");
+"VACATION" ("/msg $N VACATION",
+        "Marks your account as \"on vacation\" until the next time you authenticate to $N.",
+        "While you are \"on vacation\", your account will not be deleted for inactivity.");
+"SEARCH" ("/msg $N SEARCH <action> <criteria> <value> [<criteria> <value>]...",
+        "Searches for accounts matching the critera, and then does something to them.",
+        "$uSee Also:$u search action, search criteria");
+"SEARCH ACTION" ("$bSEARCH ACTION$b",
+        "The following actions are valid:",
+        "  PRINT      - Print matching accounts",
+        "  COUNT      - Count matching accounts",
+        "  UNREGISTER - Unregister matching accounts",
+        "$uSee Also:$u search, search criteria");
+"SEARCH CRITERIA" ("$bSEARCH CRITERIA$b",
+        "The following account search criteria are valid.  Each takes an additional argument, giving the actual criteria:",
+        "  LIMIT      - Limits the number of matches",
+        "  FLAGS      - Bits that must be turned on (e.g. +h) and/or off (e.g. -S) in an account",
+        "  REGISTERED - Registered time constraint (<Nu, <=Nu, =Nu, >=Nu or >Nu)",
+        "  SEEN       - Accounts not seen for at least this long",
+        "  ACCOUNTMASK - A glob that must match the account name",
+        "  EMAIL      - A glob that must match the account's email address",
+ifdef({/services/nickserv/disable_nicks},,
+{        "  NICKMASK   - A glob that must match a nick registered to the account",})
+        "  HOSTMASK SUPERSET - Account matches if someone with this hostmask can auth to the account",
+        "  HOSTMASK EXACT - Account matches if this exact hostmask is in list",
+        "  HOSTMASK SUBSET - Account matches if this mask \"covers\" one in their userlist",
+        "  HOSTMASK   - A glob that must match a hostmask for the account (equivalent to HOSTMASK SUPERSET)",
+        "  ACCESS     - An $O access constraint (<nnn, <=nnn, =nnn, >=nnn or >nnn)",
+        "$uSee Also:$u search, search action");
+"MERGE" ("/msg $N MERGE <from-nick|*from-account> <to-nick|*to-account>",
+        "Merge access from $bfrom-account$b into $bto-account$b.  This includes hostmasks, registered nicks, authed users, access in channels, and OpServ access (if any).  If $bto-account$b has equal  or greater access than $bfrom-account$b (or more a general hostmask, etc), $bto-account$b keeps that information.",
+        "This command is only accessible to helpers and IRC operators.",
+        "$uSee Also:$u rename");
+"MERGEDB" ("/msg $N MERGE <dbfilename>",
+        "Merge contents of $bdbfilename$b into in-memory database.  Any accounts in both will be $bOVERWRITTEN$b with the information from $bdbfilename$b, although authed users will be authed to the new account.",
+        "This command is only accessible to IRC operators.",
+        "$uSee Also:$u write");
diff --git a/src/opserv.c b/src/opserv.c
new file mode 100644 (file)
index 0000000..2062c5e
--- /dev/null
@@ -0,0 +1,4124 @@
+/* opserv.c - IRC Operator assistance service
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "conf.h"
+#include "gline.h"
+#include "global.h"
+#include "nickserv.h"
+#include "modcmd.h"
+#include "opserv.h"
+#include "timeq.h"
+#include "saxdb.h"
+
+#ifdef HAVE_SYS_TIMES_H
+#include <sys/times.h>
+#endif
+#ifdef HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#endif
+#ifdef HAVE_ARPA_INET_H
+#include <arpa/inet.h>
+#endif
+
+#define OPSERV_CONF_NAME "services/opserv"
+
+#define KEY_ALERT_CHANNEL "alert_channel"
+#define KEY_ALERT_CHANNEL_MODES "alert_channel_modes"
+#define KEY_DEBUG_CHANNEL "debug_channel"
+#define KEY_DEBUG_CHANNEL_MODES "debug_channel_modes"
+#define KEY_UNTRUSTED_MAX "untrusted_max"
+#define KEY_PURGE_LOCK_DELAY "purge_lock_delay"
+#define KEY_JOIN_FLOOD_MODERATE "join_flood_moderate"
+#define KEY_JOIN_FLOOD_MODERATE_THRESH "join_flood_moderate_threshold"
+#define KEY_NICK "nick"
+#define KEY_JOIN_POLICER "join_policer"
+#define KEY_NEW_USER_POLICER "new_user_policer"
+#define KEY_REASON "reason"
+#define KEY_RESERVES "reserves"
+#define KEY_IDENT "username" /* for compatibility with 1.0 DBs */
+#define KEY_HOSTNAME "hostname"
+#define KEY_DESC "description"
+#define KEY_BAD_WORDS "bad"
+#define KEY_EXEMPT_CHANNELS "exempt"
+#define KEY_SECRET_WORDS "secret"
+#define KEY_TRUSTED_HOSTS "trusted"
+#define KEY_OWNER "owner"
+#define KEY_GAGS "gags"
+#define KEY_ALERTS "alerts"
+#define KEY_REACTION "reaction"
+#define KEY_DISCRIM "discrim"
+#define KEY_WARN "chanwarn"
+#define KEY_MAX "max"
+#define KEY_TIME "time"
+#define KEY_MAX_CLIENTS "max_clients"
+#define KEY_LIMIT "limit"
+#define KEY_EXPIRES "expires"
+#define KEY_STAFF_AUTH_CHANNEL "staff_auth_channel"
+#define KEY_STAFF_AUTH_CHANNEL_MODES "staff_auth_channel_modes"
+#define KEY_CLONE_GLINE_DURATION "clone_gline_duration"
+#define KEY_BLOCK_GLINE_DURATION "block_gline_duration"
+#define KEY_ISSUER "issuer"
+#define KEY_ISSUED "issued"
+
+#define IDENT_FORMAT           "%s [%s@%s/%s]"
+#define IDENT_DATA(user)       user->nick, user->ident, user->hostname, inet_ntoa(user->ip)
+#define MAX_CHANNELS_WHOIS     50
+#define OSMSG_PART_REASON       "%s has no reason."
+#define OSMSG_KICK_REQUESTED    "Kick requested by %s."
+#define OSMSG_KILL_REQUESTED    "Kill requested by %s."
+#define OSMSG_GAG_REQUESTED     "Gag requested by %s."
+
+static const struct message_entry msgtab[] = {
+    { "OSMSG_USER_ACCESS_IS", "$b%s$b (account $b%s$b) has %d access." },
+    { "OSMSG_LEVEL_TOO_LOW", "You lack sufficient access to use this command." },
+    { "OSMSG_NEED_CHANNEL", "You must specify a channel for $b%s$b." },
+    { "OSMSG_INVALID_IRCMASK", "$b%s$b is an invalid IRC hostmask." },
+    { "OSMSG_ADDED_BAN", "I have banned $b%s$b from $b%s$b." },
+    { "OSMSG_GLINE_ISSUED", "G-line issued for $b%s$b." },
+    { "OSMSG_GLINE_REMOVED", "G-line removed for $b%s$b." },
+    { "OSMSG_GLINE_FORCE_REMOVED", "Unknown/expired G-line removed for $b%s$b." },
+    { "OSMSG_GLINES_ONE_REFRESHED", "All G-lines resent to $b%s$b." },
+    { "OSMSG_GLINES_REFRESHED", "All G-lines refreshed." },
+    { "OSMSG_CLEARBANS_DONE", "Cleared all bans from channel $b%s$b." },
+    { "OSMSG_CLEARMODES_DONE", "Cleared all modes from channel $b%s$b." },
+    { "OSMSG_NO_CHANNEL_MODES", "Channel $b%s$b had no modes to clear." },
+    { "OSMSG_DEOP_DONE", "Deopped the requested lusers." },
+    { "OSMSG_DEOPALL_DONE", "Deopped everyone on $b%s$b." },
+    { "OSMSG_NO_DEBUG_CHANNEL", "No debug channel has been configured." },
+    { "OSMSG_INVITE_DONE", "Invited $b%s$b to $b%s$b." },
+    { "OSMSG_ALREADY_THERE", "You are already in $b%s$b." },
+    { "OSMSG_JOIN_DONE", "I have joined $b%s$b." },
+    { "OSMSG_ALREADY_JOINED", "I am already in $b%s$b." },
+    { "OSMSG_NOT_ON_CHANNEL", "$b%s$b does not seem to be on $b%s$b." },
+    { "OSMSG_KICKALL_DONE", "I have cleared out %s." },
+    { "OSMSG_LEAVING", "Leaving $b%s$b." },
+    { "OSMSG_MODE_SET", "I have set the modes for $b%s$b." },
+    { "OSMSG_OP_DONE", "Opped the requested lusers." },
+    { "OSMSG_OPALL_DONE", "Opped everyone on $b%s$b." },
+    { "OSMSG_WHOIS_IDENT", "%s (%s@%s) from %d.%d.%d.%d" },
+    { "OSMSG_WHOIS_NICK", "Nick    : %s" },
+    { "OSMSG_WHOIS_HOST", "Host    : %s@%s" },
+    { "OSMSG_WHOIS_IP",   "Real IP : %s" },
+    { "OSMSG_WHOIS_MODES", "Modes   : +%s " },
+    { "OSMSG_WHOIS_INFO", "Info    : %s" },
+    { "OSMSG_WHOIS_NUMERIC", "Numnick : %s" },
+    { "OSMSG_WHOIS_SERVER", "Server  : %s" },
+    { "OSMSG_WHOIS_NICK_AGE", "Nick Age: %s" },
+    { "OSMSG_WHOIS_ACCOUNT", "Account : %s" },
+    { "OSMSG_WHOIS_CHANNELS", "Channels: %s" },
+    { "OSMSG_WHOIS_HIDECHANS", "Channel list omitted for your sanity." },
+    { "OSMSG_UNBAN_DONE", "Ban(s) removed from channel %s." },
+    { "OSMSG_CHANNEL_VOICED", "All users on %s voiced." },
+    { "OSMSG_CHANNEL_DEVOICED", "All voiced users on %s de-voiced." },
+    { "OSMSG_BAD_MODIFIER", "Unknown bad-word modifier $b%s$b." },
+    { "OSMSG_BAD_REDUNDANT", "$b%s$b is already covered by a bad word ($b%s$b)." },
+    { "OSMSG_BAD_GROWING", "Replacing bad word $b%s$b with shorter bad word $b%s$b." },
+    { "OSMSG_BAD_NUKING", " .. and removing redundant bad word $b%s$b." },
+    { "OSMSG_ADDED_BAD", "Added $b%s$b to the bad-word list." },
+    { "OSMSG_REMOVED_BAD", "Removed $b%s$b from the bad-word list." },
+    { "OSMSG_NOT_BAD_WORD", "$b%s$b is not a bad word." },
+    { "OSMSG_ADDED_EXEMPTION", "Added $b%s$b to the bad-word exemption list." },
+    { "OSMSG_ADDED_EXEMPTIONS", "Added %d exception(s) to the bad word list." },
+    { "OSMSG_REMOVED_EXEMPTION", "Removed $b%s$b from the exemption list." },
+    { "OSMSG_NOT_EXEMPT", "$b%s$b is not on the exempt list." },
+    { "OSMSG_ALREADY_TRUSTED", "Host $b%s$b is already trusted (use $bdeltrust$b and then $baddtrust$b to adjust)." },
+    { "OSMSG_NOT_TRUSTED", "Host $b%s$b is not trusted." },
+    { "OSMSG_BAD_IP", "$b%s$b is not a valid IP address" },
+    { "OSMSG_BAD_NUMBER", "$b%s$b is not a number" },
+    { "OSMSG_ADDED_TRUSTED", "Added trusted hosts to the trusted-hosts list." },
+    { "OSMSG_UPDATED_TRUSTED", "Updated trusted host $b%s$b." },
+    { "OSMSG_REMOVED_TRUSTED", "Removed trusted hosts from the trusted-hosts list." },
+    { "OSMSG_CLONE_EXISTS", "Nick $b%s$b is already in use." },
+    { "OSMSG_NOT_A_HOSTMASK", "The hostmask must be in user@host form." },
+    { "OSMSG_BADWORD_LIST", "Bad words: %s" },
+    { "OSMSG_EXEMPTED_LIST", "Exempted channels: %s" },
+    { "OSMSG_GLINE_COUNT", "There are %d glines active on the network." },
+    { "OSMSG_LINKS_SERVER", "%s%s (%u clients; %s)" },
+    { "OSMSG_MAX_CLIENTS", "Max clients: %d at %s" },
+    { "OSMSG_NETWORK_INFO", "Total users: %d (%d invisible, %d opers)" },
+    { "OSMSG_RESERVED_LIST", "List of reserved nicks:" },
+    { "OSMSG_TRUSTED_LIST", "List of trusted hosts:" },
+    { "OSMSG_HOST_IS_TRUSTED", "%s (%s; set %s ago by %s; expires %s: %s)" },
+    { "OSMSG_HOST_NOT_TRUSTED", "%s does not have a special trust." },
+    { "OSMSG_UPTIME_STATS", "Uptime: %s (%u lines processed, CPU time %.2fu/%.2fs)" },
+    { "OSMSG_LINE_DUMPED", "Raw line sent." },
+    { "OSMSG_RAW_PARSE_ERROR", "Error parsing raw line (not dumping to uplink)." },
+    { "OSMSG_COLLIDED_NICK", "Now temporarily holding nick $b%s$b." },
+    { "OSMSG_RESERVED_NICK", "Now reserving nick $b%s$b." },
+    { "OSMSG_NOT_RESERVED", "Nick $b%s$b is not reserved." },
+    { "OSMSG_ILLEGAL_REASON", "This channel is illegal." },
+    { "OSMSG_ILLEGAL_KILL_REASON", "Joined an illegal modeless channel - do not repeat." },
+    { "OSMSG_ILLEGAL_CHANNEL", "$b%s$b is an ILLEGAL channel. Do not re-join it." },
+    { "OSMSG_FLOOD_MODERATE", "This channel has been temporarily moderated due to a possible join flood attack detected in this channel; network staff have been notified and will investigate." },
+    { "OSMSG_CLONE_WARNING", "WARNING: You have connected the maximum permitted number of clients from one IP address (clones).  If you connect any more, your host will be temporarily banned from the network." },
+    { "OSMSG_CLONE_ADDED", "Added clone $b%s$b." },
+    { "OSMSG_CLONE_FAILED", "Unable to add user $b%s$b." },
+    { "OSMSG_NOT_A_CLONE", "Har har.  $b%s$b isn't a clone." },
+    { "OSMSG_CLONE_REMOVED", "Removed clone $b%s$b." },
+    { "OSMSG_CLONE_JOINED", "$b%s$b has joined $b%s$b." },
+    { "OSMSG_CLONE_PARTED", "$b%s$b has left $b%s$b." },
+    { "OSMSG_OPS_GIVEN", "I have given ops in $b%s$b to $b%s$b." },
+    { "OSMSG_CLONE_SAID", "$b%s$b has spoken to $b%s$b." },
+    { "OSMSG_UNKNOWN_SUBCOMMAND", "$b%s$b is not a valid subcommand of $b%s$b." },
+    { "OSMSG_UNKNOWN_OPTION", "$b%s$b has not been set." },
+    { "OSMSG_OPTION_IS", "$b%s$b is set to $b%s$b." },
+    { "OSMSG_OPTION_ROOT", "The following keys can be queried:" },
+    { "OSMSG_OPTION_LIST", "$b%s$b contains the following values:" },
+    { "OSMSG_OPTION_KEYS", "$b%s$b contains the following keys:" },
+    { "OSMSG_OPTION_LIST_EMPTY", "Empty list." },
+    { "OSMSG_SET_NOT_SET", "$b%s$b does not exist, and cannot be set." },
+    { "OSMSG_SET_BAD_TYPE", "$b%s$b is not a string, and cannot be set." },
+    { "OSMSG_SET_SUCCESS", "$b%s$b has been set to $b%s$b." },
+    { "OSMSG_SETTIME_SUCCESS", "Set time for servers named like $b%s$b." },
+    { "OSMSG_BAD_ACTION", "Unrecognized trace action $b%s$b." },
+    { "OSMSG_USER_SEARCH_RESULTS", "The following users were found:" },
+    { "OSMSG_CHANNEL_SEARCH_RESULTS", "The following channels were found:" },
+    { "OSMSG_GLINE_SEARCH_RESULTS", "The following glines were found:" },
+    { "OSMSG_LOG_SEARCH_RESULTS", "The following log entries were found:" },
+    { "OSMSG_GSYNC_RUNNING", "Synchronizing glines from %s." },
+    { "OSMSG_GTRACE_FORMAT", "%s (issued %s by %s, expires %s): %s" },
+    { "OSMSG_GAG_APPLIED", "Gagged $b%s$b, affecting %d users." },
+    { "OSMSG_GAG_ADDED", "Gagged $b%s$b." },
+    { "OSMSG_REDUNDANT_GAG", "Gag $b%s$b is redundant." },
+    { "OSMSG_GAG_NOT_FOUND", "Could not find gag $b%s$b." },
+    { "OSMSG_NO_GAGS", "No gags have been set." },
+    { "OSMSG_UNGAG_APPLIED", "Ungagged $b%s$b, affecting %d users." },
+    { "OSMSG_UNGAG_ADDED", "Ungagged $b%s$b." },
+    { "OSMSG_TIMEQ_INFO", "%u events in timeq; next in %lu seconds." },
+    { "OSMSG_ALERT_EXISTS", "An alert named $b%s$b already exists." },
+    { "OSMSG_UNKNOWN_REACTION", "Unknown alert reaction $b%s$b." },
+    { "OSMSG_ADDED_ALERT", "Added alert named $b%s$b." },
+    { "OSMSG_REMOVED_ALERT", "Removed alert named $b%s$b." },
+    { "OSMSG_NO_SUCH_ALERT", "No alert named $b%s$b could be found." },
+    { "OSMSG_ALERT_IS", "%s (by %s, reaction %s): %s" },
+    { "OSMSG_ALERTS_LIST", "Current $O alerts:" },
+    { "OSMSG_REHASH_COMPLETE", "Completed rehash of configuration database." },
+    { "OSMSG_REHASH_FAILED", "Rehash of configuration database failed, previous configuration is intact." },
+    { "OSMSG_REOPEN_COMPLETE", "Closed and reopened all log files." },
+    { "OSMSG_RECONNECTING", "Reconnecting to my uplink." },
+    { "OSMSG_NUMERIC_COLLIDE", "Numeric %d (%s) is already in use." },
+    { "OSMSG_NAME_COLLIDE", "That name is already in use." },
+    { "OSMSG_SRV_CREATE_FAILED", "Server creation failed -- check log files." },
+    { "OSMSG_SERVER_JUPED", "Added new jupe server %s." },
+    { "OSMSG_SERVER_NOT_JUPE", "That server is not a juped server." },
+    { "OSMSG_SERVER_UNJUPED", "Server jupe removed." },
+    { "OSMSG_WARN_ADDED", "Added channel activity warning for $b%s$b (%s)" },
+    { "OSMSG_WARN_EXISTS", "Channel activity warning for $b%s$b already exists." },
+    { "OSMSG_WARN_DELETED", "Removed channel activity warning for $b%s$b" },
+    { "OSMSG_WARN_NOEXIST", "Channel activity warning for $b%s$b does not exist." },
+    { "OSMSG_WARN_LISTSTART", "Channel activity warnings:" },
+    { "OSMSG_WARN_LISTENTRY", "%s (%s)" },
+    { "OSMSG_WARN_LISTEND", "End of activity warning list." },
+    { "OSMSG_UPLINK_CONNECTING", "Establishing connection with %s (%s:%d)." },
+    { "OSMSG_CURRENT_UPLINK", "$b%s$b is already the current uplink." },
+    { "OSMSG_INVALID_UPLINK", "$b%s$b is not a valid uplink name." },
+    { "OSMSG_UPLINK_DISABLED", "$b%s$b is a disabled or unavailable uplink." },
+    { "OSMSG_UPLINK_START", "Uplink $b%s$b:" },
+    { "OSMSG_UPLINK_ADDRESS", "Address: %s:%d" },
+    { "OSMSG_STUPID_GLINE", "Gline %s?  Now $bthat$b would be smooth." },
+    { "OSMSG_ACCOUNTMASK_AUTHED", "Invalid criteria: it is impossible to match an account mask but not be authed" },
+    { "OSMSG_CHANINFO_HEADER", "%s Information" },
+    { "OSMSG_CHANINFO_TIMESTAMP", "Created on: %a %b %d %H:%M:%S %Y (%s)" },
+    { "OSMSG_CHANINFO_MODES", "Modes: %s" },
+    { "OSMSG_CHANINFO_MODES_BADWORD", "Modes: %s; bad-word channel" },
+    { "OSMSG_CHANINFO_TOPIC", "Topic (set by %%s, %a %b %d %H:%M:%S %Y): %%s" },
+    { "OSMSG_CHANINFO_TOPIC_UNKNOWN", "Topic: (none / not gathered)" },
+    { "OSMSG_CHANINFO_BAN_COUNT", "Bans (%d):" },
+    { "OSMSG_CHANINFO_BAN", "%%s by %%s (%a %b %d %H:%M:%S %Y)" },
+    { "OSMSG_CHANINFO_MANY_USERS", "%d users (\"/msg $s %s %s users\" for the list)" },
+    { "OSMSG_CHANINFO_USER_COUNT", "Users (%d):" },
+    { "OSMSG_CSEARCH_CHANNEL_INFO", "%s [%d users] %s %s" },
+    { NULL, NULL }
+};
+
+#define OPSERV_SYNTAX() svccmd_send_help(user, opserv, cmd)
+
+typedef int (*discrim_search_func)(struct userNode *match, void *extra);
+
+struct userNode *opserv;
+
+static dict_t opserv_chan_warn; /* data is char* */
+static dict_t opserv_reserved_nick_dict; /* data is struct userNode* */
+static struct string_list *opserv_bad_words;
+static dict_t opserv_exempt_channels; /* data is not used */
+static dict_t opserv_trusted_hosts; /* data is struct trusted_host* */
+static dict_t opserv_hostinfo_dict; /* data is struct opserv_hostinfo* */
+static dict_t opserv_user_alerts; /* data is struct opserv_user_alert* */
+static dict_t opserv_nick_based_alerts; /* data is struct opserv_user_alert* */
+static dict_t opserv_channel_alerts; /* data is struct opserv_user_alert* */
+static struct module *opserv_module;
+static struct service *opserv_service;
+static struct log_type *OS_LOG;
+static unsigned int new_user_flood;
+static char *level_strings[1001];
+
+static struct {
+    struct chanNode *debug_channel;
+    struct chanNode *alert_channel;
+    struct chanNode *staff_auth_channel;
+    struct policer_params *join_policer_params;
+    struct policer new_user_policer;
+    unsigned long untrusted_max;
+    unsigned long clone_gline_duration;
+    unsigned long block_gline_duration;
+    unsigned long purge_lock_delay;
+    unsigned long join_flood_moderate;
+    unsigned long join_flood_moderate_threshold;
+} opserv_conf;
+
+struct trusted_host {
+    char *ipaddr;
+    char *issuer;
+    char *reason;
+    unsigned long limit;
+    time_t issued;
+    time_t expires;
+};
+
+struct gag_entry {
+    char *mask;
+    char *owner;
+    char *reason;
+    time_t expires;
+    struct gag_entry *next;
+};
+
+static struct gag_entry *gagList;
+
+struct opserv_hostinfo {
+    struct userList clients;
+    struct trusted_host *trusted;
+};
+
+static void
+opserv_free_hostinfo(void *data)
+{
+    struct opserv_hostinfo *ohi = data;
+    userList_clean(&ohi->clients);
+    free(ohi);
+}
+
+typedef struct opservDiscrim {
+    struct chanNode *channel;
+    char *mask_nick, *mask_ident, *mask_host, *mask_info, *server, *ip_mask_str, *reason, *accountmask;
+    unsigned long limit, ip_mask;
+    struct in_addr ip_addr;
+    unsigned int min_level, max_level, domain_depth, duration, min_clones, min_channels, max_channels;
+    unsigned int match_opers : 1, option_log : 1;
+    unsigned int chan_req_modes : 2, chan_no_modes : 2;
+    int authed : 2, info_space : 2;
+    time_t min_ts, max_ts;
+} *discrim_t;
+
+struct discrim_and_source {
+    discrim_t discrim;
+    struct userNode *source;
+    dict_t dict;
+    unsigned int disp_limit;
+};
+
+static discrim_t opserv_discrim_create(struct userNode *user, unsigned int argc, char *argv[], int allow_channel);
+static unsigned int opserv_discrim_search(discrim_t discrim, discrim_search_func dsf, void *data);
+static int gag_helper_func(struct userNode *match, void *extra);
+static int ungag_helper_func(struct userNode *match, void *extra);
+
+typedef enum {
+    REACT_NOTICE,
+    REACT_KILL,
+    REACT_GLINE
+} opserv_alert_reaction;
+
+struct opserv_user_alert {
+    char *owner;
+    char *text_discrim, *split_discrim;
+    discrim_t discrim;
+    opserv_alert_reaction reaction;
+};
+
+/* funny type to make it acceptible to dict_set_free_data, far below */
+static void
+opserv_free_user_alert(void *data)
+{
+    struct opserv_user_alert *alert = data;
+    if (alert->discrim->channel)
+        UnlockChannel(alert->discrim->channel);
+    free(alert->owner);
+    free(alert->text_discrim);
+    free(alert->split_discrim);
+    free(alert->discrim->reason);
+    free(alert->discrim);
+    free(alert);
+}
+
+static struct svccmd *
+opserv_get_command(const char *name) {
+    return dict_find(opserv_service->commands, name, NULL);
+}
+
+#define opserv_debug(format...) do { if (opserv_conf.debug_channel) send_channel_notice(opserv_conf.debug_channel , opserv , ## format); } while (0)
+#define opserv_alert(format...) do { if (opserv_conf.alert_channel) send_channel_notice(opserv_conf.alert_channel , opserv , ## format); } while (0)
+
+/* A lot of these commands are very similar to what ChanServ can do,
+ * but OpServ can do them even on channels that aren't registered.
+ */
+
+static MODCMD_FUNC(cmd_access)
+{
+    struct handle_info *hi;
+    const char *target;
+    unsigned int res;
+
+    target = (argc > 1) ? (const char*)argv[1] : user->nick;
+    if (!irccasecmp(target, "*")) {
+        nickserv_show_oper_accounts(user, cmd);
+        return 1;
+    }
+    if (!(hi = modcmd_get_handle_info(user, target)))
+        return 0;
+    res = (argc > 2) ? oper_try_set_access(user, cmd->parent->bot, hi, strtoul(argv[2], NULL, 0)) : 0;
+    reply("OSMSG_USER_ACCESS_IS", target, hi->handle, hi->opserv_level);
+    return res;
+}
+
+static MODCMD_FUNC(cmd_ban)
+{
+    struct mod_chanmode change;
+    struct userNode *victim;
+
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    change.args[0].mode = MODE_BAN;
+    if (is_ircmask(argv[1]))
+        change.args[0].hostmask = strdup(argv[1]);
+    else if ((victim = GetUserH(argv[1])))
+        change.args[0].hostmask = generate_hostmask(victim, 0);
+    else {
+       reply("OSMSG_INVALID_IRCMASK", argv[1]);
+       return 0;
+    }
+    modcmd_chanmode_announce(&change);
+    reply("OSMSG_ADDED_BAN", change.args[0].hostmask, channel->name);
+    free((char*)change.args[0].hostmask);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_chaninfo)
+{
+    char buffer[MAXLEN];
+    const char *fmt;
+    struct banNode *ban;
+    struct modeNode *moden;
+    unsigned int n;
+
+    reply("OSMSG_CHANINFO_HEADER", channel->name);
+    fmt = user_find_message(user, "OSMSG_CHANINFO_TIMESTAMP");
+    strftime(buffer, sizeof(buffer), fmt, gmtime(&channel->timestamp));
+    send_message_type(4, user, cmd->parent->bot, "%s", buffer);
+    irc_make_chanmode(channel, buffer);
+    if (channel->bad_channel)
+        reply("OSMSG_CHANINFO_MODES_BADWORD", buffer);
+    else
+        reply("OSMSG_CHANINFO_MODES", buffer);
+    if (channel->topic_time) {
+        fmt = user_find_message(user, "OSMSG_CHANINFO_TOPIC");
+        strftime(buffer, sizeof(buffer), fmt, gmtime(&channel->topic_time));
+        reply(buffer, channel->topic_nick, channel->topic);
+    } else {
+       irc_fetchtopic(cmd->parent->bot, channel->name);
+       reply("OSMSG_CHANINFO_TOPIC_UNKNOWN");
+    }
+    if (channel->banlist.used) {
+       reply("OSMSG_CHANINFO_BAN_COUNT", channel->banlist.used);
+        fmt = user_find_message(user, "OSMSG_CHANINFO_BAN");
+       for (n = 0; n < channel->banlist.used; n++) {
+           ban = channel->banlist.list[n];
+           strftime(buffer, sizeof(buffer), fmt, localtime(&ban->set));
+           reply(buffer, ban->ban, ban->who);
+       }
+    }
+    if ((argc < 2) && (channel->members.used >= 50)) {
+        /* early out unless they ask for users */
+        reply("OSMSG_CHANINFO_MANY_USERS", channel->members.used, argv[0], channel->name);
+        return 1;
+    }
+    reply("OSMSG_CHANINFO_USER_COUNT", channel->members.used);
+    for (n=0; n<channel->members.used; n++) {
+       moden = channel->members.list[n];
+       if (moden->modes & MODE_CHANOP)
+            send_message_type(4, user, cmd->parent->bot, " @%s (%s@%s)", moden->user->nick, moden->user->ident, moden->user->hostname);
+    }
+    for (n=0; n<channel->members.used; n++) {
+       moden = channel->members.list[n];
+       if ((moden->modes & (MODE_CHANOP|MODE_VOICE)) == MODE_VOICE)
+            send_message_type(4, user, cmd->parent->bot, " +%s (%s@%s)", moden->user->nick, moden->user->ident, moden->user->hostname);
+    }
+    for (n=0; n<channel->members.used; n++) {
+       moden = channel->members.list[n];
+       if ((moden->modes & (MODE_CHANOP|MODE_VOICE)) == 0)
+            send_message_type(4, user, cmd->parent->bot, "  %s (%s@%s)", moden->user->nick, moden->user->ident, moden->user->hostname);
+    }
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_warn) 
+{
+    char *reason, *message;
+
+    if (!IsChannelName(argv[1])) {
+       reply("OSMSG_NEED_CHANNEL", argv[0]);
+       return 0;
+    }
+    reason = dict_find(opserv_chan_warn, argv[1], NULL);
+    if (reason) {
+        reply("OSMSG_WARN_EXISTS", argv[1]);
+        return 0;
+    }
+    if (argv[2])
+        reason = strdup(unsplit_string(argv+2, argc-2, NULL));
+    else
+        reason = strdup("No reason");
+    dict_insert(opserv_chan_warn, strdup(argv[1]), reason);
+    reply("OSMSG_WARN_ADDED", argv[1], reason);
+    if (dict_find(channels, argv[1], NULL)) {
+        message = alloca(strlen(reason) + strlen(argv[1]) + 55);
+        sprintf(message, "Channel activity warning for channel %s: %s", argv[1], reason);
+        global_message(MESSAGE_RECIPIENT_OPERS, message);
+    }
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_unwarn)
+{
+    if ((argc < 2) || !IsChannelName(argv[1])) {
+        reply("OSMSG_NEED_CHANNEL", argv[0]);
+       return 0;
+    }
+    if (!dict_remove(opserv_chan_warn, argv[1])) {
+        reply("OSMSG_WARN_NOEXIST", argv[1]);
+        return 0;
+    }
+    reply("OSMSG_WARN_DELETED", argv[1]);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_clearbans)
+{
+    struct mod_chanmode *change;
+    unsigned int ii;
+
+    change = mod_chanmode_alloc(channel->banlist.used);
+    for (ii=0; ii<channel->banlist.used; ii++) {
+        change->args[ii].mode = MODE_REMOVE | MODE_BAN;
+        change->args[ii].hostmask = channel->banlist.list[ii]->ban;
+    }
+    modcmd_chanmode_announce(change);
+    mod_chanmode_free(change);
+    reply("OSMSG_CLEARBANS_DONE", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_clearmodes)
+{
+    struct mod_chanmode change;
+
+    if (!channel->modes) {
+       reply("OSMSG_NO_CHANNEL_MODES", channel->name);
+        return 0;
+    }
+    change.modes_set = 0;
+    change.modes_clear = channel->modes;
+    change.argc = 0;
+    modcmd_chanmode_announce(&change);
+    reply("OSMSG_CLEARMODES_DONE", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_deop)
+{
+    struct mod_chanmode *change;
+    unsigned int arg, count;
+
+    change = mod_chanmode_alloc(argc-1);
+    for (arg = 1, count = 0; arg < argc; ++arg) {
+        struct userNode *victim = GetUserH(argv[arg]);
+        struct modeNode *mn;
+       if (!victim || IsService(victim)
+            || !(mn = GetUserMode(channel, victim))
+            || !(mn->modes & MODE_CHANOP))
+            continue;
+        change->args[count].mode = MODE_REMOVE | MODE_CHANOP;
+        change->args[count++].member = mn;
+    }
+    if (count) {
+        change->argc = count;
+        modcmd_chanmode_announce(change);
+    }
+    mod_chanmode_free(change);
+    reply("OSMSG_DEOP_DONE");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_deopall)
+{
+    struct mod_chanmode *change;
+    unsigned int ii, count;
+
+    change = mod_chanmode_alloc(channel->members.used);
+    for (ii = count = 0; ii < channel->members.used; ++ii) {
+       struct modeNode *mn = channel->members.list[ii];
+       if (IsService(mn->user) || !(mn->modes & MODE_CHANOP))
+            continue;
+        change->args[count].mode = MODE_REMOVE | MODE_CHANOP;
+        change->args[count++].member = mn;
+    }
+    if (count) {
+        change->argc = count;
+        modcmd_chanmode_announce(change);
+    }
+    mod_chanmode_free(change);
+    reply("OSMSG_DEOPALL_DONE", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_rehash)
+{
+    extern char *services_config;
+
+    if (conf_read(services_config))
+       reply("OSMSG_REHASH_COMPLETE");
+    else
+       reply("OSMSG_REHASH_FAILED");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_reopen)
+{
+    log_reopen();
+    reply("OSMSG_REOPEN_COMPLETE");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_reconnect)
+{
+    reply("OSMSG_RECONNECTING");
+    irc_squit(self, "Reconnecting.", NULL);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_jupe)
+{
+    extern int force_n2k;
+    struct server *newsrv;
+    unsigned int num;
+    char numeric[COMBO_NUMERIC_LEN+1], srvdesc[SERVERDESCRIPTMAX+1];
+
+    num = atoi(argv[2]);
+    if ((num < 64) && !force_n2k) {
+        inttobase64(numeric, num, 1);
+        inttobase64(numeric+1, 64*64-1, 2);
+    } else {
+        inttobase64(numeric, num, 2);
+        inttobase64(numeric+2, 64*64*64-1, 3);
+    }
+#ifdef WITH_PROTOCOL_P10
+    if (GetServerN(numeric)) {
+        reply("OSMSG_NUMERIC_COLLIDE", num, numeric);
+        return 0;
+    }
+#endif
+    if (GetServerH(argv[1])) {
+        reply("OSMSG_NAME_COLLIDE");
+        return 0;
+    }
+    snprintf(srvdesc, sizeof(srvdesc), "JUPE %s", unsplit_string(argv+3, argc-3, NULL));
+    newsrv = AddServer(self, argv[1], 1, now, now, numeric, srvdesc);
+    if (!newsrv) {
+        reply("OSMSG_SRV_CREATE_FAILED");
+        return 0;
+    }
+    irc_server(newsrv);
+    reply("OSMSG_SERVER_JUPED", argv[1]);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_unjupe)
+{
+    struct server *srv;
+    char *reason;
+
+    srv = GetServerH(argv[1]);
+    if (!srv) {
+        reply("MSG_SERVER_UNKNOWN", argv[1]);
+        return 0;
+    }
+    if (strncmp(srv->description, "JUPE", 4)) {
+        reply("OSMSG_SERVER_NOT_JUPE");
+        return 0;
+    }
+    reason = (argc > 2) ? unsplit_string(argv+2, argc-2, NULL) : "Unjuping server";
+    DelServer(srv, 1, reason);
+    reply("OSMSG_SERVER_UNJUPED");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_jump)
+{
+    extern struct cManagerNode cManager;
+    void uplink_select(char *name);
+    struct uplinkNode *uplink_find(char *name);
+    struct uplinkNode *uplink;
+    char *target;
+
+    target = unsplit_string(argv+1, argc-1, NULL);
+
+    if (!strcmp(cManager.uplink->name, target)) {
+       reply("OSMSG_CURRENT_UPLINK", cManager.uplink->name);
+       return 0;
+    }
+
+    uplink = uplink_find(target);
+    if (!uplink) {
+       reply("OSMSG_INVALID_UPLINK", target);
+       return 0;
+    }
+    if (uplink->flags & UPLINK_UNAVAILABLE) {
+        reply("OSMSG_UPLINK_DISABLED", uplink->name);
+        return 0;
+    }
+
+    reply("OSMSG_UPLINK_CONNECTING", uplink->name, uplink->host, uplink->port);
+    uplink_select(target);
+    irc_squit(self, "Reconnecting.", NULL);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_die)
+{
+    char *reason, *text;
+
+    text = unsplit_string(argv+1, argc-1, NULL);
+    reason = alloca(strlen(text) + strlen(user->nick) + 20);
+    sprintf(reason, "Disconnected by %s [%s]", user->nick, text);
+    irc_squit(self, reason, text);
+    quit_services = 1;
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_restart)
+{
+    extern int services_argc;
+    extern char **services_argv;
+    char **restart_argv, *reason, *text;
+
+    text = unsplit_string(argv+1, argc-1, NULL);
+    reason = alloca(strlen(text) + strlen(user->nick) + 17);
+    sprintf(reason, "Restarted by %s [%s]", user->nick, text);
+    irc_squit(self, reason, text);
+
+    /* Append a NULL to the end of argv[]. */
+    restart_argv = (char **)alloca((services_argc + 1) * sizeof(char *));
+    memcpy(restart_argv, services_argv, services_argc * sizeof(char *));
+    restart_argv[services_argc] = NULL;
+
+    call_exit_funcs();
+
+    /* Don't blink. */
+    execv(services_argv[0], restart_argv);
+
+    /* If we're still here, that means something went wrong. Reconnect. */
+    return 1;
+}
+
+static struct gline *
+opserv_block(struct userNode *target, char *src_handle, char *reason, unsigned long duration)
+{
+    char *mask;
+    mask = alloca(MAXLEN);
+    snprintf(mask, MAXLEN, "*@%s", target->hostname);
+    if (!reason) {
+        reason = alloca(MAXLEN);
+        snprintf(reason, MAXLEN, "G-line requested by %s.", src_handle);
+    }
+    if (!duration) duration = opserv_conf.block_gline_duration;
+    return gline_add(src_handle, mask, duration, reason, now, 1);
+}
+
+static MODCMD_FUNC(cmd_block)
+{
+    struct userNode *target;
+    struct gline *gline;
+    char *reason;
+
+    target = GetUserH(argv[1]);
+    if (!target) {
+       reply("MSG_NICK_UNKNOWN", argv[1]);
+       return 0;
+    }
+    if (IsService(target)) {
+       reply("MSG_SERVICE_IMMUNE", target->nick);
+       return 0;
+    }
+    reason = (argc > 2) ? unsplit_string(argv+2, argc-2, NULL) : NULL;
+    gline = opserv_block(target, user->handle_info->handle, reason, 0);
+    reply("OSMSG_GLINE_ISSUED", gline->target);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_gline)
+{
+    unsigned long duration;
+    char *reason;
+    struct gline *gline;
+
+    reason = unsplit_string(argv+3, argc-3, NULL);
+    if (!is_gline(argv[1]) && !IsChannelName(argv[1]) && (argv[1][0] != '&')) {
+       reply("MSG_INVALID_GLINE", argv[1]);
+       return 0;
+    }
+    if (!argv[1][strspn(argv[1], "#&*?@.")] && (strlen(argv[1]) < 10)) {
+        reply("OSMSG_STUPID_GLINE", argv[1]);
+        return 0;
+    }
+    duration = ParseInterval(argv[2]);
+    if (!duration) {
+        reply("MSG_INVALID_DURATION", argv[2]);
+        return 0;
+    }
+    gline = gline_add(user->handle_info->handle, argv[1], duration, reason, now, 1);
+    reply("OSMSG_GLINE_ISSUED", gline->target);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_ungline)
+{
+    if (gline_remove(argv[1], 1))
+        reply("OSMSG_GLINE_REMOVED", argv[1]);
+    else
+        reply("OSMSG_GLINE_FORCE_REMOVED", argv[1]);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_refreshg)
+{
+    if (argc > 1) {
+        unsigned int count;
+        dict_iterator_t it;
+        struct server *srv;
+
+        for (it=dict_first(servers), count=0; it; it=iter_next(it)) {
+            srv = iter_data(it);
+            if ((srv == self) || !match_ircglob(srv->name, argv[1]))
+                continue;
+            gline_refresh_server(srv);
+            reply("OSMSG_GLINES_ONE_REFRESHED", srv->name);
+            count++;
+        }
+        if (!count) {
+            reply("MSG_SERVER_UNKNOWN", argv[1]);
+            return 0;
+        }
+    } else {
+        gline_refresh_all();
+        reply("OSMSG_GLINES_REFRESHED");
+    }
+    return 1;
+}
+
+static void
+opserv_ison(struct userNode *tell, struct userNode *target, const char *message)
+{
+    struct modeNode *mn;
+    unsigned int count, here_len, n, maxlen;
+    char buff[MAXLEN];
+
+    maxlen = tell->handle_info ? tell->handle_info->screen_width : 0;
+    if (!maxlen)
+        maxlen = MAX_LINE_SIZE;
+    for (n=count=0; n<target->channels.used; n++) {
+       mn = target->channels.list[n];
+       here_len = strlen(mn->channel->name);
+       if ((count + here_len + 4) > maxlen) {
+           buff[count] = 0;
+            send_message(tell, opserv, message, buff);
+           count = 0;
+       }
+       if (mn->modes & MODE_CHANOP)
+            buff[count++] = '@';
+       if (mn->modes & MODE_VOICE)
+            buff[count++] = '+';
+       memcpy(buff+count, mn->channel->name, here_len);
+       count += here_len;
+       buff[count++] = ' ';
+    }
+    if (count) {
+       buff[count] = 0;
+       send_message(tell, opserv, message, buff);
+    }
+}
+
+static MODCMD_FUNC(cmd_inviteme)
+{
+    struct userNode *target;
+
+    if (argc < 2) {
+       target = user;
+    } else {
+       target = GetUserH(argv[1]);
+       if (!target) {
+           reply("MSG_NICK_UNKNOWN", argv[1]);
+           return 0;
+       }
+    }
+    if (opserv_conf.debug_channel == NULL) {
+       reply("OSMSG_NO_DEBUG_CHANNEL");
+       return 0;
+    }
+    if (GetUserMode(opserv_conf.debug_channel, user)) {
+        reply("OSMSG_ALREADY_THERE", channel->name);
+        return 0;
+    }
+    irc_invite(cmd->parent->bot, target, opserv_conf.debug_channel);
+    if (target != user)
+       reply("OSMSG_INVITE_DONE", target->nick, opserv_conf.debug_channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_invite)
+{
+    if (GetUserMode(channel, user)) {
+        reply("OSMSG_ALREADY_THERE", channel->name);
+        return 0;
+    }
+    irc_invite(cmd->parent->bot, user, channel);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_join)
+{
+    struct userNode *bot = cmd->parent->bot;
+
+    if (!IsChannelName(argv[1])) {
+        reply("MSG_NOT_CHANNEL_NAME");
+        return 0;
+    } else if (!(channel = GetChannel(argv[1]))) {
+        channel = AddChannel(argv[1], now, NULL, NULL);
+        AddChannelUser(bot, channel)->modes |= MODE_CHANOP;
+    } else if (GetUserMode(channel, bot)) {
+        reply("OSMSG_ALREADY_JOINED", channel->name);
+        return 0;
+    } else {
+        struct mod_chanmode change;
+        change.modes_set = change.modes_clear = 0;
+        change.argc = 1;
+        change.args[0].mode = MODE_CHANOP;
+        change.args[0].member = AddChannelUser(bot, channel);
+        modcmd_chanmode_announce(&change);
+    }
+    irc_fetchtopic(bot, channel->name);
+    reply("OSMSG_JOIN_DONE", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_kick)
+{
+    struct userNode *target;
+    char *reason;
+
+    if (argc < 3) {
+       reason = alloca(strlen(OSMSG_KICK_REQUESTED)+strlen(user->nick)+1);
+       sprintf(reason, OSMSG_KICK_REQUESTED, user->nick);
+    } else {
+       reason = unsplit_string(argv+2, argc-2, NULL);
+    }
+    target = GetUserH(argv[1]);
+    if (!target) {
+       reply("MSG_NICK_UNKNOWN", argv[1]);
+       return 0;
+    }
+    if (!GetUserMode(channel, target)) {
+       reply("OSMSG_NOT_ON_CHANNEL", target->nick, channel->name);
+       return 0;
+    }
+    KickChannelUser(target, channel, cmd->parent->bot, reason);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_kickall)
+{
+    unsigned int limit, n, inchan;
+    struct modeNode *mn;
+    char *reason;
+    struct userNode *bot = cmd->parent->bot;
+
+    /* ircu doesn't let servers KICK users, so if OpServ's not in the
+     * channel, we have to join it in temporarily. */
+    if (!(inchan = GetUserMode(channel, bot) ? 1 : 0)) {
+        struct mod_chanmode change;
+        memset(&change, 0, sizeof(change));
+        change.args[0].mode = MODE_CHANOP;
+        change.args[0].member = AddChannelUser(bot, channel);
+        modcmd_chanmode_announce(&change);
+    }
+    if (argc < 2) {
+       reason = alloca(strlen(OSMSG_KICK_REQUESTED)+strlen(user->nick)+1);
+       sprintf(reason, OSMSG_KICK_REQUESTED, user->nick);
+    } else {
+       reason = unsplit_string(argv+1, argc-1, NULL);
+    }
+    limit = user->handle_info->opserv_level;
+    for (n=channel->members.used; n>0;) {
+       mn = channel->members.list[--n];
+       if (IsService(mn->user)
+           || (mn->user->handle_info
+               && (mn->user->handle_info->opserv_level >= limit))) {
+           continue;
+       }
+       KickChannelUser(mn->user, channel, bot, reason);
+    }
+    if (!inchan)
+        DelChannelUser(bot, channel, "My work here is done", 0);
+    reply("OSMSG_KICKALL_DONE", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_kickban)
+{
+    struct mod_chanmode change;
+    struct userNode *target;
+    char *reason;
+    char *mask;
+
+    if (argc == 2) {
+       reason = alloca(strlen(OSMSG_KICK_REQUESTED)+strlen(user->nick)+1);
+       sprintf(reason, OSMSG_KICK_REQUESTED, user->nick);
+    } else {
+       reason = unsplit_string(argv+2, argc-2, NULL);
+    }
+    target = GetUserH(argv[1]);
+    if (!target) {
+       reply("MSG_NICK_UNKNOWN", argv[1]);
+       return 0;
+    }
+    if (!GetUserMode(channel, target)) {
+       reply("OSMSG_NOT_ON_CHANNEL", target->nick, channel->name);
+       return 0;
+    }
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    change.args[0].mode = MODE_BAN;
+    change.args[0].hostmask = mask = generate_hostmask(target, 0);
+    modcmd_chanmode_announce(&change);
+    KickChannelUser(target, channel, cmd->parent->bot, reason);
+    free(mask);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_kickbanall)
+{
+    struct modeNode *mn;
+    struct userNode *bot = cmd->parent->bot;
+    struct mod_chanmode *change;
+    char *reason;
+    unsigned int limit, n, inchan;
+
+    /* ircu doesn't let servers KICK users, so if OpServ's not in the
+     * channel, we have to join it in temporarily. */
+    if (!(inchan = GetUserMode(channel, bot) ? 1 : 0)) {
+        change = mod_chanmode_alloc(2);
+        change->args[0].mode = MODE_CHANOP;
+        change->args[0].member = AddChannelUser(bot, channel);
+        change->args[1].mode = MODE_BAN;
+        change->args[1].hostmask = "*!*@*";
+    } else {
+        change = mod_chanmode_alloc(2);
+        change->args[0].mode = MODE_BAN;
+        change->args[0].hostmask = "*!*@*";
+    }
+    modcmd_chanmode_announce(change);
+    if (argc < 2) {
+       reason = alloca(strlen(OSMSG_KICK_REQUESTED)+strlen(user->nick)+1);
+       sprintf(reason, OSMSG_KICK_REQUESTED, user->nick);
+    } else {
+       reason = unsplit_string(argv+1, argc-1, NULL);
+    }
+    /* now kick them */
+    limit = user->handle_info->opserv_level;
+    for (n=channel->members.used; n>0; ) {
+       mn = channel->members.list[--n];
+       if (IsService(mn->user)
+           || (mn->user->handle_info
+               && (mn->user->handle_info->opserv_level >= limit))) {
+           continue;
+       }
+       KickChannelUser(mn->user, channel, bot, reason);
+    }
+    if (!inchan)
+        DelChannelUser(bot, channel, "My work here is done", 0);
+    reply("OSMSG_KICKALL_DONE", channel->name);
+    return 1;    
+}
+
+static MODCMD_FUNC(cmd_part)
+{
+    char *reason;
+
+    if (!IsChannelName(argv[1])) {
+        reply("MSG_NOT_CHANNEL_NAME");
+        return 0;
+    }
+    if ((channel = GetChannel(argv[1]))) {
+        if (!GetUserMode(channel, cmd->parent->bot)) {
+            reply("OSMSG_NOT_ON_CHANNEL", cmd->parent->bot->nick, channel->name);
+            return 0;
+        }
+        reason = (argc < 3) ? "Leaving." : unsplit_string(argv+2, argc-2, NULL);
+        reply("OSMSG_LEAVING", channel->name);
+        DelChannelUser(cmd->parent->bot, channel, reason, 0);
+    }
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_mode)
+{
+    if (!modcmd_chanmode(argv+1, argc-1, MCP_ALLOW_OVB|MCP_KEY_FREE|MC_ANNOUNCE)) {
+        reply("MSG_INVALID_MODES", unsplit_string(argv+1, argc-1, NULL));
+        return 0;
+    }
+    reply("OSMSG_MODE_SET", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_op)
+{
+    struct mod_chanmode *change;
+    unsigned int arg, count;
+
+    change = mod_chanmode_alloc(argc-1);
+    for (arg = 1, count = 0; arg < argc; ++arg) {
+        struct userNode *victim;
+        struct modeNode *mn;
+        if (!(victim = GetUserH(argv[arg])))
+            continue;
+        if (!(mn =  GetUserMode(channel, victim)))
+            continue;
+        if (!(mn->modes & MODE_CHANOP))
+            continue;
+        change->args[count].mode = MODE_CHANOP;
+        change->args[count++].member = mn;
+    }
+    if (count) {
+        change->argc = count;
+        modcmd_chanmode_announce(change);
+    }
+    mod_chanmode_free(change);
+    reply("OSMSG_OP_DONE");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_opall)
+{
+    struct mod_chanmode *change;
+    unsigned int ii, count;
+
+    change = mod_chanmode_alloc(channel->members.used);
+    for (ii = count = 0; ii < channel->members.used; ++ii) {
+       struct modeNode *mn = channel->members.list[ii];
+       if (mn->modes & MODE_CHANOP)
+            continue;
+        change->args[count].mode = MODE_CHANOP;
+        change->args[count++].member = mn;
+    }
+    if (count) {
+        change->argc = count;
+       modcmd_chanmode_announce(change);
+    }
+    mod_chanmode_free(change);
+    reply("OSMSG_OPALL_DONE", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_whois)
+{
+    struct userNode *target;
+    char buffer[128];
+    int bpos, herelen;
+
+#ifdef WITH_PROTOCOL_P10
+    if (argv[1][0] == '*')
+        target = GetUserN(argv[1]+1);
+    else
+        target = GetUserH(argv[1]);
+#else
+    target = GetUserH(argv[1]);
+#endif
+    if (!target) {
+        reply("MSG_NICK_UNKNOWN", argv[1]);
+        return 0;
+    }
+    reply("OSMSG_WHOIS_NICK", target->nick);
+    reply("OSMSG_WHOIS_HOST", target->ident, target->hostname);
+    reply("OSMSG_WHOIS_IP", inet_ntoa(target->ip));
+    if (target->modes) {
+       bpos = 0;
+#define buffer_cat(str) (herelen = strlen(str), memcpy(buffer+bpos, str, herelen), bpos += herelen)
+       if (IsInvisible(target)) buffer[bpos++] = 'i';
+       if (IsWallOp(target)) buffer[bpos++] = 'w';
+       if (IsOper(target)) buffer[bpos++] = 'o';
+       if (IsGlobal(target)) buffer[bpos++] = 'g';
+       if (IsServNotice(target)) buffer[bpos++] = 's';
+       if (IsHelperIrcu(target)) buffer[bpos++] = 'h';
+       if (IsService(target)) buffer[bpos++] = 'k';
+       if (IsDeaf(target)) buffer[bpos++] = 'd';
+        if (IsHiddenHost(target)) buffer[bpos++] = 'x';
+        if (IsGagged(target)) buffer_cat(" (gagged)");
+       buffer[bpos] = 0;
+       if (bpos > 0)
+            reply("OSMSG_WHOIS_MODES", buffer);
+    }
+    reply("OSMSG_WHOIS_INFO", target->info);
+#ifdef WITH_PROTOCOL_P10
+    reply("OSMSG_WHOIS_NUMERIC", target->numeric);
+#endif
+    reply("OSMSG_WHOIS_SERVER", target->uplink->name);
+    reply("OSMSG_WHOIS_ACCOUNT", (target->handle_info ? target->handle_info->handle : "Not authenticated"));
+    intervalString(buffer, now - target->timestamp);
+    reply("OSMSG_WHOIS_NICK_AGE", buffer);
+    if (target->channels.used <= MAX_CHANNELS_WHOIS)
+       opserv_ison(user, target, "OSMSG_WHOIS_CHANNELS");
+    else
+       reply("OSMSG_WHOIS_HIDECHANS");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_unban)
+{
+    struct mod_chanmode change;
+    change.modes_set = change.modes_clear = 0;
+    change.argc = 1;
+    change.args[0].mode = MODE_REMOVE | MODE_BAN;
+    change.args[0].hostmask = argv[1];
+    modcmd_chanmode_announce(&change);
+    reply("OSMSG_UNBAN_DONE", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_voiceall)
+{
+    struct mod_chanmode *change;
+    unsigned int ii, count;
+
+    change = mod_chanmode_alloc(channel->members.used);
+    for (ii = count = 0; ii < channel->members.used; ++ii) {
+       struct modeNode *mn = channel->members.list[ii];
+       if (mn->modes & (MODE_CHANOP|MODE_VOICE))
+            continue;
+        change->args[count].mode = MODE_VOICE;
+        change->args[count++].member = mn;
+    }
+    if (count) {
+        change->argc = count;
+       modcmd_chanmode_announce(change);
+    }
+    mod_chanmode_free(change);
+    reply("OSMSG_CHANNEL_VOICED", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_devoiceall)
+{
+    struct mod_chanmode *change;
+    unsigned int ii, count;
+
+    change = mod_chanmode_alloc(channel->members.used);
+    for (ii = count = 0; ii < channel->members.used; ++ii) {
+       struct modeNode *mn = channel->members.list[ii];
+       if (mn->modes & MODE_VOICE)
+            continue;
+        change->args[count].mode = MODE_REMOVE | MODE_VOICE;
+        change->args[count++].member = mn;
+    }
+    if (count) {
+        change->argc = count;
+       modcmd_chanmode_announce(change);
+    }
+    mod_chanmode_free(change);
+    reply("OSMSG_CHANNEL_DEVOICED", channel->name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_bad) {
+    dict_iterator_t it;
+    unsigned int ii, end, here_len;
+    char buffer[400];
+
+    /* Show the bad word list.. */
+    for (ii=end=0; ii<opserv_bad_words->used; ii++) {
+        here_len = strlen(opserv_bad_words->list[ii]);
+        if ((end + here_len + 2) > sizeof(buffer)) {
+            buffer[end] = 0;
+            reply("OSMSG_BADWORD_LIST", buffer);
+            end = 0;
+        }
+        memcpy(buffer+end, opserv_bad_words->list[ii], here_len);
+        end += here_len;
+        buffer[end++] = ' ';
+    }
+    buffer[end] = 0;
+    reply("OSMSG_BADWORD_LIST", buffer);
+
+    /* Show the exemption list.. */
+    for (it=dict_first(opserv_exempt_channels), end=0; it; it=iter_next(it)) {
+        here_len = strlen(iter_key(it));
+        if ((end + here_len + 2) > sizeof(buffer)) {
+            buffer[end] = 0;
+            reply("OSMSG_EXEMPTED_LIST", buffer);
+            end = 0;
+        }
+        memcpy(buffer+end, iter_key(it), here_len);
+        end += here_len;
+        buffer[end++] = ' ';
+    }
+    buffer[end] = 0;
+    reply("OSMSG_EXEMPTED_LIST", buffer);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_glines) {
+    reply("OSMSG_GLINE_COUNT", gline_count());
+    return 1;
+}
+
+static void
+trace_links(struct userNode *bot, struct userNode *user, struct server *server, unsigned int depth) {
+    unsigned int nn, pos;
+    char buffer[400];
+
+    for (nn=1; nn<=depth; nn<<=1) ;
+    for (pos=0, nn>>=1; nn>1; ) {
+        nn >>= 1;
+        buffer[pos++] = (depth & nn) ? ((nn == 1) ? '`' : ' ') : '|';
+        buffer[pos++] = (nn == 1) ? '-': ' ';
+    }
+    buffer[pos] = 0;
+    send_message(user, bot, "OSMSG_LINKS_SERVER", buffer, server->name, server->clients, server->description);
+    if (!server->children.used)
+        return;
+    for (nn=0; nn<server->children.used-1; nn++) {
+        trace_links(bot, user, server->children.list[nn], depth<<1);
+    }
+    trace_links(bot, user, server->children.list[nn], (depth<<1)|1);
+}
+
+static MODCMD_FUNC(cmd_stats_links) {
+    trace_links(cmd->parent->bot, user, self, 1);
+    return 1;
+}
+
+
+static MODCMD_FUNC(cmd_stats_max) {
+    reply("OSMSG_MAX_CLIENTS", max_clients, asctime(localtime(&max_clients_time)));
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_network) {
+    struct helpfile_table tbl;
+    unsigned int nn, tot_clients;
+    dict_iterator_t it;
+
+    tot_clients = dict_size(clients);
+    reply("OSMSG_NETWORK_INFO", tot_clients, invis_clients, curr_opers.used);
+    tbl.length = dict_size(servers)+1;
+    tbl.width = 3;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = calloc(tbl.length, sizeof(*tbl.contents));
+    tbl.contents[0] = calloc(tbl.width, sizeof(**tbl.contents));
+    tbl.contents[0][0] = "Server Name";
+    tbl.contents[0][1] = "Clients";
+    tbl.contents[0][2] = "Load";
+    for (it=dict_first(servers), nn=1; it; it=iter_next(it)) {
+        struct server *server = iter_data(it);
+        char *buffer = malloc(32);
+        tbl.contents[nn] = calloc(tbl.width, sizeof(**tbl.contents));
+        tbl.contents[nn][0] = server->name;
+        tbl.contents[nn][1] = buffer;
+        sprintf(buffer, "%u", server->clients);
+        tbl.contents[nn][2] = buffer + 16;
+        sprintf(buffer+16, "%3.3g%%", ((double)server->clients/tot_clients)*100);
+        nn++;
+    }
+    table_send(cmd->parent->bot, user->nick, 0, 0, tbl);
+    for (nn=1; nn<tbl.length; nn++) {
+        free((char*)tbl.contents[nn][1]);
+        free(tbl.contents[nn]);
+    }
+    free(tbl.contents[0]);
+    free(tbl.contents);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_network2) {
+    struct helpfile_table tbl;
+    unsigned int nn;
+    dict_iterator_t it;
+
+    tbl.length = dict_size(servers)+1;
+    tbl.width = 3;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = calloc(tbl.length, sizeof(*tbl.contents));
+    tbl.contents[0] = calloc(tbl.width, sizeof(**tbl.contents));
+    tbl.contents[0][0] = "Server Name";
+    tbl.contents[0][1] = "Numeric";
+    tbl.contents[0][2] = "Link Time";
+    for (it=dict_first(servers), nn=1; it; it=iter_next(it)) {
+        struct server *server = iter_data(it);
+        char *buffer = malloc(64);
+        int ofs;
+
+        tbl.contents[nn] = calloc(tbl.width, sizeof(**tbl.contents));
+        tbl.contents[nn][0] = server->name;
+#ifdef WITH_PROTOCOL_P10
+        sprintf(buffer, "%s (%ld)", server->numeric, base64toint(server->numeric, strlen(server->numeric)));
+#else
+        buffer[0] = 0;
+#endif
+        tbl.contents[nn][1] = buffer;
+        ofs = strlen(buffer) + 1;
+        intervalString(buffer + ofs, now - server->link);
+        if (server->self_burst) strcat(buffer + ofs, " Bursting");
+        tbl.contents[nn][2] = buffer + ofs;
+        nn++;
+    }
+    table_send(cmd->parent->bot, user->nick, 0, 0, tbl);
+    for (nn=1; nn<tbl.length; nn++) {
+        free((char*)tbl.contents[nn][1]);
+        free(tbl.contents[nn]);
+    }
+    free(tbl.contents[0]);
+    free(tbl.contents);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_reserved) {
+    dict_iterator_t it;
+
+    reply("OSMSG_RESERVED_LIST");
+    for (it = dict_first(opserv_reserved_nick_dict); it; it = iter_next(it))
+        send_message_type(4, user, cmd->parent->bot, "%s", iter_key(it));
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_trusted) {
+    dict_iterator_t it;
+    struct trusted_host *th;
+    char length[INTERVALLEN], issued[INTERVALLEN], limit[32];
+
+    if (argc > 1) {
+        th = dict_find(opserv_trusted_hosts, argv[1], NULL);
+        if (th) {
+            if (th->issued)
+                intervalString(issued, now - th->issued);
+            if (th->expires)
+                intervalString(length, th->expires - now);
+            if (th->limit)
+                sprintf(limit, "limit %lu", th->limit);
+            reply("OSMSG_HOST_IS_TRUSTED",
+                  th->ipaddr,
+                  (th->limit ? limit : "no limit"),
+                  (th->issued ? issued : "some time"),
+                  (th->issuer ? th->issuer : "<unknown>"),
+                  (th->expires ? length : "never"),
+                  (th->reason ? th->reason : "<unknown>"));
+        } else {
+            reply("OSMSG_HOST_NOT_TRUSTED", argv[1]);
+        }
+    } else {
+        reply("OSMSG_TRUSTED_LIST");
+        for (it = dict_first(opserv_trusted_hosts); it; it = iter_next(it)) {
+            th = iter_data(it);
+            if (th->issued)
+                intervalString(issued, now - th->issued);
+            if (th->expires)
+                intervalString(length, th->expires - now);
+            if (th->limit)
+                sprintf(limit, "limit %lu", th->limit);
+            reply("OSMSG_HOST_IS_TRUSTED", iter_key(it),
+                  (th->limit ? limit : "no limit"),
+                  (th->issued ? issued : "some time"),
+                  (th->issuer ? th->issuer : "<unknown>"),
+                  (th->expires ? length : "never"),
+                  (th->reason ? th->reason : "<unknown>"));
+        }
+    }
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_uplink) {
+    extern struct cManagerNode cManager;
+    struct uplinkNode *uplink;
+
+    uplink = cManager.uplink;
+    reply("OSMSG_UPLINK_START", uplink->name);
+    reply("OSMSG_UPLINK_ADDRESS", uplink->host, uplink->port);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_uptime) {
+    char uptime[INTERVALLEN];
+    struct tms buf;
+    extern time_t boot_time;
+    extern int lines_processed;
+    static long clocks_per_sec;
+
+    if (!clocks_per_sec) {
+#if defined(HAVE_SYSCONF) && defined(_SC_CLK_TCK)
+        clocks_per_sec = sysconf(_SC_CLK_TCK);
+        if (clocks_per_sec <= 0)
+#endif
+        {
+            log_module(OS_LOG, LOG_ERROR, "Unable to query sysconf(_SC_CLK_TCK), output of 'stats uptime' will be wrong");
+            clocks_per_sec = CLOCKS_PER_SEC;
+        }
+    }
+    intervalString(uptime, time(NULL)-boot_time);
+    times(&buf);
+    reply("OSMSG_UPTIME_STATS",
+          uptime, lines_processed,
+          buf.tms_utime/(double)clocks_per_sec,
+          buf.tms_stime/(double)clocks_per_sec);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_alerts) {
+    dict_iterator_t it;
+    struct opserv_user_alert *alert;
+    const char *reaction;
+
+    reply("OSMSG_ALERTS_LIST");
+    for (it = dict_first(opserv_user_alerts); it; it = iter_next(it)) {
+        alert = iter_data(it);
+        switch (alert->reaction) {
+        case REACT_NOTICE: reaction = "notice"; break;
+        case REACT_KILL: reaction = "kill"; break;
+        case REACT_GLINE: reaction = "gline"; break;
+        default: reaction = "<unknown>"; break;
+        }
+        reply("OSMSG_ALERT_IS", iter_key(it), alert->owner, reaction, alert->text_discrim);
+    }
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_gags) {
+    struct gag_entry *gag;
+    struct helpfile_table table;
+    unsigned int nn;
+
+    if (!gagList) {
+       reply("OSMSG_NO_GAGS");
+        return 1;
+    }
+    for (nn=0, gag=gagList; gag; nn++, gag=gag->next) ;
+    table.length = nn+1;
+    table.width = 4;
+    table.flags = TABLE_NO_FREE;
+    table.contents = calloc(table.length, sizeof(char**));
+    table.contents[0] = calloc(table.width, sizeof(char*));
+    table.contents[0][0] = "Mask";
+    table.contents[0][1] = "Owner";
+    table.contents[0][2] = "Expires";
+    table.contents[0][3] = "Reason";
+    for (nn=1, gag=gagList; gag; nn++, gag=gag->next) {
+        char expstr[INTERVALLEN];
+        if (gag->expires) intervalString(expstr, gag->expires - now);
+        else strcpy(expstr, "Never");
+        table.contents[nn] = calloc(table.width, sizeof(char*));
+        table.contents[nn][0] = gag->mask;
+        table.contents[nn][1] = gag->owner;
+        table.contents[nn][2] = strdup(expstr);
+        table.contents[nn][3] = gag->reason;
+    }
+    table_send(cmd->parent->bot, user->nick, 0, NULL, table);
+    for (nn=1; nn<table.length; nn++) {
+        free((char*)table.contents[nn][2]);
+        free(table.contents[nn]);
+    }
+    free(table.contents[0]);
+    free(table.contents);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_timeq) {
+    reply("OSMSG_TIMEQ_INFO", timeq_size(), timeq_next()-now);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_warn) {
+    dict_iterator_t it;
+
+    reply("OSMSG_WARN_LISTSTART");
+    for (it=dict_first(opserv_chan_warn); it; it=iter_next(it))
+        reply("OSMSG_WARN_LISTENTRY", iter_key(it), (char*)iter_data(it));
+    reply("OSMSG_WARN_LISTEND");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_dump)
+{
+    char linedup[MAXLEN], *original;
+
+    original = unsplit_string(argv+1, argc-1, NULL);
+    safestrncpy(linedup, original, sizeof(linedup));
+    /* assume it's only valid IRC if we can parse it */
+    if (parse_line(linedup, 1)) {
+       irc_raw(original);
+       reply("OSMSG_LINE_DUMPED");
+    } else
+       reply("OSMSG_RAW_PARSE_ERROR");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_raw)
+{
+    char linedup[MAXLEN], *original;
+
+    original = unsplit_string(argv+1, argc-1, NULL);
+    safestrncpy(linedup, original, sizeof(linedup));
+    /* Try to parse the line before sending it; if it's too wrong,
+     * maybe it will core us instead of our uplink. */
+    parse_line(linedup, 1);
+    irc_raw(original);
+    reply("OSMSG_LINE_DUMPED");
+    return 1;
+}
+
+static struct userNode *
+opserv_add_reserve(struct svccmd *cmd, struct userNode *user, const char *nick, const char *ident, const char *host, const char *desc)
+{
+    struct userNode *resv = GetUserH(nick);
+    if (resv) {
+       if (IsService(resv)) {
+           reply("MSG_SERVICE_IMMUNE", resv->nick);
+           return NULL;
+       }
+       if (resv->handle_info
+           && resv->handle_info->opserv_level > user->handle_info->opserv_level) {
+           reply("OSMSG_LEVEL_TOO_LOW");
+           return NULL;
+       }
+    }
+    if ((resv = AddClone(nick, ident, host, desc))) {
+        dict_insert(opserv_reserved_nick_dict, resv->nick, resv);
+    }
+    return resv;
+}
+
+static MODCMD_FUNC(cmd_collide)
+{
+    struct userNode *resv;
+
+    resv = opserv_add_reserve(cmd, user, argv[1], argv[2], argv[3], unsplit_string(argv+4, argc-4, NULL));
+    if (resv) {
+       reply("OSMSG_COLLIDED_NICK", resv->nick);
+       return 1;
+    } else {
+        reply("OSMSG_CLONE_FAILED", argv[1]);
+       return 0;
+    }
+}
+
+static MODCMD_FUNC(cmd_reserve)
+{
+    struct userNode *resv;
+
+    resv = opserv_add_reserve(cmd, user, argv[1], argv[2], argv[3], unsplit_string(argv+4, argc-4, NULL));
+    if (resv) {
+       resv->modes |= FLAGS_PERSISTENT;
+       reply("OSMSG_RESERVED_NICK", resv->nick);
+       return 1;
+    } else {
+        reply("OSMSG_CLONE_FAILED", argv[1]);
+       return 0;
+    }
+}
+
+static int
+free_reserve(char *nick)
+{
+    struct userNode *resv;
+    unsigned int rlen;
+    char *reason;
+
+    resv = dict_find(opserv_reserved_nick_dict, nick, NULL);
+    if (!resv)
+        return 0;
+
+    rlen = strlen(resv->nick)+strlen(OSMSG_PART_REASON);
+    reason = alloca(rlen);
+    snprintf(reason, rlen, OSMSG_PART_REASON, resv->nick);
+    DelUser(resv, NULL, 1, reason);
+    dict_remove(opserv_reserved_nick_dict, nick);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_unreserve)
+{
+    if (free_reserve(argv[1]))
+       reply("OSMSG_NICK_UNRESERVED", argv[1]);
+    else
+       reply("OSMSG_NOT_RESERVED", argv[1]);
+    return 1;
+}
+
+static void
+opserv_part_channel(void *data)
+{
+    DelChannelUser(opserv, data, "Leaving.", 0);
+}
+
+static int alert_check_user(const char *key, void *data, void *extra);
+
+static int
+opserv_new_user_check(struct userNode *user)
+{
+    struct opserv_hostinfo *ohi;
+    struct gag_entry *gag;
+
+    /* Check to see if we should ignore them entirely. */
+    if (IsLocal(user) || IsService(user))
+        return 0;
+
+    /* Check for alerts, and stop if we find one that kills them. */
+    if (dict_foreach(opserv_user_alerts, alert_check_user, user))
+        return 1;
+
+    /* Gag them if appropriate. */
+    for (gag = gagList; gag; gag = gag->next) {
+        if (user_matches_glob(user, gag->mask, 1)) {
+            gag_helper_func(user, NULL);
+            break;
+        }
+    }
+
+    /* Add to host info struct */
+    if (!(ohi = dict_find(opserv_hostinfo_dict, inet_ntoa(user->ip), NULL))) {
+        ohi = calloc(1, sizeof(*ohi));
+        dict_insert(opserv_hostinfo_dict, strdup(inet_ntoa(user->ip)), ohi);
+        userList_init(&ohi->clients);
+    }
+    userList_append(&ohi->clients, user);
+
+    /* Only warn of new user floods outside of bursts. */
+    if (!user->uplink->burst) {
+        if (!policer_conforms(&opserv_conf.new_user_policer, now, 10)) {
+            if (!new_user_flood) {
+                new_user_flood = 1;
+                opserv_alert("Warning: Possible new-user flood.");
+            }
+        } else {
+            new_user_flood = 0;
+        }
+    }
+
+    /* Only warn or G-line if there's an untrusted max and their IP is sane. */
+    if (opserv_conf.untrusted_max && user->ip.s_addr && (ntohl(user->ip.s_addr) != INADDR_LOOPBACK)) {
+        struct trusted_host *th = dict_find(opserv_trusted_hosts, inet_ntoa(user->ip), NULL);
+        unsigned int limit = th ? th->limit : opserv_conf.untrusted_max;
+        if (!limit) {
+            /* 0 means unlimited hosts */
+        } else if (ohi->clients.used == limit) {
+            unsigned int nn;
+            for (nn=0; nn<ohi->clients.used; nn++)
+                send_message(ohi->clients.list[nn], opserv, "OSMSG_CLONE_WARNING");
+        } else if (ohi->clients.used > limit) {
+            char target[18];
+            sprintf(target, "*@%s", inet_ntoa(user->ip));
+            gline_add(opserv->nick, target, opserv_conf.clone_gline_duration, "AUTO Excessive connections from a single host.", now, 1);
+        }
+    }
+
+    return 0;
+}
+
+static void
+opserv_user_cleanup(struct userNode *user, UNUSED_ARG(struct userNode *killer), UNUSED_ARG(const char *why))
+{
+    struct opserv_hostinfo *ohi;
+
+    if (IsLocal(user)) {
+        /* Try to remove it from the reserved nick dict without
+         * calling free_reserve, because that would call DelUser(),
+         * and we'd loop back to here. */
+        dict_remove(opserv_reserved_nick_dict, user->nick);
+        return;
+    }
+    if ((ohi = dict_find(opserv_hostinfo_dict, inet_ntoa(user->ip), NULL))) {
+        userList_remove(&ohi->clients, user);
+        if (ohi->clients.used == 0) dict_remove(opserv_hostinfo_dict, inet_ntoa(user->ip));
+    }
+}
+
+int
+opserv_bad_channel(const char *name)
+{
+    unsigned int found;
+
+    dict_find(opserv_exempt_channels, name, &found);
+    if (found)
+        return 0;
+
+    if (gline_find(name))
+        return 1;
+
+    for (found=0; found<opserv_bad_words->used; ++found)
+        if (irccasestr(name, opserv_bad_words->list[found]))
+            return 1;
+
+    return 0;
+}
+
+static void
+opserv_shutdown_channel(struct chanNode *channel, const char *reason)
+{
+    struct mod_chanmode *change;
+    unsigned int nn;
+
+    change = mod_chanmode_alloc(2);
+    change->modes_set = MODE_SECRET | MODE_INVITEONLY;
+    change->args[0].mode = MODE_CHANOP;
+    change->args[0].member = AddChannelUser(opserv, channel);
+    change->args[1].mode = MODE_BAN;
+    change->args[1].hostmask = "*!*@*";
+    mod_chanmode_announce(opserv, channel, change);
+    mod_chanmode_free(change);
+    for (nn=channel->members.used; nn>0; ) {
+        struct modeNode *mNode = channel->members.list[--nn];
+        if (IsService(mNode->user))
+            continue;
+        KickChannelUser(mNode->user, channel, opserv, reason);
+    }
+    timeq_add(now + opserv_conf.purge_lock_delay, opserv_part_channel, channel);
+}
+
+static void
+opserv_channel_check(struct chanNode *newchan)
+{
+    char *warning;
+
+    if (!newchan->join_policer.params) {
+        newchan->join_policer.last_req = now;
+        newchan->join_policer.params = opserv_conf.join_policer_params;
+    }
+    if ((warning = dict_find(opserv_chan_warn, newchan->name, NULL))) {
+        char message[MAXLEN];
+        snprintf(message, sizeof(message), "Channel activity warning for channel %s: %s", newchan->name, warning);
+        global_message(MESSAGE_RECIPIENT_OPERS, message);
+    }
+
+    /* Wait until the join check to shut channels down. */
+    newchan->bad_channel = opserv_bad_channel(newchan->name);
+}
+
+static int
+opserv_join_check(struct modeNode *mNode)
+{
+    struct userNode *user = mNode->user;
+    struct chanNode *channel = mNode->channel;
+    const char *msg;
+
+    if (IsService(user))
+        return 0;
+
+    dict_foreach(opserv_channel_alerts, alert_check_user, user);
+
+    if (channel->bad_channel) {
+        opserv_debug("Found $b%s$b in bad-word channel $b%s$b; removing the user.", user->nick, channel->name);
+        if (channel->name[0] != '#')
+            DelUser(user, opserv, 1, "OSMSG_ILLEGAL_KILL_REASON");
+        else if (!GetUserMode(channel, opserv))
+            opserv_shutdown_channel(channel, user_find_message(user, "OSMSG_ILLEGAL_REASON"));
+        else {
+            send_message(user, opserv, "OSMSG_ILLEGAL_CHANNEL", channel->name);
+            msg = user_find_message(user, "OSMSG_ILLEGAL_REASON");
+            KickChannelUser(user, channel, opserv, msg);
+        }
+        return 1;
+    }
+
+    if (user->uplink->burst)
+        return 0;
+    if (policer_conforms(&channel->join_policer, now, 1.0)) {
+        channel->join_flooded = 0;
+        return 0;
+    }
+    if (!channel->join_flooded) {
+        /* Don't moderate the channel unless it is activated and
+           the number of users in the channel is over the threshold. */
+        struct mod_chanmode change;
+        change.modes_set = change.modes_clear = change.argc = 0;
+        channel->join_flooded = 1;
+        if (opserv_conf.join_flood_moderate && (channel->members.used > opserv_conf.join_flood_moderate_threshold)) {
+            if (!GetUserMode(channel, opserv)) {
+                /* If we aren't in the channel, join it. */
+                change.args[0].mode = MODE_CHANOP;
+                change.args[0].member = AddChannelUser(opserv, channel);
+                change.argc++;
+            }
+            if (!(channel->modes & MODE_MODERATED))
+                change.modes_set |= MODE_MODERATED;
+            if (change.modes_set || change.argc)
+                mod_chanmode_announce(opserv, channel, &change);
+            send_channel_notice(channel, opserv, "OSMSG_FLOOD_MODERATE");
+            opserv_alert("Warning: Possible join flood in %s (currently %d users; channel moderated).", channel->name, channel->members.used);
+        } else {
+            opserv_alert("Warning: Possible join flood in %s (currently %d users).", channel->name, channel->members.used);
+        }
+    }
+    log_module(OS_LOG, LOG_INFO, "Join to %s during flood: "IDENT_FORMAT, channel->name, IDENT_DATA(user));
+    return 0;
+}
+
+static int
+opserv_add_bad_word(struct svccmd *cmd, struct userNode *user, const char *new_bad) {
+    unsigned int bad_idx;
+
+    for (bad_idx = 0; bad_idx < opserv_bad_words->used; ++bad_idx) {
+        char *orig_bad = opserv_bad_words->list[bad_idx];
+        if (irccasestr(new_bad, orig_bad)) {
+            if (user)
+                reply("OSMSG_BAD_REDUNDANT", new_bad, orig_bad);
+            return 0;
+        } else if (irccasestr(orig_bad, new_bad)) {
+            if (user)
+                reply("OSMSG_BAD_GROWING", orig_bad, new_bad);
+            free(orig_bad);
+            opserv_bad_words->list[bad_idx] = strdup(new_bad);
+            for (bad_idx++; bad_idx < opserv_bad_words->used; bad_idx++) {
+                orig_bad = opserv_bad_words->list[bad_idx];
+                if (!irccasestr(orig_bad, new_bad))
+                    continue;
+                if (user)
+                    reply("OSMSG_BAD_NUKING", orig_bad);
+                string_list_delete(opserv_bad_words, bad_idx);
+                bad_idx--;
+                free(orig_bad);
+            }
+            return 1;
+        }
+    }
+    string_list_append(opserv_bad_words, strdup(new_bad));
+    if (user)
+        reply("OSMSG_ADDED_BAD", new_bad);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_addbad)
+{
+    unsigned int arg, count;
+    dict_iterator_t it;
+    int bad_found, exempt_found;
+
+    /* Create the bad word if it doesn't exist. */
+    bad_found = !opserv_add_bad_word(cmd, user, argv[1]);
+
+    /* Look for exception modifiers. */
+    for (arg=2; arg<argc; arg++) {
+        if (!irccasecmp(argv[arg], "except")) {
+            reply("MSG_DEPRECATED_COMMAND", "addbad ... except", "addexempt");
+            if (++arg > argc) {
+                reply("MSG_MISSING_PARAMS", "except");
+                break;
+            }
+            for (count = 0; (arg < argc) && IsChannelName(argv[arg]); arg++) {
+                dict_find(opserv_exempt_channels, argv[arg], &exempt_found);
+                if (!exempt_found) {
+                    dict_insert(opserv_exempt_channels, strdup(argv[arg]), NULL);
+                    count++;
+                }
+            }
+            reply("OSMSG_ADDED_EXEMPTIONS", count);
+        } else {
+            reply("MSG_DEPRECATED_COMMAND", "addbad (with modifiers)", "addbad");
+            reply("OSMSG_BAD_MODIFIER", argv[arg]);
+        }
+    }
+
+    /* Scan for existing channels that match the new bad word. */
+    if (!bad_found) {
+        for (it = dict_first(channels); it; it = iter_next(it)) {
+            struct chanNode *channel = iter_data(it);
+
+            if (!opserv_bad_channel(channel->name))
+                continue;
+            channel->bad_channel = 1;
+            if (channel->name[0] == '#')
+                opserv_shutdown_channel(channel, "OSMSG_ILLEGAL_REASON");
+            else {
+                unsigned int nn;
+                for (nn=0; nn<channel->members.used; nn++) {
+                    struct userNode *user = channel->members.list[nn]->user;
+                    DelUser(user, cmd->parent->bot, 1, "OSMSG_ILLEGAL_KILL_REASON");
+                }
+            }
+        }
+    }
+
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_delbad)
+{
+    dict_iterator_t it;
+    unsigned int nn;
+
+    for (nn=0; nn<opserv_bad_words->used; nn++) {
+        if (!irccasecmp(opserv_bad_words->list[nn], argv[1])) {
+            string_list_delete(opserv_bad_words, nn);
+            for (it = dict_first(channels); it; it = iter_next(it)) {
+                channel = iter_data(it);
+                if (irccasestr(channel->name, argv[1])
+                    && !opserv_bad_channel(channel->name)) {
+                    DelChannelUser(cmd->parent->bot, channel, "Channel name no longer contains a bad word.", 1);
+                    timeq_del(0, opserv_part_channel, channel, TIMEQ_IGNORE_WHEN);
+                    channel->bad_channel = 0;
+                }
+            }
+            reply("OSMSG_REMOVED_BAD", argv[1]);
+            return 1;
+        }
+    }
+    reply("OSMSG_NOT_BAD_WORD", argv[1]);
+    return 0;
+}
+
+static MODCMD_FUNC(cmd_addexempt)
+{
+    const char *chanName;
+
+    if ((argc > 1) && IsChannelName(argv[1])) {
+        chanName = argv[1];
+    } else {
+        reply("MSG_NOT_CHANNEL_NAME");
+        OPSERV_SYNTAX();
+        return 0;
+    }
+    dict_insert(opserv_exempt_channels, strdup(chanName), NULL);
+    channel = GetChannel(chanName);
+    if (channel) {
+        if (channel->bad_channel) {
+            DelChannelUser(cmd->parent->bot, channel, "Channel is now exempt from bad-word checking.", 1);
+            timeq_del(0, opserv_part_channel, channel, TIMEQ_IGNORE_WHEN);
+        }
+        channel->bad_channel = 0;
+    }
+    reply("OSMSG_ADDED_EXEMPTION", chanName);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_delexempt)
+{
+    const char *chanName;
+
+    if ((argc > 1) && IsChannelName(argv[1])) {
+        chanName = argv[1];
+    } else {
+        reply("MSG_NOT_CHANNEL_NAME");
+        OPSERV_SYNTAX();
+        return 0;
+    }
+    if (!dict_remove(opserv_exempt_channels, chanName)) {
+        reply("OSMSG_NOT_EXEMPT", chanName);
+        return 0;
+    }
+    reply("OSMSG_REMOVED_EXEMPTION", chanName);
+    return 1;
+}
+
+static void
+opserv_expire_trusted_host(void *data)
+{
+    struct trusted_host *th = data;
+    dict_remove(opserv_trusted_hosts, th->ipaddr);
+}
+
+static void
+opserv_add_trusted_host(const char *ipaddr, unsigned int limit, const char *issuer, time_t issued, time_t expires, const char *reason)
+{
+    struct trusted_host *th;
+    th = calloc(1, sizeof(*th));
+    if (!th)
+        return;
+    th->ipaddr = strdup(ipaddr);
+    th->reason = reason ? strdup(reason) : NULL;
+    th->issuer = issuer ? strdup(issuer) : NULL;
+    th->issued = issued;
+    th->limit = limit;
+    th->expires = expires;
+    dict_insert(opserv_trusted_hosts, th->ipaddr, th);
+    if (th->expires)
+        timeq_add(th->expires, opserv_expire_trusted_host, th);
+}
+
+static void
+free_trusted_host(void *data)
+{
+    struct trusted_host *th = data;
+    free(th->ipaddr);
+    free(th->reason);
+    free(th->issuer);
+    free(th);
+}
+
+static MODCMD_FUNC(cmd_addtrust)
+{
+    unsigned long interval;
+    char *reason, *tmp;
+    struct in_addr tmpaddr;
+    int count;
+
+    if (dict_find(opserv_trusted_hosts, argv[1], NULL)) {
+        reply("OSMSG_ALREADY_TRUSTED", argv[1]);
+        return 0;
+    }
+
+    if (!inet_aton(argv[1], &tmpaddr)) {
+        reply("OSMSG_BAD_IP", argv[1]);
+        return 0;
+    }
+
+    count = strtoul(argv[2], &tmp, 10);
+    if (!count || *tmp != '\0') {
+        reply("OSMSG_BAD_NUMBER", argv[2]);
+        return 0;
+    }
+
+    interval = ParseInterval(argv[3]);
+    if (!interval && strcmp(argv[3], "0")) {
+        reply("MSG_INVALID_DURATION", argv[3]);
+        return 0;
+    }
+
+    reason = unsplit_string(argv+4, argc-4, NULL);
+    opserv_add_trusted_host(argv[1], count, user->handle_info->handle, now, interval ? (now + interval) : 0, reason);
+    reply("OSMSG_ADDED_TRUSTED");
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_edittrust)
+{
+    unsigned long interval;
+    struct trusted_host *th;
+    char *reason, *tmp;
+    int count;
+
+    th = dict_find(opserv_trusted_hosts, argv[1], NULL);
+    if (!th) {
+        reply("OSMSG_NOT_TRUSTED", argv[1]);
+        return 0;
+    }
+    count = strtoul(argv[2], &tmp, 10);
+    if (!count || *tmp) {
+        reply("OSMSG_BAD_NUMBER", argv[2]);
+        return 0;
+    }
+    interval = ParseInterval(argv[3]);
+    if (!interval && strcmp(argv[3], "0")) {
+        reply("MSG_INVALID_DURATION", argv[3]);
+        return 0;
+    }
+    reason = unsplit_string(argv+4, argc-4, NULL);
+    if (th->expires)
+        timeq_del(th->expires, opserv_expire_trusted_host, th, 0);
+
+    free(th->reason);
+    th->reason = strdup(reason);
+    free(th->issuer);
+    th->issuer = strdup(user->handle_info->handle);
+    th->issued = now;
+    th->limit = count;
+    if (interval) {
+        th->expires = now + interval;
+        timeq_add(th->expires, opserv_expire_trusted_host, th);
+    } else
+        th->expires = 0;
+    reply("OSMSG_UPDATED_TRUSTED", th->ipaddr);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_deltrust)
+{
+    unsigned int n;
+
+    for (n=1; n<argc; n++) {
+        struct trusted_host *th = dict_find(opserv_trusted_hosts, argv[n], NULL);
+        if (!th)
+            continue;
+        if (th->expires)
+            timeq_del(th->expires, opserv_expire_trusted_host, th, 0);
+        dict_remove(opserv_trusted_hosts, argv[n]);
+    }
+    reply("OSMSG_REMOVED_TRUSTED");
+    return 1;
+}
+
+/* This doesn't use dict_t because it's a little simpler to open-code the
+ * comparisons (and simpler arg-passing for the ADD subcommand).
+ */
+static MODCMD_FUNC(cmd_clone)
+{
+    int i;
+    struct userNode *clone;
+
+    clone = GetUserH(argv[2]);
+    if (!irccasecmp(argv[1], "ADD")) {
+        char *userinfo;
+        char ident[USERLEN+1];
+
+       if (argc < 5) {
+           reply("MSG_MISSING_PARAMS", argv[1]);
+           OPSERV_SYNTAX();
+           return 0;
+       }
+       if (clone) {
+           reply("OSMSG_CLONE_EXISTS", argv[2]);
+           return 0;
+       }
+       userinfo = unsplit_string(argv+4, argc-4, NULL);
+       for (i=0; argv[3][i] && (i<USERLEN); i++) {
+           if (argv[3][i] == '@') {
+               ident[i++] = 0;
+               break;
+           } else {
+                ident[i] = argv[3][i];
+            }
+       }
+       if (!argv[3][i] || (i==USERLEN)) {
+           reply("OSMSG_NOT_A_HOSTMASK");
+           return 0;
+       }
+       if (!(clone = AddClone(argv[2], ident, argv[3]+i, userinfo))) {
+            reply("OSMSG_CLONE_FAILED", argv[2]);
+            return 0;
+        }
+        reply("OSMSG_CLONE_ADDED", clone->nick);
+       return 1;
+    }
+    if (!clone) {
+       reply("MSG_NICK_UNKNOWN", argv[2]);
+       return 0;
+    }
+    if (clone->uplink != self || IsService(clone)) {
+       reply("OSMSG_NOT_A_CLONE", clone->nick);
+       return 0;
+    }
+    if (!irccasecmp(argv[1], "REMOVE")) {
+       const char *reason;
+       if (argc > 3) {
+           reason = unsplit_string(argv+3, argc-3, NULL);
+       } else {
+           char *tmp;
+           tmp = alloca(strlen(clone->nick) + strlen(OSMSG_PART_REASON));
+           sprintf(tmp, OSMSG_PART_REASON, clone->nick);
+           reason = tmp;
+       }
+       DelUser(clone, NULL, 1, reason);
+       reply("OSMSG_CLONE_REMOVED", argv[2]);
+       return 1;
+    }
+    if (argc < 4) {
+       reply("MSG_MISSING_PARAMS", argv[1]);
+       OPSERV_SYNTAX();
+       return 0;
+    }
+    channel = GetChannel(argv[3]);
+    if (!irccasecmp(argv[1], "JOIN")) {
+       if (!channel
+           && !(channel = AddChannel(argv[3], now, NULL, NULL))) {
+           reply("MSG_CHANNEL_UNKNOWN", argv[3]);
+           return 0;
+       }
+       AddChannelUser(clone, channel);
+       reply("OSMSG_CLONE_JOINED", clone->nick, channel->name);
+       return 1;
+    }
+    if (!irccasecmp(argv[1], "PART")) {
+       if (!channel) {
+           reply("MSG_CHANNEL_UNKNOWN", argv[3]);
+           return 0;
+       }
+       if (!GetUserMode(channel, clone)) {
+           reply("OSMSG_NOT_ON_CHANNEL", clone->nick, channel->name);
+           return 0;
+       }
+       reply("OSMSG_CLONE_PARTED", clone->nick, channel->name);
+       DelChannelUser(clone, channel, "Leaving.", 0);
+       return 1;
+    }
+    if (!irccasecmp(argv[1], "OP")) {
+        struct mod_chanmode change;
+       if (!channel) {
+           reply("MSG_CHANNEL_UNKNOWN", argv[3]);
+           return 0;
+       }
+        change.modes_set = change.modes_clear = 0;
+        change.argc = 1;
+        change.args[0].mode = MODE_CHANOP;
+        change.args[0].member = GetUserMode(channel, clone);
+        if (!change.args[0].member) {
+            reply("OSMSG_NOT_ON_CHANNEL", clone->nick, channel->name);
+            return 0;
+       }
+        modcmd_chanmode_announce(&change);
+       reply("OSMSG_OPS_GIVEN", channel->name, clone->nick);
+       return 1;
+    }
+    if (argc < 5) {
+       reply("MSG_MISSING_PARAMS", argv[1]);
+       OPSERV_SYNTAX();
+       return 0;
+    }
+    if (!irccasecmp(argv[1], "SAY")) {
+       char *text = unsplit_string(argv+4, argc-4, NULL);
+       irc_privmsg(clone, argv[3], text);
+       reply("OSMSG_CLONE_SAID", clone->nick, argv[3]);
+       return 1;
+    }
+    reply("OSMSG_UNKNOWN_SUBCOMMAND", argv[1], argv[0]);
+    return 0;
+}
+
+static struct helpfile_expansion
+opserv_help_expand(const char *variable)
+{
+    struct helpfile_expansion exp;
+    struct svccmd *cmd;
+    dict_iterator_t it;
+    int row;
+    unsigned int level;
+
+    if (!irccasecmp(variable, "index")) {
+        exp.type = HF_TABLE;
+        exp.value.table.length = 1;
+        exp.value.table.width = 2;
+        exp.value.table.flags = TABLE_REPEAT_HEADERS | TABLE_REPEAT_ROWS;
+        exp.value.table.contents = calloc(dict_size(opserv_service->commands)+1, sizeof(char**));
+        exp.value.table.contents[0] = calloc(exp.value.table.width, sizeof(char*));
+        exp.value.table.contents[0][0] = "Command";
+        exp.value.table.contents[0][1] = "Level";
+        for (it=dict_first(opserv_service->commands); it; it=iter_next(it)) {
+            cmd = iter_data(it);
+            row = exp.value.table.length++;
+            exp.value.table.contents[row] = calloc(exp.value.table.width, sizeof(char*));
+            exp.value.table.contents[row][0] = iter_key(it);
+            level = cmd->min_opserv_level;
+            if (!level_strings[level]) {
+                level_strings[level] = malloc(16);
+                snprintf(level_strings[level], 16, "%3d", level);
+            }
+            exp.value.table.contents[row][1] = level_strings[level];
+        }
+    } else if (!strncasecmp(variable, "level", 5)) {
+        cmd = opserv_get_command(variable+6);
+        exp.type = HF_STRING;
+        if (cmd) {
+            level = cmd->min_opserv_level;
+            exp.value.str = malloc(16);
+            snprintf(exp.value.str, 16, "%3d", level);
+        } else {
+            exp.value.str = NULL;
+        }
+    } else {
+        exp.type = HF_STRING;
+        exp.value.str = NULL;
+    }
+    return exp;
+}
+
+struct modcmd *
+opserv_define_func(const char *name, modcmd_func_t *func, int min_level, int reqchan, int min_argc)
+{
+    char buf[16], *flags = NULL;
+    unsigned int iflags = 0;
+    sprintf(buf, "%d", min_level);
+    switch (reqchan) {
+    case 1: flags = "+acceptchan"; break;
+    case 3: flags = "+acceptpluschan"; /* fall through */
+    case 2: iflags = MODCMD_REQUIRE_CHANNEL; break;
+    }
+    if (flags) {
+        return modcmd_register(opserv_module, name, func, min_argc, iflags, "level", buf, "flags", flags, NULL);
+    } else {
+        return modcmd_register(opserv_module, name, func, min_argc, iflags, "level", buf, NULL);
+    }
+}
+
+int add_reserved(const char *key, void *data, void *extra)
+{
+    struct record_data *rd = data;
+    const char *ident, *hostname, *desc;
+    struct userNode *reserve;
+    ident = database_get_data(rd->d.object, KEY_IDENT, RECDB_QSTRING);
+    if (!ident) {
+       log_module(OS_LOG, LOG_ERROR, "Missing ident for reserve of %s", key);
+       return 0;
+    }
+    hostname = database_get_data(rd->d.object, KEY_HOSTNAME, RECDB_QSTRING);
+    if (!hostname) {
+       log_module(OS_LOG, LOG_ERROR, "Missing hostname for reserve of %s", key);
+       return 0;
+    }
+    desc = database_get_data(rd->d.object, KEY_DESC, RECDB_QSTRING);
+    if (!desc) {
+       log_module(OS_LOG, LOG_ERROR, "Missing description for reserve of %s", key);
+       return 0;
+    }
+    if ((reserve = AddClone(key, ident, hostname, desc))) {
+        reserve->modes |= FLAGS_PERSISTENT;
+        dict_insert(extra, reserve->nick, reserve);
+    }
+    return 0;
+}
+
+static unsigned int
+foreach_matching_user(const char *hostmask, discrim_search_func func, void *extra)
+{
+    discrim_t discrim;
+    char *dupmask;
+    unsigned int matched;
+
+    if (!self->uplink) return 0;
+    discrim = calloc(1, sizeof(*discrim));
+    discrim->limit = dict_size(clients);
+    discrim->max_level = ~0;
+    discrim->max_ts = now;
+    discrim->max_channels = INT_MAX;
+    discrim->authed = -1;
+    discrim->info_space = -1;
+    dupmask = strdup(hostmask);
+    if (split_ircmask(dupmask, &discrim->mask_nick, &discrim->mask_ident, &discrim->mask_host)) {
+        if (discrim->mask_host && !discrim->mask_host[strspn(discrim->mask_host, "0123456789.?*")]) {
+            if (!parse_ipmask(discrim->mask_host, &discrim->ip_addr, &discrim->ip_mask)) {
+                log_module(OS_LOG, LOG_ERROR, "Couldn't parse %s as an IP mask!", discrim->mask_host);
+                free(discrim);
+                free(dupmask);
+                return 0;
+            }
+            discrim->mask_host = 0;
+        }
+        matched = opserv_discrim_search(discrim, func, extra);
+    } else {
+       log_module(OS_LOG, LOG_ERROR, "Couldn't split IRC mask for gag %s!", hostmask);
+        matched = 0;
+    }
+    free(discrim);
+    free(dupmask);
+    return matched;
+}
+
+static unsigned int
+gag_free(struct gag_entry *gag)
+{
+    unsigned int ungagged;
+
+    /* Remove from gag list */
+    if (gagList == gag) {
+        gagList = gag->next;
+    } else {
+        struct gag_entry *prev;
+        for (prev = gagList; prev->next != gag; prev = prev->next) ;
+        prev->next = gag->next;
+    }
+
+    ungagged = foreach_matching_user(gag->mask, ungag_helper_func, NULL);
+
+    /* Deallocate storage */
+    free(gag->reason);
+    free(gag->owner);
+    free(gag->mask);
+    free(gag);
+
+    return ungagged;
+}
+
+static void
+gag_expire(void *data)
+{
+    gag_free(data);
+}
+
+unsigned int
+gag_create(const char *mask, const char *owner, const char *reason, time_t expires)
+{
+    struct gag_entry *gag;
+
+    /* Create gag and put it into linked list */
+    gag = calloc(1, sizeof(*gag));
+    gag->mask = strdup(mask);
+    gag->owner = strdup(owner ? owner : "<unknown>");
+    gag->reason = strdup(reason ? reason : "<unknown>");
+    gag->expires = expires;
+    if (gag->expires)
+        timeq_add(gag->expires, gag_expire, gag);
+    gag->next = gagList;
+    gagList = gag;
+
+    /* If we're linked, see if who the gag applies to */
+    return foreach_matching_user(mask, gag_helper_func, gag);
+}
+
+static int
+add_gag_helper(const char *key, void *data, UNUSED_ARG(void *extra))
+{
+    struct record_data *rd = data;
+    char *owner, *reason, *expstr;
+    time_t expires;
+
+    owner = database_get_data(rd->d.object, KEY_OWNER, RECDB_QSTRING);
+    reason = database_get_data(rd->d.object, KEY_REASON, RECDB_QSTRING);
+    expstr = database_get_data(rd->d.object, KEY_EXPIRES, RECDB_QSTRING);
+    expires = expstr ? strtoul(expstr, NULL, 0) : 0;
+    gag_create(key, owner, reason, expires);
+
+    return 0;
+}
+
+static struct opserv_user_alert *
+opserv_add_user_alert(struct userNode *req, const char *name, opserv_alert_reaction reaction, const char *text_discrim)
+{
+    unsigned int wordc;
+    char *wordv[MAXNUMPARAMS], *discrim_copy;
+    struct opserv_user_alert *alert;
+    char *name_dup;
+
+    if (dict_find(opserv_user_alerts, name, NULL)) {
+       send_message(req, opserv, "OSMSG_ALERT_EXISTS", name);
+       return NULL;
+    }
+    alert = malloc(sizeof(*alert));
+    alert->owner = strdup(req->handle_info ? req->handle_info->handle : req->nick);
+    alert->text_discrim = strdup(text_discrim);
+    discrim_copy = strdup(text_discrim); /* save a copy of the discrim */
+    wordc = split_line(discrim_copy, false, ArrayLength(wordv), wordv);
+    alert->discrim = opserv_discrim_create(req, wordc, wordv, 0);
+    if (!alert->discrim) {
+        free(alert->text_discrim);
+        free(discrim_copy);
+        free(alert);
+        return NULL;
+    }
+    alert->split_discrim = discrim_copy;
+    name_dup = strdup(name);
+    if (!alert->discrim->reason)
+        alert->discrim->reason = strdup(name);
+    alert->reaction = reaction;
+    dict_insert(opserv_user_alerts, name_dup, alert);
+    if (alert->discrim->channel)
+        dict_insert(opserv_channel_alerts, name_dup, alert);
+    else if (alert->discrim->mask_nick)
+        dict_insert(opserv_nick_based_alerts, name_dup, alert);
+    return alert;
+}
+
+static int
+add_chan_warn(const char *key, void *data, UNUSED_ARG(void *extra))
+{
+    struct record_data *rd = data;
+    char *reason = GET_RECORD_QSTRING(rd);
+
+    /* i hope this can't happen */
+    if (!reason)
+        reason = "No Reason";
+
+    dict_insert(opserv_chan_warn, strdup(key), strdup(reason));
+    return 0;
+}
+
+static int
+add_user_alert(const char *key, void *data, UNUSED_ARG(void *extra))
+{
+    dict_t alert_dict;
+    const char *discrim, *react, *owner;
+    opserv_alert_reaction reaction;
+    struct opserv_user_alert *alert;
+
+    if (!(alert_dict = GET_RECORD_OBJECT((struct record_data *)data))) {
+        log_module(OS_LOG, LOG_ERROR, "Bad type (not a record) for alert %s.", key);
+        return 1;
+    }
+    discrim = database_get_data(alert_dict, KEY_DISCRIM, RECDB_QSTRING);
+    react = database_get_data(alert_dict, KEY_REACTION, RECDB_QSTRING);
+    if (!react || !irccasecmp(react, "notice"))
+        reaction = REACT_NOTICE;
+    else if (!irccasecmp(react, "kill"))
+        reaction = REACT_KILL;
+    else if (!irccasecmp(react, "gline"))
+        reaction = REACT_GLINE;
+    else {
+        log_module(OS_LOG, LOG_ERROR, "Invalid reaction %s for alert %s.", react, key);
+        return 0;
+    }
+    alert = opserv_add_user_alert(opserv, key, reaction, discrim);
+    if (!alert) {
+        log_module(OS_LOG, LOG_ERROR, "Unable to create alert %s from database.", key);
+        return 0;
+    }
+    owner = database_get_data(alert_dict, KEY_OWNER, RECDB_QSTRING);
+    free(alert->owner);
+    alert->owner = strdup(owner ? owner : "<unknown>");
+    return 0;
+}
+
+static int
+trusted_host_read(const char *host, void *data, UNUSED_ARG(void *extra))
+{
+    struct record_data *rd = data;
+    const char *limit, *str, *reason, *issuer;
+    time_t issued, expires;
+
+    if (rd->type == RECDB_QSTRING) {
+        /* old style host by itself */
+        limit = GET_RECORD_QSTRING(rd);
+        issued = 0;
+        issuer = NULL;
+        expires = 0;
+        reason = NULL;
+    } else if (rd->type == RECDB_OBJECT) {
+        dict_t obj = GET_RECORD_OBJECT(rd);
+        /* new style structure */
+        limit = database_get_data(obj, KEY_LIMIT, RECDB_QSTRING);
+        str = database_get_data(obj, KEY_EXPIRES, RECDB_QSTRING);
+        expires = str ? ParseInterval(str) : 0;
+        reason = database_get_data(obj, KEY_REASON, RECDB_QSTRING);
+        issuer = database_get_data(obj, KEY_ISSUER, RECDB_QSTRING);
+        str = database_get_data(obj, KEY_ISSUED, RECDB_QSTRING);
+        issued = str ? ParseInterval(str) : 0;
+    } else
+        return 0;
+
+    if (expires && (expires < now))
+        return 0;
+    opserv_add_trusted_host(host, (limit ? strtoul(limit, NULL, 0) : 0), issuer, issued, expires, reason);
+    return 0;
+}
+
+static int
+opserv_saxdb_read(struct dict *conf_db)
+{
+    dict_t object;
+    struct record_data *rd;
+    dict_iterator_t it;
+    unsigned int nn;
+
+    if ((object = database_get_data(conf_db, KEY_RESERVES, RECDB_OBJECT)))
+        dict_foreach(object, add_reserved, opserv_reserved_nick_dict);
+    if ((rd = database_get_path(conf_db, KEY_BAD_WORDS))) {
+        switch (rd->type) {
+        case RECDB_STRING_LIST:
+            /* Add words one by one just in case there are overlaps from an old DB. */
+            for (nn=0; nn<rd->d.slist->used; ++nn)
+                opserv_add_bad_word(NULL, NULL, rd->d.slist->list[nn]);
+            break;
+        case RECDB_OBJECT:
+            for (it=dict_first(rd->d.object); it; it=iter_next(it)) {
+                opserv_add_bad_word(NULL, NULL, iter_key(it));
+                rd = iter_data(it);
+                if (rd->type == RECDB_STRING_LIST)
+                    for (nn=0; nn<rd->d.slist->used; nn++)
+                        dict_insert(opserv_exempt_channels, strdup(rd->d.slist->list[nn]), NULL);
+            }
+            break;
+        default:
+            /* do nothing */;
+        }
+    }
+    if ((rd = database_get_path(conf_db, KEY_EXEMPT_CHANNELS))
+        && (rd->type == RECDB_STRING_LIST)) {
+        for (nn=0; nn<rd->d.slist->used; ++nn)
+            dict_insert(opserv_exempt_channels, strdup(rd->d.slist->list[nn]), NULL);
+    }
+    if ((object = database_get_data(conf_db, KEY_MAX_CLIENTS, RECDB_OBJECT))) {
+        char *str;
+        if ((str = database_get_data(object, KEY_MAX, RECDB_QSTRING)))
+            max_clients = atoi(str);
+        if ((str = database_get_data(object, KEY_TIME, RECDB_QSTRING)))
+            max_clients_time = atoi(str);
+    }
+    if ((object = database_get_data(conf_db, KEY_TRUSTED_HOSTS, RECDB_OBJECT)))
+        dict_foreach(object, trusted_host_read, opserv_trusted_hosts);
+    if ((object = database_get_data(conf_db, KEY_GAGS, RECDB_OBJECT)))
+        dict_foreach(object, add_gag_helper, NULL);
+    if ((object = database_get_data(conf_db, KEY_ALERTS, RECDB_OBJECT)))
+        dict_foreach(object, add_user_alert, NULL);
+    if ((object = database_get_data(conf_db, KEY_WARN, RECDB_OBJECT)))
+        dict_foreach(object, add_chan_warn, NULL);
+    return 0;
+}
+
+static int
+opserv_saxdb_write(struct saxdb_context *ctx)
+{
+    struct string_list *slist;
+    dict_iterator_t it;
+
+    /* reserved nicks */
+    if (dict_size(opserv_reserved_nick_dict)) {
+        saxdb_start_record(ctx, KEY_RESERVES, 1);
+        for (it = dict_first(opserv_reserved_nick_dict); it; it = iter_next(it)) {
+            struct userNode *user = iter_data(it);
+            if (!IsPersistent(user)) continue;
+            saxdb_start_record(ctx, iter_key(it), 0);
+            saxdb_write_string(ctx, KEY_IDENT, user->ident);
+            saxdb_write_string(ctx, KEY_HOSTNAME, user->hostname);
+            saxdb_write_string(ctx, KEY_DESC, user->info);
+            saxdb_end_record(ctx);
+        }
+        saxdb_end_record(ctx);
+    }
+    /* bad word set */
+    if (opserv_bad_words->used) {
+        saxdb_write_string_list(ctx, KEY_BAD_WORDS, opserv_bad_words);
+    }
+    /* insert exempt channel names */
+    if (dict_size(opserv_exempt_channels)) {
+        slist = alloc_string_list(dict_size(opserv_exempt_channels));
+        for (it=dict_first(opserv_exempt_channels); it; it=iter_next(it)) {
+            string_list_append(slist, strdup(iter_key(it)));
+        }
+        saxdb_write_string_list(ctx, KEY_EXEMPT_CHANNELS, slist);
+        free_string_list(slist);
+    }
+    /* trusted hosts takes a little more work */
+    if (dict_size(opserv_trusted_hosts)) {
+        saxdb_start_record(ctx, KEY_TRUSTED_HOSTS, 1);
+        for (it = dict_first(opserv_trusted_hosts); it; it = iter_next(it)) {
+            struct trusted_host *th = iter_data(it);
+            saxdb_start_record(ctx, iter_key(it), 0);
+            if (th->limit) saxdb_write_int(ctx, KEY_LIMIT, th->limit);
+            if (th->expires) saxdb_write_int(ctx, KEY_EXPIRES, th->expires);
+            if (th->issued) saxdb_write_int(ctx, KEY_ISSUED, th->issued);
+            if (th->issuer) saxdb_write_string(ctx, KEY_ISSUER, th->issuer);
+            if (th->reason) saxdb_write_string(ctx, KEY_REASON, th->reason);
+            saxdb_end_record(ctx);
+        }
+        saxdb_end_record(ctx);
+    }
+    /* gags */
+    if (gagList) {
+        struct gag_entry *gag;
+        saxdb_start_record(ctx, KEY_GAGS, 1);
+        for (gag = gagList; gag; gag = gag->next) {
+            saxdb_start_record(ctx, gag->mask, 0);
+            saxdb_write_string(ctx, KEY_OWNER, gag->owner);
+            saxdb_write_string(ctx, KEY_REASON, gag->reason);
+            if (gag->expires) saxdb_write_int(ctx, KEY_EXPIRES, gag->expires);
+            saxdb_end_record(ctx);
+        }
+        saxdb_end_record(ctx);
+    }
+    /* channel warnings */
+    if (dict_size(opserv_chan_warn)) {
+        saxdb_start_record(ctx, KEY_WARN, 0);
+        for (it = dict_first(opserv_chan_warn); it; it = iter_next(it)) {
+            saxdb_write_string(ctx, iter_key(it), iter_data(it));
+        }
+        saxdb_end_record(ctx);
+    }
+    /* alerts */
+    if (dict_size(opserv_user_alerts)) {
+        saxdb_start_record(ctx, KEY_ALERTS, 1);
+        for (it = dict_first(opserv_user_alerts); it; it = iter_next(it)) {
+            struct opserv_user_alert *alert = iter_data(it);
+            const char *reaction;
+            saxdb_start_record(ctx, iter_key(it), 0);
+            saxdb_write_string(ctx, KEY_DISCRIM, alert->text_discrim);
+            saxdb_write_string(ctx, KEY_OWNER, alert->owner);
+            switch (alert->reaction) {
+            case REACT_NOTICE: reaction = "notice"; break;
+            case REACT_KILL: reaction = "kill"; break;
+            case REACT_GLINE: reaction = "gline"; break;
+            default:
+                reaction = NULL;
+                log_module(OS_LOG, LOG_ERROR, "Invalid reaction type %d for alert %s (while writing database).", alert->reaction, iter_key(it));
+                break;
+            }
+            if (reaction) saxdb_write_string(ctx, KEY_REACTION, reaction);
+            saxdb_end_record(ctx);
+        }
+        saxdb_end_record(ctx);
+    }
+    /* max clients */
+    saxdb_start_record(ctx, KEY_MAX_CLIENTS, 0);
+    saxdb_write_int(ctx, KEY_MAX, max_clients);
+    saxdb_write_int(ctx, KEY_TIME, max_clients_time);
+    saxdb_end_record(ctx);
+    return 0;
+}
+
+static int
+query_keys_helper(const char *key, UNUSED_ARG(void *data), void *extra)
+{
+    send_message_type(4, extra, opserv, "$b%s$b", key);
+    return 0;
+}
+
+static MODCMD_FUNC(cmd_query)
+{
+    struct record_data *rd;
+    unsigned int i;
+    char *nodename;
+
+    if (argc < 2) {
+       reply("OSMSG_OPTION_ROOT");
+       conf_enum_root(query_keys_helper, user);
+       return 1;
+    }
+
+    nodename = unsplit_string(argv+1, argc-1, NULL);
+    if (!(rd = conf_get_node(nodename))) {
+       reply("OSMSG_UNKNOWN_OPTION", nodename);
+       return 0;
+    }
+
+    if (rd->type == RECDB_QSTRING)
+       reply("OSMSG_OPTION_IS", nodename, rd->d.qstring);
+    else if (rd->type == RECDB_STRING_LIST) {
+       reply("OSMSG_OPTION_LIST", nodename);
+       if (rd->d.slist->used)
+           for (i=0; i<rd->d.slist->used; i++)
+               send_message_type(4, user, cmd->parent->bot, "$b%s$b", rd->d.slist->list[i]);
+       else
+           reply("OSMSG_OPTION_LIST_EMPTY");
+    } else if (rd->type == RECDB_OBJECT) {
+       reply("OSMSG_OPTION_KEYS", nodename);
+       dict_foreach(rd->d.object, query_keys_helper, user);
+    }
+
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_set)
+{
+    struct record_data *rd;
+
+    /* I originally wanted to be able to fully manipulate the config
+       db with this, but i wussed out. feel free to fix this - you'll
+       need to handle quoted strings which have been split, and likely
+       invent a syntax for it. -Zoot */
+
+    if (!(rd = conf_get_node(argv[1]))) {
+       reply("OSMSG_SET_NOT_SET", argv[1]);
+       return 0;
+    }
+
+    if (rd->type != RECDB_QSTRING) {
+       reply("OSMSG_SET_BAD_TYPE", argv[1]);
+       return 0;
+    }
+
+    free(rd->d.qstring);
+    rd->d.qstring = strdup(argv[2]);
+    conf_call_reload_funcs();
+    reply("OSMSG_SET_SUCCESS", argv[1], argv[2]);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_settime)
+{
+    const char *srv_name_mask = "*";
+    time_t new_time = now;
+
+    if (argc > 1)
+        srv_name_mask = argv[1];
+    if (argc > 2)
+        new_time = time(NULL);
+    irc_settime(srv_name_mask, new_time);
+    reply("OSMSG_SETTIME_SUCCESS", srv_name_mask);
+    return 1;
+}
+
+static discrim_t
+opserv_discrim_create(struct userNode *user, unsigned int argc, char *argv[], int allow_channel)
+{
+    unsigned int i, j;
+    discrim_t discrim;
+
+    discrim = calloc(1, sizeof(*discrim));
+    discrim->limit = 250;
+    discrim->max_level = ~0;
+    discrim->max_ts = INT_MAX;
+    discrim->domain_depth = 2;
+    discrim->max_channels = INT_MAX;
+    discrim->authed = -1;
+    discrim->info_space = -1;
+
+    for (i=0; i<argc; i++) {
+        if (irccasecmp(argv[i], "log") == 0) {
+            discrim->option_log = 1;
+            continue;
+        }
+        /* Assume all other criteria require arguments. */
+        if (i == argc - 1) {
+            send_message(user, opserv, "MSG_MISSING_PARAMS", argv[i]);
+            goto fail;
+        }
+       if (irccasecmp(argv[i], "mask") == 0) {
+           if (!is_ircmask(argv[++i])) {
+               send_message(user, opserv, "OSMSG_INVALID_IRCMASK", argv[i]);
+               goto fail;
+           }
+           if (!split_ircmask(argv[i],
+                               &discrim->mask_nick,
+                               &discrim->mask_ident,
+                               &discrim->mask_host)) {
+               send_message(user, opserv, "OSMSG_INVALID_IRCMASK", argv[i]);
+               goto fail;
+           }
+       } else if (irccasecmp(argv[i], "nick") == 0) {
+           discrim->mask_nick = argv[++i];
+       } else if (irccasecmp(argv[i], "ident") == 0) {
+           discrim->mask_ident = argv[++i];
+       } else if (irccasecmp(argv[i], "host") == 0) {
+           discrim->mask_host = argv[++i];
+       } else if (irccasecmp(argv[i], "info") == 0) {
+           discrim->mask_info = argv[++i];
+       } else if (irccasecmp(argv[i], "server") == 0) {
+           discrim->server = argv[++i];
+       } else if (irccasecmp(argv[i], "ip") == 0) {
+           j = parse_ipmask(argv[++i], &discrim->ip_addr, &discrim->ip_mask);
+           if (!j) discrim->ip_mask_str = argv[i];
+    } else if (irccasecmp(argv[i], "account") == 0) {
+        if (discrim->authed == 0) {
+            send_message(user, opserv, "OSMSG_ACCOUNTMASK_AUTHED");
+            goto fail;
+        }
+        discrim->accountmask = argv[++i];
+        discrim->authed = 1;
+    } else if (irccasecmp(argv[i], "authed") == 0) {
+        i++; /* true_string and false_string are macros! */
+        if (true_string(argv[i])) {
+            discrim->authed = 1;
+        } else if (false_string(argv[i])) {
+            if (discrim->accountmask) {
+                send_message(user, opserv, "OSMSG_ACCOUNTMASK_AUTHED");
+                goto fail;
+            }
+            discrim->authed = 0;
+        } else {
+            send_message(user, opserv, "MSG_INVALID_BINARY", argv[i]);
+            goto fail;
+        }
+    } else if (irccasecmp(argv[i], "info_space") == 0) {
+        /* XXX: A hack because you can't check explicitly for a space through
+         * any other means */
+        i++;
+        if (true_string(argv[i])) {
+            discrim->info_space = 1;
+        } else if (false_string(argv[i])) {
+            discrim->info_space = 0;
+        } else {
+            send_message(user, opserv, "MSG_INVALID_BINARY", argv[i]);
+            goto fail;
+        }
+    } else if (irccasecmp(argv[i], "duration") == 0) {
+        discrim->duration = ParseInterval(argv[++i]);
+       } else if (irccasecmp(argv[i], "channel") == 0) {
+            for (j=0, i++; ; j++) {
+                switch (argv[i][j]) {
+                case '#':
+                    goto find_channel;
+                case '-':
+                    discrim->chan_no_modes  |= MODE_CHANOP | MODE_VOICE;
+                    break;
+                case '+':
+                    discrim->chan_req_modes |= MODE_VOICE;
+                    discrim->chan_no_modes  |= MODE_CHANOP;
+                    break;
+                case '@':
+                    discrim->chan_req_modes |= MODE_CHANOP;
+                    break;
+                case '\0':
+                    send_message(user, opserv, "MSG_NOT_CHANNEL_NAME");
+                    goto fail;
+                }
+            }
+          find_channel:
+            discrim->chan_no_modes &= ~discrim->chan_req_modes;
+           if (!(discrim->channel = GetChannel(argv[i]+j))) {
+                /* secretly "allow_channel" now means "if a channel name is
+                 * specified, require that it currently exist" */
+                if (allow_channel) {
+                    send_message(user, opserv, "MSG_CHANNEL_UNKNOWN", argv[i]);
+                    goto fail;
+                } else {
+                    discrim->channel = AddChannel(argv[i]+j, now, NULL, NULL);
+                }
+           }
+            LockChannel(discrim->channel);
+        } else if (irccasecmp(argv[i], "numchannels") == 0) {
+            discrim->min_channels = discrim->max_channels = strtoul(argv[++i], NULL, 10);
+       } else if (irccasecmp(argv[i], "limit") == 0) {
+           discrim->limit = strtoul(argv[++i], NULL, 10);
+        } else if (irccasecmp(argv[i], "reason") == 0) {
+            discrim->reason = strdup(unsplit_string(argv+i+1, argc-i-1, NULL));
+            i = argc;
+        } else if (irccasecmp(argv[i], "last") == 0) {
+            discrim->min_ts = now - ParseInterval(argv[++i]);
+        } else if ((irccasecmp(argv[i], "linked") == 0)
+                   || (irccasecmp(argv[i], "nickage") == 0)) {
+            const char *cmp = argv[++i];
+            if (cmp[0] == '<') {
+                if (cmp[1] == '=') {
+                    discrim->min_ts = now - ParseInterval(cmp+2);
+                } else {
+                    discrim->min_ts = now - (ParseInterval(cmp+1) - 1);
+                }
+            } else if (cmp[0] == '>') {
+                if (cmp[1] == '=') {
+                    discrim->max_ts = now - ParseInterval(cmp+2);
+                } else {
+                    discrim->max_ts = now - (ParseInterval(cmp+1) - 1);
+                }
+            } else {
+                discrim->min_ts = now - ParseInterval(cmp+2);
+            }
+        } else if (irccasecmp(argv[i], "access") == 0) {
+            const char *cmp = argv[++i];
+            if (cmp[0] == '<') {
+                if (discrim->min_level == 0) discrim->min_level = 1;
+                if (cmp[1] == '=') {
+                    discrim->max_level = strtoul(cmp+2, NULL, 0);
+                } else {
+                    discrim->max_level = strtoul(cmp+1, NULL, 0) - 1;
+                }
+            } else if (cmp[0] == '=') {
+                discrim->min_level = discrim->max_level = strtoul(cmp+1, NULL, 0);
+            } else if (cmp[0] == '>') {
+                if (cmp[1] == '=') {
+                    discrim->min_level = strtoul(cmp+2, NULL, 0);
+                } else {
+                    discrim->min_level = strtoul(cmp+1, NULL, 0) + 1;
+                }
+            } else {
+                discrim->min_level = strtoul(cmp+2, NULL, 0);
+            }
+        } else if ((irccasecmp(argv[i], "abuse") == 0)
+                   && (irccasecmp(argv[++i], "opers") == 0)) {
+            discrim->match_opers = 1;
+        } else if (irccasecmp(argv[i], "depth") == 0) {
+            discrim->domain_depth = strtoul(argv[++i], NULL, 0);
+        } else if (irccasecmp(argv[i], "clones") == 0) {
+            discrim->min_clones = strtoul(argv[++i], NULL, 0);
+        } else {
+            send_message(user, opserv, "MSG_INVALID_CRITERIA", argv[i]);
+            goto fail;
+        }
+    }
+
+    if (discrim->mask_nick && !strcmp(discrim->mask_nick, "*")) {
+       discrim->mask_nick = 0;
+    }
+    if (discrim->mask_ident && !strcmp(discrim->mask_ident, "*")) {
+        discrim->mask_ident = 0;
+    }
+    if (discrim->mask_info && !strcmp(discrim->mask_info, "*")) {
+       discrim->mask_info = 0;
+    }
+    if (discrim->mask_host && !discrim->mask_host[strspn(discrim->mask_host, "*.")]) {
+        discrim->mask_host = 0;
+    }
+    return discrim;
+  fail:
+    free(discrim);
+    return NULL;
+}
+
+static int
+discrim_match(discrim_t discrim, struct userNode *user)
+{
+    unsigned int access;
+
+    if ((user->timestamp < discrim->min_ts)
+        || (user->timestamp > discrim->max_ts)
+        || (user->channels.used < discrim->min_channels)
+        || (user->channels.used > discrim->max_channels)
+        || (discrim->authed == 0 && user->handle_info)
+        || (discrim->authed == 1 && !user->handle_info)
+        || (discrim->info_space == 0 && user->info[0] == ' ')
+        || (discrim->info_space == 1 && user->info[0] != ' ')
+        || (discrim->mask_nick && !match_ircglob(user->nick, discrim->mask_nick))
+        || (discrim->mask_ident && !match_ircglob(user->ident, discrim->mask_ident))
+        || (discrim->mask_host && !match_ircglob(user->hostname, discrim->mask_host))
+        || (discrim->mask_info && !match_ircglob(user->info, discrim->mask_info))
+        || (discrim->server && !match_ircglob(user->uplink->name, discrim->server))
+        || (discrim->accountmask && (!user->handle_info || !match_ircglob(user->handle_info->handle, discrim->accountmask)))
+        || (discrim->ip_mask && !MATCH_IPMASK(user->ip, discrim->ip_addr, discrim->ip_mask))) {
+        return 0;
+    }
+    if (discrim->channel && !GetUserMode(discrim->channel, user)) return 0;
+    access = user->handle_info ? user->handle_info->opserv_level : 0;
+    if ((access < discrim->min_level)
+        || (access > discrim->max_level)) {
+        return 0;
+    }
+    if (discrim->ip_mask_str) {
+        if (!match_ircglob(inet_ntoa(user->ip), discrim->ip_mask_str)) return 0;
+    }
+    if (discrim->min_clones > 1) {
+        struct opserv_hostinfo *ohi = dict_find(opserv_hostinfo_dict, inet_ntoa(user->ip), NULL);
+        if (!ohi || (ohi->clients.used < discrim->min_clones)) return 0;
+    }
+    return 1;
+}
+
+static unsigned int
+opserv_discrim_search(discrim_t discrim, discrim_search_func dsf, void *data)
+{
+    unsigned int nn, count;
+    struct userList matched;
+
+    userList_init(&matched);
+    /* Try most optimized search methods first */
+    if (discrim->channel) {
+        for (nn=0;
+                (nn < discrim->channel->members.used)
+                && (matched.used < discrim->limit);
+                nn++) {
+            struct modeNode *mn = discrim->channel->members.list[nn];
+            if (((mn->modes & discrim->chan_req_modes) != discrim->chan_req_modes)
+                    || ((mn->modes & discrim->chan_no_modes) != 0)) {
+                continue;
+            }
+            if (discrim_match(discrim, mn->user)) {
+                userList_append(&matched, mn->user);
+            }
+        }
+    } else if (discrim->ip_mask_str && !discrim->ip_mask_str[strcspn(discrim->ip_mask_str, "?*")]) {
+        struct opserv_hostinfo *ohi = dict_find(opserv_hostinfo_dict, discrim->ip_mask_str, NULL);
+        if (!ohi) {
+            userList_clean(&matched);
+            return 0;
+        }
+        for (nn=0; (nn<ohi->clients.used) && (matched.used < discrim->limit); nn++) {
+            if (discrim_match(discrim, ohi->clients.list[nn])) {
+                userList_append(&matched, ohi->clients.list[nn]);
+            }
+        }
+    } else {
+        dict_iterator_t it;
+        for (it=dict_first(clients); it && (matched.used < discrim->limit); it=iter_next(it)) {
+            if (discrim_match(discrim, iter_data(it))) {
+                userList_append(&matched, iter_data(it));
+            }
+        }
+    }
+
+    if (!matched.used) {
+        userList_clean(&matched);
+        return 0;
+    }
+
+    if (discrim->option_log) {
+        log_module(OS_LOG, LOG_INFO, "Logging matches for search:");
+    }
+    for (nn=0; nn<matched.used; nn++) {
+        struct userNode *user = matched.list[nn];
+        if (discrim->option_log) {
+            log_module(OS_LOG, LOG_INFO, "  %s!%s@%s", user->nick, user->ident, user->hostname);
+        }
+        if (dsf(user, data)) {
+           /* If a search function returns true, it ran into a
+              problem. Stop going through the list. */
+           break;
+       }
+    }
+    if (discrim->option_log) {
+        log_module(OS_LOG, LOG_INFO, "End of matching users.");
+    }
+    count = matched.used;
+    userList_clean(&matched);
+    return count;
+}
+
+static int
+trace_print_func(struct userNode *match, void *extra)
+{
+    struct discrim_and_source *das = extra;
+    if (match->handle_info) {
+        send_message_type(4, das->source, opserv, "%s!%s@%s %s", match->nick, match->ident, match->hostname, match->handle_info->handle);
+    } else {
+        send_message_type(4, das->source, opserv, "%s!%s@%s", match->nick, match->ident, match->hostname);
+    }
+    return 0;
+}
+
+static int
+trace_count_func(UNUSED_ARG(struct userNode *match), UNUSED_ARG(void *extra))
+{
+    return 0;
+}
+
+static int
+is_oper_victim(struct userNode *user, struct userNode *target, int match_opers)
+{
+    return !(IsService(target)
+             || (!match_opers && IsOper(target))
+             || (target->handle_info
+                 && target->handle_info->opserv_level > user->handle_info->opserv_level));
+}
+
+static int
+trace_gline_func(struct userNode *match, void *extra)
+{
+    struct discrim_and_source *das = extra;
+
+    if (is_oper_victim(das->source, match, das->discrim->match_opers)) {
+        opserv_block(match, das->source->handle_info->handle, das->discrim->reason, das->discrim->duration);
+    }
+
+    return 0;
+}
+
+static int
+trace_kill_func(struct userNode *match, void *extra)
+{
+    struct discrim_and_source *das = extra;
+
+    if (is_oper_victim(das->source, match, das->discrim->match_opers)) {
+       char *reason;
+        if (das->discrim->reason) {
+            reason = das->discrim->reason;
+        } else {
+            reason = alloca(strlen(OSMSG_KILL_REQUESTED)+strlen(das->source->nick)+1);
+            sprintf(reason, OSMSG_KILL_REQUESTED, das->source->nick);
+        }
+        DelUser(match, opserv, 1, reason);
+    }
+
+    return 0;
+}
+
+static int
+is_gagged(char *mask)
+{
+    struct gag_entry *gag;
+
+    for (gag = gagList; gag; gag = gag->next) {
+        if (match_ircglobs(gag->mask, mask)) return 1;
+    }
+    return 0;
+}
+
+static int
+trace_gag_func(struct userNode *match, void *extra)
+{
+    struct discrim_and_source *das = extra;
+
+    if (is_oper_victim(das->source, match, das->discrim->match_opers)) {
+        char *reason, *mask;
+        int masksize;
+        if (das->discrim->reason) {
+            reason = das->discrim->reason;
+        } else {
+            reason = alloca(strlen(OSMSG_GAG_REQUESTED)+strlen(das->source->nick)+1);
+            sprintf(reason, OSMSG_GAG_REQUESTED, das->source->nick);
+        }
+       masksize = 5+strlen(match->hostname);
+       mask = alloca(masksize);
+        snprintf(mask, masksize, "*!*@%s", match->hostname);
+       if (!is_gagged(mask)) {
+            gag_create(mask, das->source->handle_info->handle, reason,
+                       das->discrim->duration ? (now + das->discrim->duration) : 0);
+        }
+    }
+
+    return 0;
+}
+
+static int
+trace_domains_func(struct userNode *match, void *extra)
+{
+    struct discrim_and_source *das = extra;
+    unsigned long *count;
+    unsigned int depth;
+    char *hostname;
+
+    if (!match->hostname[strspn(match->hostname, "0123456789.")]) {
+        char ipmask[16];
+        unsigned long matchip = ntohl(match->ip.s_addr);
+        /* raw IP address.. use up to first three octets of IP */
+        switch (das->discrim->domain_depth) {
+        default:
+            snprintf(ipmask, sizeof(ipmask), "%lu.%lu.%lu.*", (matchip>>24)&255, (matchip>>16)&255, (matchip>>8)&255);
+            break;
+        case 2:
+            snprintf(ipmask, sizeof(ipmask), "%lu.%lu.*", (matchip>>24)&255, (matchip>>16)&255);
+            break;
+        case 1:
+            snprintf(ipmask, sizeof(ipmask), "%lu.*", (matchip>>24)&255);
+            break;
+        }
+        hostname = ipmask;
+    } else {
+        hostname = match->hostname + strlen(match->hostname);
+        for (depth=das->discrim->domain_depth; 
+             depth && (hostname > match->hostname);
+             depth--) {
+            hostname--;
+            while ((hostname > match->hostname) && (*hostname != '.')) hostname--;
+        }
+        if (*hostname == '.') hostname++; /* advance past last dot we saw */
+    }
+    if (!(count = dict_find(das->dict, hostname, NULL))) {
+        count = calloc(1, sizeof(*count));
+        dict_insert(das->dict, strdup(hostname), count);
+    }
+    (*count)++;
+    return 0;
+}
+
+static int
+opserv_show_hostinfo(const char *key, void *data, void *extra)
+{
+    unsigned long *count = data;
+    struct discrim_and_source *das = extra;
+
+    send_message_type(4, das->source, opserv, "%s %lu", key, *count);
+    return !--das->disp_limit;
+}
+
+static MODCMD_FUNC(cmd_trace)
+{
+    struct discrim_and_source das;
+    discrim_search_func action;
+    unsigned int matches;
+    struct svccmd *subcmd;
+    char buf[MAXLEN];
+
+    sprintf(buf, "trace %s", argv[1]);
+    if (!(subcmd = opserv_get_command(buf))) {
+       reply("OSMSG_BAD_ACTION", argv[1]);
+        return 0;
+    }
+    if (!svccmd_can_invoke(user, cmd->parent->bot, subcmd, channel, SVCCMD_NOISY))
+        return 0;
+    if (!irccasecmp(argv[1], "print"))
+        action = trace_print_func;
+    else if (!irccasecmp(argv[1], "count"))
+        action = trace_count_func;
+    else if (!irccasecmp(argv[1], "domains"))
+        action = trace_domains_func;
+    else if (!irccasecmp(argv[1], "gline"))
+        action = trace_gline_func;
+    else if (!irccasecmp(argv[1], "kill"))
+        action = trace_kill_func;
+    else if (!irccasecmp(argv[1], "gag"))
+        action = trace_gag_func;
+    else {
+       reply("OSMSG_BAD_ACTION", argv[1]);
+       return 0;
+    }
+
+    if (user->handle_info->opserv_level < subcmd->min_opserv_level) {
+        reply("OSMSG_LEVEL_TOO_LOW");
+        return 0;
+    }
+
+    das.dict = NULL;
+    das.source = user;
+    das.discrim = opserv_discrim_create(user, argc-2, argv+2, 1);
+    if (!das.discrim)
+        return 0;
+
+    if (action == trace_print_func)
+       reply("OSMSG_USER_SEARCH_RESULTS");
+    else if (action == trace_count_func)
+       das.discrim->limit = INT_MAX;
+    else if ((action == trace_gline_func) && !das.discrim->duration)
+        das.discrim->duration = opserv_conf.block_gline_duration;
+    else if (action == trace_domains_func) {
+        das.dict = dict_new();
+        dict_set_free_data(das.dict, free);
+        dict_set_free_keys(das.dict, free);
+        das.disp_limit = das.discrim->limit;
+        das.discrim->limit = INT_MAX;
+    }
+    matches = opserv_discrim_search(das.discrim, action, &das);
+
+    if (action == trace_domains_func)
+        dict_foreach(das.dict, opserv_show_hostinfo, &das);
+
+    if (matches)
+       reply("MSG_MATCH_COUNT", matches);
+    else
+       reply("MSG_NO_MATCHES");
+
+    if (das.discrim->channel)
+        UnlockChannel(das.discrim->channel);
+    free(das.discrim->reason);
+    free(das.discrim);
+    dict_delete(das.dict);
+    return 1;
+}
+
+typedef void (*cdiscrim_search_func)(struct chanNode *match, void *data);
+
+typedef struct channel_discrim {
+    char *name, *topic;
+
+    unsigned int min_users, max_users;
+    time_t min_ts, max_ts;
+    unsigned int limit;
+} *cdiscrim_t;
+
+static cdiscrim_t opserv_cdiscrim_create(struct userNode *user, unsigned int argc, char *argv[]);
+static unsigned int opserv_cdiscrim_search(cdiscrim_t discrim, cdiscrim_search_func dsf, void *data);
+
+static time_t
+smart_parse_time(const char *str) {
+    /* If an interval-style string is given, treat as time before now.
+     * If it's all digits, treat directly as a Unix timestamp. */
+    return str[strspn(str, "0123456789")] ? (time_t)(now - ParseInterval(str)) : (time_t)atoi(str);
+}
+
+static cdiscrim_t
+opserv_cdiscrim_create(struct userNode *user, unsigned int argc, char *argv[])
+{
+    cdiscrim_t discrim;
+    unsigned int i;
+
+    discrim = calloc(1, sizeof(*discrim));
+    discrim->limit = 25;
+
+    for (i = 0; i < argc; i++) {
+       /* Assume all criteria require arguments. */
+       if (i == (argc - 1)) {
+           send_message(user, opserv, "MSG_MISSING_PARAMS", argv[i]);
+           return NULL;
+       }
+
+       if (!irccasecmp(argv[i], "name"))
+           discrim->name = argv[++i];
+       else if (!irccasecmp(argv[i], "topic"))
+           discrim->topic = argv[++i];
+       else if (!irccasecmp(argv[i], "users")) {
+           const char *cmp = argv[++i];
+            if (cmp[0] == '<') {
+                if (cmp[1] == '=')
+                    discrim->max_users = strtoul(cmp+2, NULL, 0);
+                else
+                    discrim->max_users = strtoul(cmp+1, NULL, 0) - 1;
+            } else if (cmp[0] == '=') {
+                discrim->min_users = discrim->max_users = strtoul(cmp+1, NULL, 0);
+            } else if (cmp[0] == '>') {
+                if (cmp[1] == '=')
+                    discrim->min_users = strtoul(cmp+2, NULL, 0);
+                else
+                    discrim->min_users = strtoul(cmp+1, NULL, 0) + 1;
+            } else {
+                discrim->min_users = strtoul(cmp+2, NULL, 0);
+            }
+       } else if (!irccasecmp(argv[i], "timestamp")) {
+           const char *cmp = argv[++i];
+            if (cmp[0] == '<') {
+                if (cmp[1] == '=')
+                    discrim->max_ts = smart_parse_time(cmp+2);
+                else
+                    discrim->max_ts = smart_parse_time(cmp+1)-1;
+            } else if (cmp[0] == '=') {
+                discrim->min_ts = discrim->max_ts = smart_parse_time(cmp+1);
+            } else if (cmp[0] == '>') {
+                if (cmp[1] == '=')
+                    discrim->min_ts = smart_parse_time(cmp+2);
+                else
+                    discrim->min_ts = smart_parse_time(cmp+1)+1;
+            } else {
+                discrim->min_ts = smart_parse_time(cmp);
+            }
+       } else if (!irccasecmp(argv[i], "limit")) {
+           discrim->limit = strtoul(argv[++i], NULL, 10);
+       } else {
+           send_message(user, opserv, "MSG_INVALID_CRITERIA", argv[i]);
+           goto fail;
+       }
+    }
+
+    if (discrim->name && !strcmp(discrim->name, "*"))
+       discrim->name = 0;
+    if (discrim->topic && !strcmp(discrim->topic, "*"))
+       discrim->topic = 0;
+
+    return discrim;
+  fail:
+    free(discrim);
+    return NULL;
+}
+
+static int
+cdiscrim_match(cdiscrim_t discrim, struct chanNode *chan)
+{
+    if ((discrim->name && !match_ircglob(chan->name, discrim->name)) ||
+        (discrim->topic && !match_ircglob(chan->topic, discrim->topic)) ||
+        (discrim->min_users && chan->members.used < discrim->min_users) ||
+        (discrim->max_users && chan->members.used > discrim->max_users) ||
+        (discrim->min_ts && chan->timestamp < discrim->min_ts) ||
+            (discrim->max_ts && chan->timestamp > discrim->max_ts)) {
+       return 0;
+    }
+    return 1;
+}
+
+static unsigned int opserv_cdiscrim_search(cdiscrim_t discrim, cdiscrim_search_func dsf, void *data)
+{
+    unsigned int count = 0;
+    dict_iterator_t it, next;
+
+    for (it = dict_first(channels); it && count < discrim->limit ; it = next) {
+       struct chanNode *chan = iter_data(it);
+
+       /* Hold on to the next channel in case we decide to
+          add actions that destructively modify the channel. */
+       next = iter_next(it);
+       if ((chan->members.used > 0) && cdiscrim_match(discrim, chan)) {
+           dsf(chan, data);
+           count++;
+       }
+    }
+
+    return count;
+}
+
+void channel_count(UNUSED_ARG(struct chanNode *channel), UNUSED_ARG(void *data))
+{
+}
+
+void channel_print(struct chanNode *channel, void *data)
+{
+    char modes[MAXLEN];
+    irc_make_chanmode(channel, modes);
+    send_message(data, opserv, "OSMSG_CSEARCH_CHANNEL_INFO", channel->name, channel->members.used, modes, channel->topic);
+}
+
+static MODCMD_FUNC(cmd_csearch)
+{
+    cdiscrim_t discrim;
+    unsigned int matches;
+    cdiscrim_search_func action;
+    struct svccmd *subcmd;
+    char buf[MAXLEN];
+
+    if (!irccasecmp(argv[1], "count"))
+       action = channel_count;
+    else if (!irccasecmp(argv[1], "print"))
+       action = channel_print;
+    else {
+       reply("OSMSG_BAD_ACTION", argv[1]);
+       return 0;
+    }
+
+    sprintf(buf, "%s %s", argv[0], argv[0]);
+    if ((subcmd = opserv_get_command(buf))
+        && !svccmd_can_invoke(user, cmd->parent->bot, subcmd, channel, SVCCMD_NOISY)) {
+        return 0;
+    }
+
+    discrim = opserv_cdiscrim_create(user, argc - 2, argv + 2);
+    if (!discrim)
+       return 0;
+
+    if (action == channel_print)
+       reply("OSMSG_CHANNEL_SEARCH_RESULTS");
+    else if (action == channel_count)
+       discrim->limit = INT_MAX;
+
+    matches = opserv_cdiscrim_search(discrim, action, user);
+
+    if (matches)
+       reply("MSG_MATCH_COUNT", matches);
+    else
+       reply("MSG_NO_MATCHES");
+
+    free(discrim);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_gsync)
+{
+    struct server *src;
+    if (argc > 1) {
+        src = GetServerH(argv[1]);
+        if (!src) {
+            reply("MSG_SERVER_UNKNOWN", argv[1]);
+            return 0;
+        }
+    } else {
+        src = self->uplink;
+    }
+    irc_stats(cmd->parent->bot, src, 'G');
+    reply("OSMSG_GSYNC_RUNNING", src->name);
+    return 1;
+}
+
+struct gline_extra {
+    struct userNode *user;
+    struct string_list *glines;
+};
+
+static void
+gtrace_print_func(struct gline *gline, void *extra)
+{
+    struct gline_extra *xtra = extra;
+    char *when_text, set_text[20];
+    strftime(set_text, sizeof(set_text), "%Y-%m-%d", localtime(&gline->issued));
+    when_text = asctime(localtime(&gline->expires));
+    when_text[strlen(when_text)-1] = 0; /* strip lame \n */
+    send_message(xtra->user, opserv, "OSMSG_GTRACE_FORMAT", gline->target, set_text, gline->issuer, when_text, gline->reason);
+}
+
+static void
+gtrace_count_func(UNUSED_ARG(struct gline *gline), UNUSED_ARG(void *extra))
+{
+}
+
+static void
+gtrace_ungline_func(struct gline *gline, void *extra)
+{
+    struct gline_extra *xtra = extra;
+    string_list_append(xtra->glines, strdup(gline->target));
+}
+
+static MODCMD_FUNC(cmd_gtrace)
+{
+    struct gline_discrim *discrim;
+    gline_search_func action;
+    unsigned int matches, nn;
+    struct gline_extra extra;
+    struct svccmd *subcmd;
+    char buf[MAXLEN];
+
+    if (!irccasecmp(argv[1], "print"))
+        action = gtrace_print_func;
+    else if (!irccasecmp(argv[1], "count"))
+        action = gtrace_count_func;
+    else if (!irccasecmp(argv[1], "ungline"))
+        action = gtrace_ungline_func;
+    else {
+        reply("OSMSG_BAD_ACTION", argv[1]);
+        return 0;
+    }
+    sprintf(buf, "%s %s", argv[0], argv[0]);
+    if ((subcmd = opserv_get_command(buf))
+        && !svccmd_can_invoke(user, cmd->parent->bot, subcmd, channel, SVCCMD_NOISY)) {
+        return 0;
+    }
+
+    discrim = gline_discrim_create(user, cmd->parent->bot, argc-2, argv+2);
+    if (!discrim)
+        return 0;
+
+    if (action == gtrace_print_func)
+        reply("OSMSG_GLINE_SEARCH_RESULTS");
+    else if (action == gtrace_count_func)
+        discrim->limit = INT_MAX;
+
+    extra.user = user;
+    extra.glines = alloc_string_list(4);
+    matches = gline_discrim_search(discrim, action, &extra);
+
+    if (action == gtrace_ungline_func)
+        for (nn=0; nn<extra.glines->used; nn++)
+            gline_remove(extra.glines->list[nn], 1);
+    free_string_list(extra.glines);
+
+    if (matches)
+        reply("MSG_MATCH_COUNT", matches);
+    else
+        reply("MSG_NO_MATCHES");
+    free(discrim->alt_target_mask);
+    free(discrim);
+    return 1;
+}
+
+static int
+alert_check_user(const char *key, void *data, void *extra)
+{
+    struct opserv_user_alert *alert = data;
+    struct userNode *user = extra;
+
+    if (!discrim_match(alert->discrim, user))
+        return 0;
+
+    if ((alert->reaction != REACT_NOTICE)
+        && IsOper(user)
+        && !alert->discrim->match_opers) {
+        return 0;
+    }
+
+    /* The user matches the alert criteria, so trigger the reaction. */
+    if (alert->discrim->option_log)
+        log_module(OS_LOG, LOG_INFO, "Alert %s triggered by user %s!%s@%s (%s).", key, user->nick, user->ident, user->hostname, alert->discrim->reason);
+
+    /* Return 1 to halt alert matching, such as when killing the user
+       that triggered the alert. */
+    switch (alert->reaction) {
+    case REACT_KILL:
+        DelUser(user, opserv, 1, alert->discrim->reason);
+        return 1;
+    case REACT_GLINE:
+        opserv_block(user, alert->owner, alert->discrim->reason, alert->discrim->duration);
+        return 1;
+    default:
+        log_module(OS_LOG, LOG_ERROR, "Invalid reaction type %d for alert %s.", alert->reaction, key);
+        /* fall through to REACT_NOTICE case */
+    case REACT_NOTICE:
+        opserv_alert("Alert $b%s$b triggered by user $b%s$b!%s@%s (%s).", key, user->nick, user->ident, user->hostname, alert->discrim->reason);
+        break;
+    }
+    return 0;
+}
+
+static void
+opserv_alert_check_nick(struct userNode *user, UNUSED_ARG(const char *old_nick))
+{
+    struct gag_entry *gag;
+    dict_foreach(opserv_nick_based_alerts, alert_check_user, user);
+    /* Gag them if appropriate (and only if). */
+    user->modes &= ~FLAGS_GAGGED;
+    for (gag = gagList; gag; gag = gag->next) {
+        if (user_matches_glob(user, gag->mask, 1)) {
+            gag_helper_func(user, NULL);
+            break;
+        }
+    }
+}
+
+static void
+opserv_staff_alert(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
+{
+    const char *type;
+
+    if (!opserv_conf.staff_auth_channel
+        || user->uplink->burst
+        || !user->handle_info)
+        return;
+    else if (user->handle_info->opserv_level)
+        type = "OPER";
+    else if (IsNetworkHelper(user))
+        type = "NETWORK HELPER";
+    else if (IsSupportHelper(user))
+        type = "SUPPORT HELPER";
+    else
+        return;
+
+    if (user->ip.s_addr)
+        send_channel_notice(opserv_conf.staff_auth_channel, opserv, IDENT_FORMAT" authed to %s account %s", IDENT_DATA(user), type, user->handle_info->handle);
+    else
+        send_channel_notice(opserv_conf.staff_auth_channel, opserv, "%s [%s@%s] authed to %s account %s", user->nick, user->ident, user->hostname, type, user->handle_info->handle);
+}
+
+static MODCMD_FUNC(cmd_log)
+{
+    struct logSearch *discrim;
+    unsigned int matches;
+    struct logReport report;
+
+    discrim = log_discrim_create(cmd->parent->bot, user, argc, argv);
+    if (!discrim)
+        return 0;
+
+    reply("OSMSG_LOG_SEARCH_RESULTS");
+    report.reporter = opserv;
+    report.user = user;
+    matches = log_entry_search(discrim, log_report_entry, &report);
+
+    if (matches)
+       reply("MSG_MATCH_COUNT", matches);
+    else
+       reply("MSG_NO_MATCHES");
+
+    free(discrim);
+    return 1;
+}
+
+static int
+gag_helper_func(struct userNode *match, UNUSED_ARG(void *extra))
+{
+    if (IsOper(match) || IsLocal(match))
+        return 0;
+    match->modes |= FLAGS_GAGGED;
+    return 0;
+}
+
+static MODCMD_FUNC(cmd_gag)
+{
+    struct gag_entry *gag;
+    unsigned int gagged;
+    unsigned long duration;
+    char *reason;
+
+    reason = unsplit_string(argv + 3, argc - 3, NULL);
+
+    if (!is_ircmask(argv[1])) {
+       reply("OSMSG_INVALID_IRCMASK", argv[1]);
+        return 0;
+    }
+
+    for (gag = gagList; gag; gag = gag->next)
+       if (match_ircglobs(gag->mask, argv[1]))
+            break;
+
+    if (gag) {
+       reply("OSMSG_REDUNDANT_GAG", argv[1]);
+       return 0;
+    }
+
+    duration = ParseInterval(argv[2]);
+    gagged = gag_create(argv[1], user->handle_info->handle, reason, (duration?now+duration:0));
+
+    if (gagged)
+       reply("OSMSG_GAG_APPLIED", argv[1], gagged);
+    else
+       reply("OSMSG_GAG_ADDED", argv[1]);
+    return 1;
+}
+
+static int
+ungag_helper_func(struct userNode *match, UNUSED_ARG(void *extra))
+{
+    match->modes &= ~FLAGS_GAGGED;
+    return 0;
+}
+
+static MODCMD_FUNC(cmd_ungag)
+{
+    struct gag_entry *gag;
+    unsigned int ungagged;
+
+    for (gag = gagList; gag; gag = gag->next)
+       if (!strcmp(gag->mask, argv[1]))
+            break;
+
+    if (!gag) {
+       reply("OSMSG_GAG_NOT_FOUND", argv[1]);
+       return 0;
+    }
+
+    timeq_del(gag->expires, gag_expire, gag, 0);
+    ungagged = gag_free(gag);
+
+    if (ungagged)
+       reply("OSMSG_UNGAG_APPLIED", argv[1], ungagged);
+    else
+       reply("OSMSG_UNGAG_ADDED", argv[1]);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_addalert)
+{
+    opserv_alert_reaction reaction;
+    struct svccmd *subcmd;
+    const char *name;
+    char buf[MAXLEN];
+
+    name = argv[1];
+    sprintf(buf, "addalert %s", argv[2]);
+    if (!(subcmd = opserv_get_command(buf))) {
+       reply("OSMSG_UNKNOWN_REACTION", argv[2]);
+       return 0;
+    }
+    if (!irccasecmp(argv[2], "notice"))
+        reaction = REACT_NOTICE;
+    else if (!irccasecmp(argv[2], "kill"))
+        reaction = REACT_KILL;
+    else if (!irccasecmp(argv[2], "gline"))
+        reaction = REACT_GLINE;
+    else {
+       reply("OSMSG_UNKNOWN_REACTION", argv[2]);
+       return 0;
+    }
+    if (!svccmd_can_invoke(user, cmd->parent->bot, subcmd, channel, SVCCMD_NOISY)
+        || !opserv_add_user_alert(user, name, reaction, unsplit_string(argv + 3, argc - 3, NULL)))
+        return 0;
+    reply("OSMSG_ADDED_ALERT", name);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_delalert)
+{
+    unsigned int i;
+    for (i=1; i<argc; i++) {
+        dict_remove(opserv_nick_based_alerts, argv[i]);
+        dict_remove(opserv_channel_alerts, argv[i]);
+       if (dict_remove(opserv_user_alerts, argv[i]))
+           reply("OSMSG_REMOVED_ALERT", argv[i]);
+        else
+           reply("OSMSG_NO_SUCH_ALERT", argv[i]);
+    }
+    return 1;
+}
+
+static void
+opserv_conf_read(void)
+{
+    struct record_data *rd;
+    dict_t conf_node, child;
+    const char *str, *str2;
+    struct policer_params *pp;
+    dict_iterator_t it;
+
+    rd = conf_get_node(OPSERV_CONF_NAME);
+    if (!rd || rd->type != RECDB_OBJECT) {
+       log_module(OS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", OPSERV_CONF_NAME);
+       return;
+    }
+    conf_node = rd->d.object;
+    str = database_get_data(conf_node, KEY_DEBUG_CHANNEL, RECDB_QSTRING);
+    if (str) {
+        str2 = database_get_data(conf_node, KEY_DEBUG_CHANNEL_MODES, RECDB_QSTRING);
+        if (!str2)
+            str2 = "+tinms";
+       opserv_conf.debug_channel = AddChannel(str, now, str2, NULL);
+        AddChannelUser(opserv, opserv_conf.debug_channel)->modes |= MODE_CHANOP;
+    } else {
+       opserv_conf.debug_channel = NULL;
+    }
+    str = database_get_data(conf_node, KEY_ALERT_CHANNEL, RECDB_QSTRING);
+    if (str) {
+        str2 = database_get_data(conf_node, KEY_ALERT_CHANNEL_MODES, RECDB_QSTRING);
+        if (!str2)
+            str2 = "+tns";
+       opserv_conf.alert_channel = AddChannel(str, now, str2, NULL);
+        AddChannelUser(opserv, opserv_conf.alert_channel)->modes |= MODE_CHANOP;
+    } else {
+       opserv_conf.alert_channel = NULL;
+    }
+    str = database_get_data(conf_node, KEY_STAFF_AUTH_CHANNEL, RECDB_QSTRING);
+    if (str) {
+        str2 = database_get_data(conf_node, KEY_STAFF_AUTH_CHANNEL_MODES, RECDB_QSTRING);
+        if (!str2)
+            str2 = "+timns";
+        opserv_conf.staff_auth_channel = AddChannel(str, now, str2, NULL);
+        AddChannelUser(opserv, opserv_conf.staff_auth_channel)->modes |= MODE_CHANOP;
+    } else {
+        opserv_conf.staff_auth_channel = NULL;
+    }
+    str = database_get_data(conf_node, KEY_UNTRUSTED_MAX, RECDB_QSTRING);
+    opserv_conf.untrusted_max = str ? strtoul(str, NULL, 0) : 5;
+    str = database_get_data(conf_node, KEY_PURGE_LOCK_DELAY, RECDB_QSTRING);
+    opserv_conf.purge_lock_delay = str ? strtoul(str, NULL, 0) : 60;
+    str = database_get_data(conf_node, KEY_JOIN_FLOOD_MODERATE, RECDB_QSTRING);
+    opserv_conf.join_flood_moderate = str ? strtoul(str, NULL, 0) : 1;
+    str = database_get_data(conf_node, KEY_JOIN_FLOOD_MODERATE_THRESH, RECDB_QSTRING);
+    opserv_conf.join_flood_moderate_threshold = str ? strtoul(str, NULL, 0) : 50;
+    str = database_get_data(conf_node, KEY_NICK, RECDB_QSTRING);
+    if (str)
+        NickChange(opserv, str, 0);
+    str = database_get_data(conf_node, KEY_CLONE_GLINE_DURATION, RECDB_QSTRING);
+    opserv_conf.clone_gline_duration = str ? ParseInterval(str) : 3600;
+    str = database_get_data(conf_node, KEY_BLOCK_GLINE_DURATION, RECDB_QSTRING);
+    opserv_conf.block_gline_duration = str ? ParseInterval(str) : 3600;
+
+    if (!opserv_conf.join_policer_params)
+        opserv_conf.join_policer_params = policer_params_new();
+    policer_params_set(opserv_conf.join_policer_params, "size", "20");
+    policer_params_set(opserv_conf.join_policer_params, "drain-rate", "1");
+    if ((child = database_get_data(conf_node, KEY_JOIN_POLICER, RECDB_OBJECT)))
+       dict_foreach(child, set_policer_param, opserv_conf.join_policer_params);
+
+    for (it = dict_first(channels); it; it = iter_next(it)) {
+        struct chanNode *cNode = iter_data(it);
+        cNode->join_policer.params = opserv_conf.join_policer_params;
+    }
+
+    if (opserv_conf.new_user_policer.params)
+        pp = opserv_conf.new_user_policer.params;
+    else
+        pp = opserv_conf.new_user_policer.params = policer_params_new();
+    policer_params_set(pp, "size", "200");
+    policer_params_set(pp, "drain-rate", "3");
+    if ((child = database_get_data(conf_node, KEY_NEW_USER_POLICER, RECDB_OBJECT)))
+       dict_foreach(child, set_policer_param, pp);
+}
+
+static void
+opserv_db_init(void) {
+    /* set up opserv_trusted_hosts dict */
+    dict_delete(opserv_trusted_hosts);
+    opserv_trusted_hosts = dict_new();
+    dict_set_free_data(opserv_trusted_hosts, free_trusted_host);
+    /* set up opserv_chan_warn dict */
+    dict_delete(opserv_chan_warn);
+    opserv_chan_warn = dict_new();
+    dict_set_free_keys(opserv_chan_warn, free);
+    dict_set_free_data(opserv_chan_warn, free);
+    /* set up opserv_user_alerts */
+    dict_delete(opserv_channel_alerts);
+    opserv_channel_alerts = dict_new();
+    dict_delete(opserv_nick_based_alerts);
+    opserv_nick_based_alerts = dict_new();
+    dict_delete(opserv_user_alerts);
+    opserv_user_alerts = dict_new();
+    dict_set_free_keys(opserv_user_alerts, free);
+    dict_set_free_data(opserv_user_alerts, opserv_free_user_alert);
+    /* set up opserv_bad_words */
+    free_string_list(opserv_bad_words);
+    opserv_bad_words = alloc_string_list(4);
+    /* and opserv_exempt_channels */
+    dict_delete(opserv_exempt_channels);
+    opserv_exempt_channels = dict_new();
+    dict_set_free_keys(opserv_exempt_channels, free);
+}
+
+static void
+opserv_db_cleanup(void)
+{
+    unsigned int nn;
+
+    dict_delete(opserv_chan_warn);
+    dict_delete(opserv_reserved_nick_dict);
+    free_string_list(opserv_bad_words);
+    dict_delete(opserv_exempt_channels);
+    dict_delete(opserv_trusted_hosts);
+    unreg_del_user_func(opserv_user_cleanup);
+    dict_delete(opserv_hostinfo_dict);
+    dict_delete(opserv_nick_based_alerts);
+    dict_delete(opserv_channel_alerts);
+    dict_delete(opserv_user_alerts);
+    for (nn=0; nn<ArrayLength(level_strings); ++nn)
+        free(level_strings[nn]);
+    while (gagList)
+        gag_free(gagList);
+    policer_params_delete(opserv_conf.join_policer_params);
+    policer_params_delete(opserv_conf.new_user_policer.params);
+}
+
+void
+init_opserv(const char *nick)
+{
+    opserv = AddService(nick, "Oper Services");
+    OS_LOG = log_register_type("OpServ", "file:opserv.log");
+    conf_register_reload(opserv_conf_read);
+
+    memset(level_strings, 0, sizeof(level_strings));
+    opserv_module = module_register("OpServ", OS_LOG, "opserv.help", opserv_help_expand);
+    opserv_define_func("ACCESS", cmd_access, 0, 0, 0);
+    opserv_define_func("ADDALERT", cmd_addalert, 800, 0, 4);
+    opserv_define_func("ADDALERT NOTICE", NULL, 0, 0, 0);
+    opserv_define_func("ADDALERT GLINE", NULL, 900, 0, 0);
+    opserv_define_func("ADDALERT KILL", NULL, 900, 0, 0);
+    opserv_define_func("ADDBAD", cmd_addbad, 800, 0, 2);
+    opserv_define_func("ADDEXEMPT", cmd_addexempt, 800, 0, 2);
+    opserv_define_func("ADDTRUST", cmd_addtrust, 800, 0, 5);
+    opserv_define_func("BAN", cmd_ban, 100, 2, 2);
+    opserv_define_func("BLOCK", cmd_block, 100, 0, 2);
+    opserv_define_func("CHANINFO", cmd_chaninfo, 0, 3, 0);
+    opserv_define_func("CLEARBANS", cmd_clearbans, 300, 2, 0);
+    opserv_define_func("CLEARMODES", cmd_clearmodes, 400, 2, 0);
+    opserv_define_func("CLONE", cmd_clone, 999, 0, 3);
+    opserv_define_func("COLLIDE", cmd_collide, 800, 0, 5);
+    opserv_define_func("CSEARCH", cmd_csearch, 100, 0, 3);
+    opserv_define_func("CSEARCH COUNT", cmd_csearch, 0, 0, 0);
+    opserv_define_func("CSEARCH PRINT", cmd_csearch, 0, 0, 0);
+    opserv_define_func("DELALERT", cmd_delalert, 800, 0, 2);
+    opserv_define_func("DELBAD", cmd_delbad, 800, 0, 2);
+    opserv_define_func("DELEXEMPT", cmd_delexempt, 800, 0, 2);
+    opserv_define_func("DELTRUST", cmd_deltrust, 800, 0, 2);
+    opserv_define_func("DEOP", cmd_deop, 100, 2, 2);
+    opserv_define_func("DEOPALL", cmd_deopall, 400, 2, 0);
+    opserv_define_func("DEVOICEALL", cmd_devoiceall, 300, 2, 0);
+    opserv_define_func("DIE", cmd_die, 900, 0, 2);
+    opserv_define_func("DUMP", cmd_dump, 999, 0, 2);
+    opserv_define_func("EDITTRUST", cmd_edittrust, 800, 0, 5);
+    opserv_define_func("GAG", cmd_gag, 600, 0, 4);
+    opserv_define_func("GLINE", cmd_gline, 600, 0, 4);
+    opserv_define_func("GSYNC", cmd_gsync, 600, 0, 0);
+    opserv_define_func("GTRACE", cmd_gtrace, 100, 0, 3);
+    opserv_define_func("GTRACE COUNT", NULL, 0, 0, 0);
+    opserv_define_func("GTRACE PRINT", NULL, 0, 0, 0);
+    opserv_define_func("INVITE", cmd_invite, 100, 2, 0);
+    opserv_define_func("INVITEME", cmd_inviteme, 100, 0, 0);
+    opserv_define_func("JOIN", cmd_join, 601, 0, 2);
+    opserv_define_func("JUMP", cmd_jump, 900, 0, 2);
+    opserv_define_func("JUPE", cmd_jupe, 900, 0, 4);
+    opserv_define_func("KICK", cmd_kick, 100, 2, 2);
+    opserv_define_func("KICKALL", cmd_kickall, 400, 2, 0);
+    opserv_define_func("KICKBAN", cmd_kickban, 100, 2, 2);
+    opserv_define_func("KICKBANALL", cmd_kickbanall, 450, 2, 0);
+    opserv_define_func("LOG", cmd_log, 900, 0, 2);
+    opserv_define_func("MODE", cmd_mode, 100, 2, 2);
+    opserv_define_func("OP", cmd_op, 100, 2, 2);
+    opserv_define_func("OPALL", cmd_opall, 400, 2, 0);
+    opserv_define_func("PART", cmd_part, 601, 0, 2);
+    opserv_define_func("QUERY", cmd_query, 0, 0, 0);
+    opserv_define_func("RAW", cmd_raw, 999, 0, 2);
+    opserv_define_func("RECONNECT", cmd_reconnect, 900, 0, 0);
+    opserv_define_func("REFRESHG", cmd_refreshg, 600, 0, 0);
+    opserv_define_func("REHASH", cmd_rehash, 900, 0, 0);
+    opserv_define_func("REOPEN", cmd_reopen, 900, 0, 0);
+    opserv_define_func("RESERVE", cmd_reserve, 800, 0, 5);
+    opserv_define_func("RESTART", cmd_restart, 900, 0, 2);
+    opserv_define_func("SET", cmd_set, 900, 0, 3);
+    opserv_define_func("SETTIME", cmd_settime, 901, 0, 0);
+    opserv_define_func("STATS ALERTS", cmd_stats_alerts, 0, 0, 0);
+    opserv_define_func("STATS BAD", cmd_stats_bad, 0, 0, 0);
+    opserv_define_func("STATS GAGS", cmd_stats_gags, 0, 0, 0);
+    opserv_define_func("STATS GLINES", cmd_stats_glines, 0, 0, 0);
+    opserv_define_func("STATS LINKS", cmd_stats_links, 0, 0, 0);
+    opserv_define_func("STATS MAX", cmd_stats_max, 0, 0, 0);
+    opserv_define_func("STATS NETWORK", cmd_stats_network, 0, 0, 0);
+    opserv_define_func("STATS NETWORK2", cmd_stats_network2, 0, 0, 0);
+    opserv_define_func("STATS RESERVED", cmd_stats_reserved, 0, 0, 0);
+    opserv_define_func("STATS TIMEQ", cmd_stats_timeq, 0, 0, 0);
+    opserv_define_func("STATS TRUSTED", cmd_stats_trusted, 0, 0, 0);
+    opserv_define_func("STATS UPLINK", cmd_stats_uplink, 0, 0, 0);
+    opserv_define_func("STATS UPTIME", cmd_stats_uptime, 0, 0, 0);
+    opserv_define_func("STATS WARN", cmd_stats_warn, 0, 0, 0);
+    opserv_define_func("TRACE", cmd_trace, 100, 0, 3);
+    opserv_define_func("TRACE PRINT", NULL, 0, 0, 0);
+    opserv_define_func("TRACE COUNT", NULL, 0, 0, 0);
+    opserv_define_func("TRACE DOMAINS", NULL, 0, 0, 0);
+    opserv_define_func("TRACE GLINE", NULL, 600, 0, 0);
+    opserv_define_func("TRACE GAG", NULL, 600, 0, 0);
+    opserv_define_func("TRACE KILL", NULL, 600, 0, 0);
+    opserv_define_func("UNBAN", cmd_unban, 100, 2, 2);
+    opserv_define_func("UNGAG", cmd_ungag, 600, 0, 2);
+    opserv_define_func("UNGLINE", cmd_ungline, 600, 0, 2);
+    modcmd_register(opserv_module, "GTRACE UNGLINE", NULL, 0, 0, "template", "ungline", NULL);
+    opserv_define_func("UNJUPE", cmd_unjupe, 900, 0, 2);
+    opserv_define_func("UNRESERVE", cmd_unreserve, 800, 0, 2);
+    opserv_define_func("UNWARN", cmd_unwarn, 800, 0, 0);
+    opserv_define_func("VOICEALL", cmd_voiceall, 300, 2, 0);
+    opserv_define_func("WARN", cmd_warn, 800, 0, 2);
+    opserv_define_func("WHOIS", cmd_whois, 0, 0, 2);
+
+    opserv_reserved_nick_dict = dict_new();
+    opserv_hostinfo_dict = dict_new();
+    dict_set_free_keys(opserv_hostinfo_dict, free);
+    dict_set_free_data(opserv_hostinfo_dict, opserv_free_hostinfo);
+
+    reg_new_user_func(opserv_new_user_check);
+    reg_nick_change_func(opserv_alert_check_nick);
+    reg_del_user_func(opserv_user_cleanup);
+    reg_new_channel_func(opserv_channel_check);
+    reg_join_func(opserv_join_check);
+    reg_auth_func(opserv_staff_alert);
+
+    opserv_db_init();
+    saxdb_register("OpServ", opserv_saxdb_read, opserv_saxdb_write);
+    opserv_service = service_find(opserv->nick);
+
+    reg_exit_func(opserv_db_cleanup);
+    message_register_table(msgtab);
+}
diff --git a/src/opserv.h b/src/opserv.h
new file mode 100644 (file)
index 0000000..5109d17
--- /dev/null
@@ -0,0 +1,26 @@
+/* opserv.h - IRC Operator assistant service
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef _opserv_h
+#define _opserv_h
+
+void init_opserv(const char *nick);
+unsigned int gag_create(const char *mask, const char *owner, const char *reason, time_t expires);
+int opserv_bad_channel(const char *name);
+
+#endif
diff --git a/src/opserv.help b/src/opserv.help
new file mode 100644 (file)
index 0000000..f5980e7
--- /dev/null
@@ -0,0 +1,422 @@
+"<INDEX>" ("$b$O Help$b",
+        "$b$O$b is the IRC operator service, giving authorized users extra information and control over the network.",
+        "$uNetwork-related command categories:$u",
+        "  USER     - User information, control and searching",
+        "  CHANNEL  - Channel information and control",
+        "  GLINES   - G-line control and searching",
+        "$usrvx-internal command categories:$u",
+        "  CLONES   - Control for fake servers and users",
+        "  LOGS     - Log control and searching",
+        "  PROXY    - Proxy checking controls",
+        "  RUNNING  - Control srvx's run-time state",
+        "  SERVICES - Control of the other services in srvx",
+        "  OTHER    - Miscellaneous commands",
+        "$b/msg $O help <category>$b for a list of commands in each category, or $b/msg $O help <command>$b for syntax and usage for a command.");
+"COMMANDS" "${index}";
+
+"USER" ("$bUSER COMMANDS$b",
+        "Search for, gives information on, and gives some control over IRC users.",
+        "Alerts are a way to automatically detect (and optionally G-line) certain users.  For example, a \"gline info sub7server\" alert would G-line any users using \"sub7server\" as their full name (user info field).",
+        "  ACCESS   [${level/access}]",
+        "  ADDALERT [${level/addalert}]",
+        "  ADDTRUST [${level/addtrust}]",
+        "  DELALERT [${level/delalert}]",
+        "  DELTRUST [${level/deltrust}]",
+        "  INVITEME [${level/inviteme}]",
+        "  TRACE    [${level/trace}]",
+        "  WHOIS    [${level/whois}]");
+"ACCESS" ("/msg $O ACCESS [nick|*account] [new-level]",
+        "Displays the $O access level for the specified user or account.  With no arguments, displays your own access level.  With two arguments, sets the target's $O access level to the second argument (assuming you have sufficient access to do so).");
+"ADDALERT" ("/msg $O ADDALERT <name> <reaction> <criteria>",
+        "Adds the specified alert to the $b$O$b alert list.",
+        "$uSee Also:$u delalert, alert reaction, trace criteria");
+"ADDTRUST" ("/msg $O ADDTRUST <ip> <number> <duration> <reason>",
+        "Allows the specified IP address to have the specified amount of clones before being removed (rather than the usual limit).",
+        "You may use 0 as the duration if you do not wish the trust to ever expire.",
+        "$uSee Also:$u deltrust, stats trusted");
+"ALERT REACTION" ("$bALERT REACTION$b",
+        "Valid alert actions (to be taken when an alert is hit) are:",
+        "$bNOTICE$b:       Send a notice to the $b$O$b alert channel",
+        "$bKILL$b:         Disconnect the user",
+        "$bGLINE$b:        Gline the user that tripped the alert",
+        "$uSee Also:$u addalert, delalert");
+"DELALERT" ("/msg $O DELALERT <alert> [alert]...",
+        "Remove the specified alerts from the $b$O$b alert list.",
+        "$uSee Also:$u addalert, stats");
+"DELTRUST" ("/msg $O DELTRUST <ip>",
+        "Deletes a trusted IP from $b$O's$b trusted hosts list. A trusted IP address is exempted from normal client limits. A list of currently trusted IPs is displayed by $bstats trusted$b.",
+        "$uSee Also:$u addtrust, stats");
+"INVITEME" ("/msg $O INVITEME [nick]",
+        "Invites the specified user (if omitted, you) to $O's debug channel.",
+        "This is currently pointless, since no output is sent to the debug channel.");
+"TRACE" ("/msg $O TRACE <action> <criteria> <value> [<criteria> <value>]...",
+        "Searches through the current users for those matching the specified criteria, and applies the specified action to them. A list of actions can be found in $bhelp trace action$b and a list of criteria in $bhelp trace criteria$b.",
+        "$uSee Also:$u trace action, trace criteria");
+"TRACE ACTION" ("$bTRACE ACTION$b",
+        "Options for action in $btrace$b are:",
+        "$bPRINT$b:   Display the hostmask to you.",
+        "$bCOUNT$b:   Count all matching users.",
+        "$bKILL$b:    Kill matching clients.",
+        "$bGLINE$b:   Issue a gline for the client's host (by default, 1 hour long).",
+        "$bGAG$b:     Gag all matching users (by default, does not expire).",
+        "$bDOMAINS$b: Display counts of users in each domain (length specified by DEPTH criteria.", 
+        "Note: By default, IRC operators are not affected by the KILL, GLINE or GAG actions.  You can override this by specifying the $bABUSE OPERS$b criteria for a trace.  Even if you do specify $bABUSE OPERS$b, it will not affect opers at your access level or above.");
+"TRACE CRITERIA" ("$bTRACE CRITERIA$b",
+        "Criteria and values for $btrace$b (a search with $btrace$b must match all specified items):",
+        "$bMASK$b nick!user@host    Specifies a full hostmask to search for.",
+        "$bNICK$b nick              Specifies a nick to search for.",
+        "$bIDENT$b ident            Specifies an ident to search for.",
+        "$bHOST$b host              Specifies a hostname to search for.",
+        "$bINFO$b infoline          Specifies a user's info to search for.",
+        "$bSERVER$b server          Specifies a server to search for.",
+        "$bIP$b 127.0.0.1           Specifies an IP to search for (independent of hostname).",
+        "$bACCOUNT$b account        Specifies an account to search for.",
+        "$bAUTHED$b yes/no          Specifies if matching users must be authenticated with $N or not",
+        "$bCHANNEL$b #target        Specifies a channel the client must be in.",
+        "$bNUMCHANNELS$b 5          Specifies a number of channels the client must be in.",
+        "$bLIMIT$b 50               Limits the number of responses to a certain number.",
+        "$bNICKAGE$b cmp            Client has had nick this long (<Nu, <=Nu, =Nu, >=Nu or >Nu)",
+        "$bACCESS$b cmp             Access constraints (<nnn, <=nnn, =nnn, >=nnn or >nnn)",
+        "$bREASON$b reason          Reason for kill or gline (must be listed last).",
+        "$bDEPTH$b depth            How many domain-name parts to use for $bDOMAINS$b action.",
+        "$bDURATION$b duration      How long to apply a G-line or gag.",
+        "$bCLONES$b min             Ignore clients from hosts with fewer than this many connections.",
+        "$bINFO_SPACE$b yes/no      Clients match only if their info starts with a space (' ') character.",
+        "$bABUSE OPERS$b            Force adverse actions to affect opers as well.",
+        "$bLOG$b                    Record matching users in $O's log file (in addition to acting).",
+        "Additionally, the $bCHANNEL$b target may be prefixed with @ to select channel operators, + to select voiced users (will not select chanops unless @ is also used), or - to select non-voiced non-chanop users.  For example, CHANNEL #foo will select all users in #foo; CHANNEL +#foo will select only users voiced in #foo; CHANNEL @+#foo will select ops and voiced users in #foo; etc.");
+"WHOIS" ("/msg $O WHOIS <nick>",
+        "Displays detailed information for the named user.");
+
+"CHANNEL" ("$bCHANNEL SUB-CATEGORIES$b",
+        "Channel information, control, and searching.",
+        "  CHANNEL REACTIONS   - Automated reactions based on channel names",
+        "  CHANNEL CONTROL     - Channel information and control",
+        "There are also some miscellaneous channel-related commands:",
+        "  CHANINFO [${level/chaninfo}]",
+        "  CSEARCH  [${level/csearch}]",
+        "  JOIN     [${level/join}]",
+        "  PART     [${level/part}]");
+"CHANINFO" ("/msg $O CHANINFO <#channel> [users]",
+        "Displays very detailed information on the specified channel. If the channel is omitted, then $bchaninfo$b will be done on the channel where the command was given.  You must give a second parameter ($busers$b) to list users in the channel.",
+        "$uSee Also:$u whois");
+"CSEARCH" ("/msg $O CSEARCH <action> <criteria> <value> [<criteria> <value>]...",
+        "Searches through the network's channels for those matching the specified criteria, and applies the specified action to them. A list of actions can be found under $bhelp csearch action$b and a list of criteria in $bhelp csearch criteria$b.",
+        "$uSee Also:$u csearch action, csearch criteria");
+"CSEARCH ACTION" ("$bCSEARCH ACTION$b",
+        "Options for action in $bcsearch$b are:",
+        "$bPRINT$b:   Display the channel and user count.",
+        "$bCOUNT$b:   Count all matching channels.");
+"CSEARCH CRITERIA" ("$bCSEARCH CRITERIA$b",
+        "Criteria and values for $bcsearch$b (a search with $bcsearch$b must match all specified items):",
+        "$bNAME$b name              Specifies a name to search for.",
+        "$bTOPIC$b topic            Specifies a topic to search for.",
+       "$bUSERS$b cmp              User count constraint (<Nu, <=Nu, =Nu, >=Nu or >Nu)",
+        "$bTIMESTAMP$b cmp          Timestamp constraint (<Nu, <=Nu, =Nu, >=Nu or >Nu; supports interval notation)",
+        "$bLIMIT$b 50               Limits the number of responses to a certain number.");
+"JOIN" ("/msg $O JOIN <#channel> ",
+        "Makes $b$O$b join the specified channel.",
+        "$uSee Also:$u part");
+"PART" ("/msg $O PART <#channel> ",
+        "Makes $b$O$b leave the specified channel.",
+        "$uSee Also:$u join");
+
+"CHANNEL REACTIONS" ("$bCHANNEL REACTION COMMANDS$b",
+        "\"Bad-word\" channels are for assisting in enforcement of network policies (such as disallowing \"warez\" channels).  \"Warning\" channels are for channels which may be abused when they are present, but should be allowed otherwise.",
+        "  ADDBAD    [${level/addbad}]",
+        "  ADDEXEMPT [${level/addexempt}]",
+        "  DELBAD    [${level/delbad}]",
+        "  DELEXEMPT [${level/delexempt}]",
+        "  UNWARN    [${level/unwarn}]",
+        "  WARN      [${level/warn}]");
+"ADDBAD" ("/msg $O ADDBAD <word>",
+        "Adds a bad word to $b$O's$b bad word list. Bad words make any channel that has a bad word anywhere in a channel's name illegal. A list of current bad words can be displayed by $bstats bad$b.",
+        "$uSee Also:$u addexempt, delbad, stats");
+"ADDEXEMPT" ("/msg $O ADDEXEMPT <#channel>",
+        "Adds a channel to $O's \"exempt\" list.  These channels (and only these channels) are never considered to contain prohibited words.  Note that you $bmust$b specify the whole channel name, and may not use wildcards.",
+        "For example, if you have added $ufree$u to the bad-word list, you could add $u#FreeBSD$u to the exempt list, and anyone could join #FreeBSD.  Users joining #FreeBSDISOz would be kickbanned by $O.",
+        "The current exempt list is displayed with the current bad-words in $bstats bad$b.",
+        "$uSee Also:$u addbad, delexempt, stats");
+"DELBAD" ("/msg $O DELBAD <keyword>",
+        "Deletes a bad word from $b$O's$b bad word list. Bad words make any channel that has a bad word anywhere in a channel's name illegal. A list of current bad words can be displayed by $bstats bad$b.",
+        "$uSee Also:$u addbad, delexempt, stats");
+"DELEXEMPT" ("/msg $O DELEXEMPT <#channel>",
+        "Removes a channel from $O's bad-word-exempt channel list.",
+        "$uSee Also:$u addexempt, delbad, stats");
+"UNWARN"  ("/msg $O UNWARN <#channel>",
+        "Deletes the activity warning for a channel.",
+       "$uSee Also:$u warn, stats warn");
+"WARN"  ("/msg $O WARN <#channel> [reason]",
+        "Adds an activity warning for the channel.",
+       "$uSee Also:$u unwarn, stats warn");
+
+"CHANNEL CONTROL" ("$bCHANNEL CONTROL COMMANDS$b",
+        "These are analogous to the same (or similar) $C commands, but can be used on any channel.",
+        "  BAN        [${level/ban}]",
+        "  CLEARBANS  [${level/clearbans}]",
+        "  CLEARMODES [${level/clearmodes}]",
+        "  DEOP       [${level/deop}]",
+        "  DEOPALL    [${level/deopall}]",
+        "  DEVOICEALL [${level/devoiceall}]",
+        "  KICK       [${level/kick}]",
+        "  KICKALL    [${level/kickall}]",
+        "  KICKBAN    [${level/kickban}]",
+        "  KICKBANALL [${level/kickbanall}]",
+        "  MODE       [${level/mode}]",
+        "  OP         [${level/op}]",
+        "  OPALL      [${level/opall}]",
+        "  UNBAN      [${level/unban}]",
+        "  VOICEALL   [${level/voiceall}]");
+"BAN" ("/msg $O BAN <#channel> <nick|hostmask>",
+        "Bans the specified hostmask from the specified channel.",
+        "If a nick is used instead of hostmask, the hostmask is generated based on the nickname.",
+        "If the channel is omitted, the $bban$b will be done in the channel where the command was given.",
+        "$uSee Also:$u kickban, kickbanall, unban");
+"UNBAN" ("/msg $O UNBAN <#channel> <hostmask>",
+       "Unbans the specified hostmask from the specified channel.",
+       "If the channel is omitted, the $bunban$b will be done in the channel where the command was given.",
+       "$uSee Also:$u kickban, kickbanall, ban");
+"CLEARBANS" ("/msg $O CLEARBANS <#channel> ",
+        "Clears all bans in the specified channel.",
+        "If the channel is omitted, then $bclearbans$b will be done in the channel where the command was given.",
+        "$uSee Also:$u ban, unban");
+"CLEARMODES" ("/msg $O CLEARMODES <#channel> ",
+        "Clears the specified channel of all modes.",
+        "If the channel is omitted, then $bclearmodes$b will be done in the channel where the command was given.",
+        "$uSee Also:$u mode");
+"DEOP" ("/msg $O DEOP <#channel> <nick> [nick]...",
+        "Deops the specified user from the specified channel.",
+        "If the channel is omitted, then $bdeop$b will be done in the channel where the command was given.",
+        "$uSee Also:$u deopall, devoiceall, op, opall");
+"DEOPALL" ("/msg $O DEOPALL <#channel>",
+        "Deops all members of the specified channel.",
+        "If the channel is omitted, then $bdeopall$b will be done in the channel where the command was given.",
+        "$uSee Also:$u deop, devoiceall, op, opall");
+"DEVOICEALL" ("/msg $O DEVOICEALL <#channel>",
+        "Devoice all members of the specified channel who do not have channel ops.",
+        "If the channel is omitted, then $bdevoiceall$b will be done in the channel where the command was given.",
+        "$uSee Also:$u deop, deopall, op, opall");
+"KICK" ("/msg $O KICK <#channel> <nick> [reason]",
+        "Kicks the specified user from the specified channel.",
+        "If the channel is omitted, then $bkick$b will be done in the channel where the command was given.",
+        "$uSee Also:$u ban, kickall, kickban, kickbanall");
+"KICKALL" ("/msg $O KICKALL <#channel> [reason]",
+        "Kicks all users in the specified channel except for the user issuing the command.",
+        "If the channel is omitted, then $bkickall$b will be done in the channel where the command was given.",
+        "$uSee Also:$u ban, kick, kickbanall");
+"KICKBAN" ("/msg $O KICKBAN <#channel> <nick> [reason]",
+        "Kicks and bans the specified user. $b$O$b determines the hostmask to ban from the nick specified.",
+        "If the channel is omitted, then $bkickban$b will be done in the channel where the command was given.",
+        "$uSee Also:$u ban, kickall, kickbanall");
+"KICKBANALL" ("/msg $O KICKBANALL <#channel> [reason]",
+        "Kick and bans all members of the specified channel except for the user issuing the command.",
+        "If the channel is omitted, then $bkickbanall$b will be done in the channel where the command was given.",
+        "$uSee Also:$u ban, kick, kickban");
+"MODE" ("/msg $O MODE <#channel> <+/- mode>",
+        "Sets the specified modes (but cannot include voice, ban or op changes) on a channel.",
+        "If the channel is omitted, then $bmode$b will be done in the channel where the command was given.",
+        "$uSee Also:$u ban, deop, kickban, op");
+"OP" ("/msg $O OP <#channel> <nick> [nick]...",
+        "Ops specified nicknames the specified channel.",
+        "If the channel is omitted, then $bop$b will be done in the channel where the command was given.",
+        "$uSee Also:$u deop, deopall, opall");
+"OPALL" ("/msg $O OPALL <#channel>",
+        "Ops all members of the specified channel.",
+        "If the channel is omitted, then $bopall$b will be done in the channel where the command was given.",
+        "$uSee Also:$u deopall");
+"VOICEALL" ("/msg $O VOICEALL <#channel>",
+        "Voices all members of the specified channel who do not have channel ops.",
+        "If the channel is omitted, then $bvoiceall$b will be done in the channel where the command was given.",
+        "$uSee Also:$u opall, deopall, devoiceall");
+
+"GLINES" ("$bGLINE COMMANDS$b",
+        "Searches for, issues, and removes G-lines (network-global K-lines).",
+        "  BLOCK    [${level/block}]",
+        "  GLINE    [${level/gline}]",
+        "  GTRACE   [${level/gtrace}]",
+        "  GSYNC    [${level/gsync}]",
+        "  REFRESHG [${level/refreshg}]",
+        "  UNGLINE  [${level/ungline}]");
+"BLOCK" ("/msg $O BLOCK <nick> [reason]",
+        "GLINES the host of the specified nick for one hour  If no reason is given, use a default reason.",
+        "$uSee Also:$u gline, ungline");
+"GLINE" ("/msg $O GLINE <user@host> <duration> <reason>",
+        "Issues a GLINE (network ban) on the network for the speicified user@host for the specified duration (making the expiration time: net time + duration).",
+        "$uSee Also:$u trace, ungline");
+"GTRACE" ("/msg $O GTRACE <action> <criteria> [<criteria> <value>]...",
+        "Searches through the glines, much like $bTRACE$b does for users.",
+        "$uSee Also:$u trace, gtrace action, gtrace criteria");
+"GTRACE ACTION" ("$bGTRACE ACTION$b",
+        "Options for the action in $bgtrace$b are:",
+        "$bPRINT$b: Display the glines (mask, issuer, expiration time, reason)",
+        "$bCOUNT$b: Count the number of matching glines",
+       "$bUNGLINE$b: Remove matching glines");
+"GTRACE CRITERIA" ("$bGTRACE CRITERIA$b",
+        "Criteria and values for $bgtrace$b (a search with $bgtrace$b must match all the criteria you give):",
+        "$bMASK SUPERSET$b user@host G-line matches if it applies to someone with this hostmask.",
+        "$bMASK SUBSET$b user@host  G-line matches if this hostmask \"covers\" the G-line target.",
+        "$bMASK EXACT$b user@host   G-line matches only if the target is exactly this.",
+        "$bMASK$b user@host         Specifies a mask to search for (equivalent to MASK SUPERSET).",
+        "$bLIMIT$b count            Limits the number of matching glines.",
+        "$bREASON$b reason          Looks for glines with the given reason.",
+        "$bISSUER$b account         Looks for glines issued by the given account.",
+        "$bAFTER$b interval         Looks for glines that expire more than $binterval$b in the future.");
+"GSYNC" ("/msg $O GSYNC [server]",
+       "Requests a list of GLINES from its uplink or the specified server.  This can be used in the event srvx is down for period and becomes desynced.",
+       "$uSee Also:$u refreshg, gline, ungline");
+"REFRESHG" ("/msg $O REFRESHG [server]",
+        "Re-issues all GLINES in $b$O's$b database. Usually used for newly joining or desynched servers.  If a server mask is specified, the GLINES are only sent to server(s) with matching names.",
+        "$uSee Also:$u gline, ungline, gsync");
+"UNGLINE" ("/msg $O UNGLINE <user@host>",
+        "Removes a gline from the network before it expires.",
+        "$uSee Also:$u gline");
+
+"CLONES" ("$bCLONE/JUPE COMMANDS$b",
+        "Commands that add, remove or control fake (reserved) users or servers.",
+        "  CLONE     [${level/clone}]",
+        "  COLLIDE   [${level/collide}]",
+        "  JUPE      [${level/jupe}]",
+        "  RESERVE   [${level/reserve}]",
+        "  UNJUPE    [${level/unjupe}]",
+        "  UNRESERVE [${level/unreserve}]");
+"CLONE" ("/msg $O CLONE <sub-command> <sub-command arguments>",
+        "Creats and manipulates a fake user. Sub-commands for $bclone$b are:",
+        "$bADD$b:    Adds a new clone. Arguments: <nickname> <user@host> <info>",
+        "$bREMOVE$b: Removes a clone. Arguments: <nickname>",
+        "$bJOIN$b:   Joins a clone to a channel. Arguments: <nickname> <channel>",
+        "$bPART$b:   Parts a clone from a channel. Arguments: <nickname> <channel>",
+        "$bOP$b:     Ops a clone in a channel. Arguments: <nickname> <channel>",
+        "$bSAY$b:    Makes a clone say something to a channel. Arguments: <nickname> <channel> <text>");
+"COLLIDE" ("/msg $O COLLIDE <nick> <ident> <host> <description>",
+        "Creates a clone with the specified properties, colliding any existing user with the same nick.",
+        "$uSee Also:$u clone");
+"JUPE" ("/msg $O JUPE <srvname> <srvnum> <description>",
+       "Causes srvx to create a \"juped\" (dummy) server.  This can be used to prevent a poorly connected server from connecting.",
+       "$uSee Also:$u unjupe, reserve, unreserve");
+"RESERVE" ("/msg $O RESERVE <nickname> <user> <host> <comment>",
+        "Used to ban, protect, or jupe a given nick.  Unlike $bclone$b and $bcollide$b, reserved nicks are saved across restarts of srvx.",
+        "$uSee Also:$u clone, unreserve, jupe");
+"UNJUPE" ("/msg $O UNJUPE <srvname>",
+       "Causes srvx to unjupe a jupe server.",
+       "$uSee Also:$u jupe, reserve, unreserve");
+"UNRESERVE" ("/msg $O UNRESERVE <nick>",
+        "Removes a nick from $b$O's$b reserve list.",
+        "$uSee Also:$u reserve");
+
+"LOGS" ("$bLOGGING COMMANDS$b",
+        "Controls what goes into logs and searches them.",
+        "  LOG             [${level/log}]",
+        "  REOPEN          [${level/reopen}]");
+"LOG" ("/msg $O LOG <criteria> <value> [<criteria> <value>]...",
+        "Searches services logs based on critera specified and displays the results.",
+        "Criteria and associated values (a search match all specified items):",
+        "$bBOT$b -         A service bot's name (for example, $O).",
+        "$bCHANNEL$b -     The channel where a command was issued (accepts wildcards).",
+        "$bNICK$b -        The nickname issuing the command (accepts wildcards).",
+        "$bACCOUNT$b -     The account of the user who issued the command (accepts wildcards).",
+        "$bHOSTMASK$b -    The ident@host of the user who issued the command (accepts wildcards).",
+        "$bAGE$b -         Age of commands to find (for example, 1m or >3m).",
+        "$bLIMIT$b -       Maximum number of results to show.",
+        "$bLEVEL$b -       Comma-separated list of COMMAND, OVERRIDE, STAFF, to return only those commands.",
+        "$bTYPE$b -        Name of module that generated log (see $bSTATS MODULES$b).",
+        "By default, all levels of audit log entries are returned. You may exclude levels from the results by using the level criteria and the '-' character in front of the level name.");
+"REOPEN" ("/msg $O REOPEN",
+        "Close and re-open all the log files.",
+        "$uSee Also:$u log, loginfo");
+
+"RUNNING" ("$bRUNNING COMMANDS$b",
+        "These commands control srvx's runtime state.",
+        "  DIE       [${level/die}]",
+        "  JUMP      [${level/jump}]",
+        "  RECONNECT [${level/reconnect}]",
+        "  REHASH    [${level/rehash}]",
+        "  RESTART   [${level/restart}]",
+        "  WRITE     [${level/write}]",
+        "  WRITEALL  [${level/writeall}]");
+"DIE" ("/msg $O DIE <reason>",
+        "SQUIT srvx with the given reason and shuts it down.",
+        "$uSee Also:$u reconnect, restart");
+"JUMP" ("/msg $O JUMP <uplink>",
+        "Causes srvx to connect to the specified uplink.",
+        "$uSee Also:$u die, reconnect, restart");
+"RECONNECT" ("/msg $O RECONNECT ",
+        "Causes srvx to reconnect to its current uplink.",
+        "$uSee Also:$u die, jump, rehash, restart");
+"REHASH" ("/msg $O REHASH",
+        "Causes srvx to re-read its configuration file and update its state as much as possible.",
+        "$uSee Also:$u die, reconnect, restart");
+"RESTART" ("/msg $O RESTART <reason>",
+        "Causes srvx to SQUIT with the specified reason and restart.",
+        "$uSee Also:$u die, reconnect, rehash");
+
+"SERVICES" ("$bSERVICES COMMANDS$b",
+        "These commands control how other services behave.",
+        "  BANEMAIL   [${level/banemail}]",
+        "  BIND       [${level/bind}]",
+        "  GAG        [${level/gag}]",
+        "  HELPSERV   [${level/helpserv}]",
+        "  QUERY      [${level/query}]",
+        "  SET        [${level/set}]",
+        "  UNBANEMAIL [${level/unbanemail}]",
+        "  UNBIND     [${level/unbind}]",
+        "  UNGAG      [${level/ungag}]");
+"GAG" ("/msg $O GAG <mask> <duration> <reason>",
+        "Sets a complete services-wide ignore on all users matching the provided mask. All services will completely ignore all private messages or notices from gagged users, who are only notified when a gag goes into effect, or they sign onto the network. Gags have no effect on opers, and as a safety feature, only affect the first 250 matching users.  The gag will automatically expire after $b<duration>$b (or last forever if that is zero).",
+        "$uSee Also:$u ungag, trace");
+"HELPSERV" ("/msg $O HELPSERV <command>",
+        "Sends a command to the HelpServ system. It is used for all \"override\" commands, as well as registration and unregistration.",
+        "$uSee also:$u /msg $O HELPSERV HELP");
+"QUERY" ("/msg $O QUERY <option>",
+        "Displays the value of the given configuration key.  If the key is a composite entry, show the subkeys inside it.",
+        "$uSee Also:$u set");
+"SET" ("/msg $O SET <option> <value>",
+        "Modifies the internal configuration database. Currently, only keys that have been previously set may be modified.",
+        "$uSee Also:$u query");
+"UNGAG" ("/msg $O UNGAG <hostmask>",
+        "Ungags a gaged hostmask and displays how many users were affected by $bungag$b. You can get a list of gaged hostmasks from $bgags$b.",
+        "$uSee Also:$u gag, stats gags");
+
+"OTHER" ("$bOTHER COMMANDS$b",
+        "These commands do not easily fall into one of the other categories.",
+        "  DUMP    [${level/dump}]",
+        "  HELP    [${level/help}]",
+        "  RAW     [${level/raw}]",
+        "  STATS   [${level/stats}]",
+        "  SETTIME [${level/settime}]",
+        "  VERSION [${level/version}]");
+"DUMP" ("/msg $O DUMP <raw line>",
+        "Dumps a raw server message into the stream. Unlike $braw$b, $bdump$b checks line syntax before sending it, making it much safer to use then $braw$b. If $bdump$b detects a syntax error in the line, it is not sent. It is $bHIGHLY$b recommended that you use $bdump$b instead of $braw$b.",
+        "$uSee Also:$u raw");
+"RAW" ("/msg $O RAW <raw line>",
+        "Dumps a raw server message into the stream. Unlike $bdump$b, $braw$b does $bNOT$b check line syntax before sending it, making $braw$b dangerous. It will however, after the line is sent, warn of a parse error if there is a problem with the line. It is $bHIGHLY$b recommended that $bdump$b be used instead of $braw$b because it is much safer.",
+        "$uSee Also:$u dump");
+"SETTIME" ("/msg $O SETTIME [servermask] [resync]",
+        "Sets the time on the named server(s) to match the time known to srvx.",
+        "For example, using $b*$b as the mask sets the time on all servers; using a server's full name sets the time only on that one.",
+        "If the RESYNC argument is provided, sets the time to what srvx believes the local time is, rather than what it believes the network time is.");
+"STATS" ("/msg $O STATS <subject>",
+        "Displays statistics about a specified subject. Subjects include:",
+        "$bALERTS$b:     The list of current \"alerts\".",
+        "$bBAD$b:        Current list of bad words and exempted channels.",
+        "$bGAGS$b:       The list of current gags.",
+        "$bGLINES$b:     Reports the current number of glines.",
+        "$bLINKS$b:      Information about the link to the network.",
+        "$bMAX$b:        The max clients seen on the network.",
+        "$bNETWORK$b:    Displays network information such as total users and how many users are on each server.",
+        "$bNETWORK2$b:   Additional information about the network, such as numerics and linked times.",
+        "$bOPERS$b:      A list of users that are currently +o.",
+        "$bPROXYCHECK$b: Information about proxy checking in srvx.",
+        "$bRESERVED$b:   The list of currently reserved nicks.",
+        "$bTIMEQ$b:      The number of events in the timeq, and how long until the next one.",
+        "$bTRUSTED$b:    The list of currently trusted IPs.",
+        "$bUPTIME$b:     Srvx uptime, lines processed, and CPU time.",
+        "$bWARN$b:       The list of channels with activity warnings.",
+        "$bMODULES$b:    Shows loaded modules that implement commands.",
+        "$bSERVICES$b:   Shows active service bots.");
+"VERSION" ("/msg $O VERSION ",
+        "Sends you the srvx version and all version information for $b$C$b, $b$O$b, $b$N$b, and $b$G$b.");
+
+"INDEX" "${index}";
+"SEX" ("$bSEX$b",
+        "I'm sorry, but I can't give you help regarding your sex life.",
+        "But if you're so desperate you'll ask an IRC service bot for advice, you need therapy.");
diff --git a/src/policer.c b/src/policer.c
new file mode 100644 (file)
index 0000000..62b9253
--- /dev/null
@@ -0,0 +1,67 @@
+/* policer.c - Leaky bucket
+ * Copyright 2000-2002 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "common.h"
+#include "policer.h"
+
+/* This policer uses the "leaky bucket" (GCRA) algorithm. */ 
+
+struct policer_params {
+    double bucket_size;
+    double drain_rate;
+};
+
+struct policer_params *
+policer_params_new(void)
+{
+    struct policer_params *params = malloc(sizeof(struct policer_params));
+    params->bucket_size = 0.0;
+    params->drain_rate = 0.0;
+    return params;
+}
+
+int
+policer_params_set(struct policer_params *params, const char *param, const char *value)
+{
+    if (!irccasecmp(param, "size")) {
+       params->bucket_size = strtod(value, NULL);
+    } else if (!irccasecmp(param, "drain-rate")) {
+       params->drain_rate = strtod(value, NULL);
+    } else {
+       return 0;
+    }
+    return 1;
+}
+
+void
+policer_params_delete(struct policer_params *params)
+{
+    free(params);
+}
+
+int
+policer_conforms(struct policer *pol, time_t reqtime, double weight)
+{
+    int res;
+    pol->level -= pol->params->drain_rate * (reqtime - pol->last_req);
+    if (pol->level < 0.0) pol->level = 0.0;
+    res = pol->level < pol->params->bucket_size;
+    pol->level += weight;
+    pol->last_req = reqtime;
+    return res;
+}
diff --git a/src/policer.h b/src/policer.h
new file mode 100644 (file)
index 0000000..89c26ce
--- /dev/null
@@ -0,0 +1,37 @@
+/* policer.h - Leaky bucket
+ * Copyright 2000-2001 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef POLICER_H
+#define POLICER_H
+
+struct policer_params;
+
+struct policer_params *policer_params_new(void);
+int policer_params_set(struct policer_params *params, const char *param, const char *value);
+void policer_params_delete(struct policer_params *params);
+
+struct policer {
+    double level;
+    time_t last_req;
+    struct policer_params *params;
+};
+
+int policer_conforms(struct policer *pol, time_t reqtime, double weight);
+void policer_delete(struct policer *pol);
+
+#endif /* ndef POLICER_H */
diff --git a/src/proto-bahamut.c b/src/proto-bahamut.c
new file mode 100644 (file)
index 0000000..f10f0f2
--- /dev/null
@@ -0,0 +1,1445 @@
+/* proto-bahamut.c - IRC protocol output
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "proto-common.c"
+
+#define CAPAB_TS3       0x01
+#define CAPAB_NOQUIT    0x02
+#define CAPAB_SSJOIN    0x04
+#define CAPAB_BURST     0x08
+#define CAPAB_UNCONNECT 0x10
+#define CAPAB_NICKIP    0x20
+#define CAPAB_TSMODE    0x40
+#define CAPAB_ZIP       0x80
+
+struct service_message_info {
+    privmsg_func_t on_privmsg;
+    privmsg_func_t on_notice;
+};
+
+static dict_t service_msginfo_dict; /* holds service_message_info structs */
+static int uplink_capab;
+static void privmsg_user_helper(struct userNode *un, void *data);
+
+void irc_svsmode(struct userNode *target, char *modes, unsigned long stamp);
+
+struct server *
+AddServer(struct server *uplink, const char *name, int hops, time_t boot, time_t link, UNUSED_ARG(const char *numeric), const char *description) {
+    struct server* sNode;
+
+    sNode = calloc(1, sizeof(*sNode));
+    sNode->uplink = uplink;
+    safestrncpy(sNode->name, name, sizeof(sNode->name));
+    sNode->hops = hops;
+    sNode->boot = boot;
+    sNode->link = link;
+    sNode->users = dict_new();
+    safestrncpy(sNode->description, description, sizeof(sNode->description));
+    serverList_init(&sNode->children);
+    if (sNode->uplink) {
+        /* uplink may be NULL if we're just building ourself */
+        serverList_append(&sNode->uplink->children, sNode);
+    }
+    dict_insert(servers, sNode->name, sNode);
+
+    if (hops && !self->burst) {
+        unsigned int n;
+        for (n=0; n<slf_used; n++) {
+            slf_list[n](sNode);
+        }
+    }
+
+    return sNode;
+}
+
+void
+DelServer(struct server* serv, int announce, const char *message) {
+    unsigned int nn;
+    dict_iterator_t it, next;
+
+    if (!serv) return;
+    if (announce && (serv->uplink == self) && (serv != self->uplink)) {
+        irc_squit(serv, message, NULL);
+    }
+    for (nn=serv->children.used; nn>0;) {
+        if (serv->children.list[--nn] != self) {
+            DelServer(serv->children.list[nn], false, "uplink delinking");
+        }
+    }
+    for (it=dict_first(serv->users); it; it=next) {
+        next = iter_next(it);
+        DelUser(iter_data(it), NULL, false, "server delinking");
+    }
+    if (serv->uplink) serverList_remove(&serv->uplink->children, serv);
+    if (serv == self->uplink) self->uplink = NULL;
+    dict_remove(servers, serv->name);
+    serverList_clean(&serv->children);
+    dict_delete(serv->users);
+    free(serv);
+}
+
+int
+is_valid_nick(const char *nick) {
+    /* IRC has some of The Most Fucked-Up ideas about character sets
+     * in the world.. */
+    if ((*nick < 'A') || (*nick >= '~')) return 0;
+    for (++nick; *nick; ++nick) {
+        if (!((*nick >= 'A') && (*nick < '~'))
+            && !isdigit(*nick)
+            && (*nick != '-')) {
+            return 0;
+        }
+    }
+    if (strlen(nick) > nicklen) return 0;
+    return 1;
+}
+
+struct userNode *
+AddUser(struct server* uplink, const char *nick, const char *ident, const char *hostname, const char *modes, const char *userinfo, time_t timestamp, struct in_addr realip, const char *stamp) {
+    struct userNode *uNode, *oldUser;
+    unsigned int nn;
+
+    if (!uplink) {
+        log_module(MAIN_LOG, LOG_WARNING, "AddUser(%p, %s, ...): server does not exist!", uplink, nick);
+        return NULL;
+    }
+
+    if (!is_valid_nick(nick)) {
+        log_module(MAIN_LOG, LOG_WARNING, "AddUser(%p, %s, ...): invalid nickname detected.", uplink, nick);
+        return NULL;
+    }
+
+    if ((oldUser = GetUserH(nick))) {
+        if (IsLocal(oldUser) && IsService(oldUser)) {
+            /* The service should collide the new user off. */
+            oldUser->timestamp = timestamp - 1;
+            irc_user(oldUser);
+        }
+        if (oldUser->timestamp > timestamp) {
+            /* "Old" user is really newer; remove them */
+            DelUser(oldUser, 0, 1, "Overruled by older nick");
+        } else {
+            /* User being added is too new */
+            return NULL;
+        }
+    }
+
+    uNode = calloc(1, sizeof(*uNode));
+    uNode->nick = strdup(nick);
+    safestrncpy(uNode->ident, ident, sizeof(uNode->ident));
+    safestrncpy(uNode->info, userinfo, sizeof(uNode->info));
+    safestrncpy(uNode->hostname, hostname, sizeof(uNode->hostname));
+    uNode->ip = realip;
+    uNode->timestamp = timestamp;
+    modeList_init(&uNode->channels);
+    uNode->uplink = uplink;
+    dict_insert(uplink->users, uNode->nick, uNode);
+    if (++uNode->uplink->clients > uNode->uplink->max_clients) {
+        uNode->uplink->max_clients = uNode->uplink->clients;
+    }
+
+    dict_insert(clients, uNode->nick, uNode);
+    if (dict_size(clients) > max_clients) {
+        max_clients = dict_size(clients);
+        max_clients_time = now;
+    }
+
+    mod_usermode(uNode, modes);
+    if (stamp) call_account_func(uNode, stamp);
+    if (IsLocal(uNode)) irc_user(uNode);
+    for (nn=0; nn<nuf_used; nn++) {
+        if (nuf_list[nn](uNode)) break;
+    }
+    return uNode;
+}
+
+struct userNode *
+AddService(const char *nick, const char *desc) {
+    time_t timestamp = now;
+    struct userNode *old_user = GetUserH(nick);
+    struct in_addr ipaddr = { INADDR_LOOPBACK };
+    if (old_user) {
+        if (IsLocal(old_user))
+            return old_user;
+        timestamp = old_user->timestamp - 1;
+    }
+    return AddUser(self, nick, nick, self->name, "+oikr", desc, timestamp, ipaddr, 0);
+}
+
+struct userNode *
+AddClone(const char *nick, const char *ident, const char *hostname, const char *desc) {
+    time_t timestamp = now;
+    struct userNode *old_user = GetUserH(nick);
+    struct in_addr ipaddr = { INADDR_LOOPBACK };
+    if (old_user) {
+        if (IsLocal(old_user))
+            return old_user;
+        timestamp = old_user->timestamp - 1;
+    }
+    return AddUser(self, nick, ident, hostname, "+ir", desc, timestamp, ipaddr, 0);
+}
+
+void
+free_user(struct userNode *user)
+{
+    free(user->nick);
+    free(user);
+}
+
+void
+DelUser(struct userNode* user, struct userNode *killer, int announce, const char *why) {
+    unsigned int nn;
+
+    for (nn=user->channels.used; nn>0;) {
+        DelChannelUser(user, user->channels.list[--nn]->channel, false, 0);
+    }
+    for (nn=duf_used; nn>0; ) duf_list[--nn](user, killer, why);
+    user->uplink->clients--;
+    dict_remove(user->uplink->users, user->nick);
+    if (IsOper(user)) userList_remove(&curr_opers, user);
+    if (IsInvisible(user)) invis_clients--;
+    if (user == dict_find(clients, user->nick, NULL)) dict_remove(clients, user->nick);
+    if (announce) {
+        if (IsLocal(user)) {
+            irc_quit(user, why);
+        } else {
+            irc_kill(killer, user, why);
+        }
+    }
+    modeList_clean(&user->channels);
+    user->dead = 1;
+    if (dead_users.size) {
+        userList_append(&dead_users, user);
+    } else {
+        free_user(user);
+    }
+}
+
+void
+irc_server(struct server *srv) {
+    if (srv == self) {
+        putsock("SERVER %s %d :%s", srv->name, srv->hops, srv->description);
+    } else {
+        putsock(":%s SERVER %s %d :%s", self->name, srv->name, srv->hops, srv->description);
+    }
+}
+
+void
+irc_user(struct userNode *user) {
+    char modes[32];
+    int modelen = 0;
+    if (IsOper(user)) modes[modelen++] = 'o';
+    if (IsInvisible(user)) modes[modelen++] = 'i';
+    if (IsWallOp(user)) modes[modelen++] = 'w';
+    if (IsService(user)) modes[modelen++] = 'k';
+    if (IsServNotice(user)) modes[modelen++] = 's';
+    if (IsDeaf(user)) modes[modelen++] = 'd';
+    if (IsReggedNick(user)) modes[modelen++] = 'r';
+    if (IsGlobal(user)) modes[modelen++] = 'g';
+    modes[modelen] = 0;
+    putsock("NICK %s %d "FMT_TIME_T" +%s %s %s %s %d %u :%s",
+            user->nick, user->uplink->hops+2, user->timestamp, modes,
+            user->ident, user->hostname, user->uplink->name, 0, ntohl(user->ip.s_addr), user->info);
+}
+
+void
+irc_account(struct userNode *user, const char *stamp)
+{
+    if (IsReggedNick(user)) {
+        irc_svsmode(user, "+rd", base64toint(stamp, IDLEN));
+    } else {
+        irc_svsmode(user, "+d", base64toint(stamp, IDLEN));
+    }
+}
+
+void
+irc_regnick(struct userNode *user)
+{
+    if (IsReggedNick(user)) {
+        irc_svsmode(user, "+r", 0);
+    } else {
+        irc_svsmode(user, "-r", 0);
+    }
+}
+
+void
+irc_nick(struct userNode *user, const char *old_nick) {
+    if (user->uplink == self) {
+        /* update entries in PRIVMSG/NOTICE handlers (if they exist) */
+        struct service_message_info *smi = dict_find(service_msginfo_dict, user->nick, NULL);
+        if (smi) {
+            dict_remove2(service_msginfo_dict, old_nick, 1);
+            dict_insert(service_msginfo_dict, user->nick, smi);
+        }
+    }
+    putsock(":%s NICK %s :"FMT_TIME_T, old_nick, user->nick, user->timestamp);
+}
+
+void
+irc_pass(const char *passwd) {
+    putsock("PASS %s :TS", passwd);
+}
+
+void
+irc_capab() {
+    putsock("CAPAB TS3 NOQUIT SSJOIN BURST UNCONNECT NICKIP TSMODE");
+}
+
+void
+irc_svinfo() {
+    putsock("SVINFO 3 3 0 :"FMT_TIME_T, now);
+}
+
+void
+irc_burst() {
+    putsock("BURST");
+}
+
+void
+irc_introduce(const char *passwd) {
+    extern time_t burst_begin;
+
+    irc_pass(passwd);
+    irc_capab();
+    irc_server(self);
+    irc_svinfo();
+    burst_length = 0;
+    burst_begin = now;
+    timeq_add(now + ping_freq, timed_send_ping, 0);
+}
+
+void
+irc_ping(const char *something) {
+    putsock("PING :%s", something);
+}
+
+void
+irc_pong(const char *who, const char *data) {
+    putsock(":%s PONG %s :%s", self->name, who, data);
+}
+
+void
+irc_quit(struct userNode *user, const char *message) {
+    putsock(":%s QUIT :%s", user->nick, message);
+}
+
+void
+irc_squit(struct server *srv, const char *message, const char *service_message) {
+    if (!service_message) service_message = message;
+    /* If we're leaving the network, QUIT all our clients. */
+    if ((srv == self) && (cManager.uplink->state == CONNECTED)) {
+        dict_iterator_t it;
+        for (it = dict_first(srv->users); it; it = iter_next(it)) {
+            irc_quit(iter_data(it), service_message);
+        }
+    }
+    putsock(":%s SQUIT %s 0 :%s", self->name, srv->name, message);
+    if (srv == self) {
+        /* Reconnect to the currently selected server. */
+        cManager.uplink->tries = 0;
+        log_module(MAIN_LOG, LOG_INFO, "Squitting from uplink: %s", message);
+        close_socket();
+    }
+}
+
+void
+irc_privmsg(struct userNode *from, const char *to, const char *message) {
+    putsock(":%s PRIVMSG %s :%s", from->nick, to, message);
+}
+
+void
+irc_notice(struct userNode *from, const char *to, const char *message) {
+    putsock(":%s NOTICE %s :%s", from->nick, to, message);
+}
+
+void
+irc_wallchops(UNUSED_ARG(struct userNode *from), UNUSED_ARG(const char *to), UNUSED_ARG(const char *message)) {
+}
+
+void
+irc_join(struct userNode *who, struct chanNode *what) {
+    if (what->members.used == 1) {
+        putsock(":%s SJOIN "FMT_TIME_T" %s + :@%s", self->name, what->timestamp, what->name, who->nick);
+    } else {
+        putsock(":%s SJOIN "FMT_TIME_T" %s", who->nick, what->timestamp, what->name);
+    }
+}
+
+void
+irc_invite(struct userNode *from, struct userNode *who, struct chanNode *to) {
+    putsock(":%s INVITE %s %s", from->nick, who->nick, to->name);
+}
+
+void
+irc_mode(struct userNode *who, struct chanNode *target, const char *modes) {
+    putsock(":%s MODE %s "FMT_TIME_T" %s", who->nick, target->name, target->timestamp, modes);
+}
+
+void
+irc_svsmode(struct userNode *target, char *modes, unsigned long stamp) {
+    extern struct userNode *nickserv;
+    if (stamp) {
+       putsock(":%s SVSMODE %s "FMT_TIME_T" %s %lu", nickserv->nick, target->nick, target->timestamp, modes, stamp);
+    } else {
+       putsock(":%s SVSMODE %s "FMT_TIME_T" %s", nickserv->nick, target->nick, target->timestamp, modes);
+    }
+}
+
+void
+irc_kick(struct userNode *who, struct userNode *target, struct chanNode *from, const char *msg) {
+    putsock(":%s KICK %s %s :%s", who->nick, from->name, target->nick, msg);
+    ChannelUserKicked(who, target, from);
+}
+
+void
+irc_part(struct userNode *who, struct chanNode *what, const char *reason) {
+    if (reason) {
+        putsock(":%s PART %s :%s", who->nick, what->name, reason);
+    } else {
+        putsock(":%s PART %s", who->nick, what->name);
+    }
+}
+
+void
+irc_topic(struct userNode *who, struct chanNode *what, const char *topic) {
+    putsock(":%s TOPIC %s :%s", who->nick, what->name, topic);
+}
+
+void
+irc_fetchtopic(struct userNode *from, const char *to) {
+    if (!from || !to) return;
+    putsock(":%s TOPIC %s", from->nick, to);
+}
+
+void
+irc_gline(struct server *srv, struct gline *gline) {
+    char host[HOSTLEN+1], ident[USERLEN+1], *sep;
+    unsigned int len;
+    if (srv) {
+        log_module(MAIN_LOG, LOG_WARNING, "%s tried to send a targeted G-line for %s (not supported by protocol!)", gline->issuer, gline->target);
+        return;
+    }
+    if (!(sep = strchr(gline->target, '@'))) {
+        log_module(MAIN_LOG, LOG_ERROR, "%s tried to add G-line with bad mask %s", gline->issuer, gline->target);
+        return;
+    }
+    len = sep - gline->target + 1;
+    if (len > ArrayLength(ident)) len = ArrayLength(ident);
+    safestrncpy(ident, gline->target, len);
+    safestrncpy(host, sep+1, ArrayLength(host));
+    putsock(":%s AKILL %s %s "FMT_TIME_T" %s "FMT_TIME_T" :%s", self->name, host, ident, gline->expires-gline->issued, gline->issuer, gline->issued, gline->reason);
+}
+
+void
+irc_settime(UNUSED_ARG(const char *srv_name_mask), UNUSED_ARG(time_t new_time))
+{
+    /* Bahamut has nothing like this, so ignore it. */
+}
+
+void
+irc_ungline(const char *mask) {
+    char host[HOSTLEN+1], ident[USERLEN+1], *sep;
+    unsigned int len;
+    if (!(sep = strchr(mask, '@'))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Tried to remove G-line with bad mask %s", mask);
+        return;
+    }
+    len = sep - mask + 1;
+    if (len > ArrayLength(ident)) len = ArrayLength(ident);
+    safestrncpy(ident, mask, len);
+    safestrncpy(host, sep+1, ArrayLength(host));
+    putsock(":%s RAKILL %s %s", self->name, host, ident);
+}
+
+void
+irc_error(const char *to, const char *message) {
+    if (to) {
+        putsock("%s ERROR :%s", to, message);
+    } else {
+        putsock(":%s ERROR :%s", self->name, message);
+    }
+}
+
+void
+irc_kill(struct userNode *from, struct userNode *target, const char *message) {
+    if (from) {
+        putsock(":%s KILL %s :%s!%s (%s)", from->nick, target->nick, self->name, from->nick, message);
+    } else {
+        putsock(":%s KILL %s :%s (%s)", self->name, target->nick, self->name, message);
+    }
+}
+
+void
+irc_raw(const char *what) {
+    putsock("%s", what);
+}
+
+void
+irc_stats(struct userNode *from, struct server *target, char type) {
+    putsock(":%s STATS %c :%s", from->nick, type, target->name);
+}
+
+void
+irc_svsnick(struct userNode *from, struct userNode *target, const char *newnick)
+{
+    putsock(":%s SVSNICK %s %s :"FMT_TIME_T, from->nick, target->nick, newnick, now);
+}
+
+void
+irc_numeric(struct userNode *user, unsigned int num, const char *format, ...) {
+    va_list arg_list;
+    char buffer[MAXLEN];
+    va_start(arg_list, format);
+    vsnprintf(buffer, MAXLEN-2, format, arg_list);
+    buffer[MAXLEN-1] = 0;
+    putsock(":%s %03d %s %s", self->name, num, user->nick, buffer);
+}
+
+static void
+parse_foreach(char *target_list, foreach_chanfunc cf, foreach_nonchan nc, foreach_userfunc uf, foreach_nonuser nu, void *data) {
+    char *j, old;
+    do {
+       j = target_list;
+       while (*j != 0 && *j != ',') j++;
+       old = *j;
+        *j = 0;
+       if (IsChannelName(target_list)) {
+           struct chanNode *chan = GetChannel(target_list);
+            if (chan) {
+                if (cf) cf(chan, data);
+            } else {
+                if (nc) nc(target_list, data);
+            }
+       } else {
+           struct userNode *user;
+            struct privmsg_desc *pd = data;
+
+            pd->is_qualified = 0;
+            if (*target_list == '@') {
+                user = NULL;
+            } else if (strchr(target_list, '@')) {
+               struct server *server;
+
+                pd->is_qualified = 1;
+               user = GetUserH(strtok(target_list, "@"));
+               server = GetServerH(strtok(NULL, "@"));
+
+               if (user && (user->uplink != server)) {
+                   /* Don't attempt to index into any arrays
+                      using a user's numeric on another server. */
+                   user = NULL;
+               }
+           } else {
+               user = GetUserH(target_list);
+           }
+
+            if (user) {
+                if (uf) uf(user, data);
+            } else {
+                if (nu) nu(target_list, data);
+            }
+       }
+       target_list = j+1;
+    } while (old == ',');
+}
+
+static CMD_FUNC(cmd_notice) {
+    struct privmsg_desc pd;
+    if ((argc < 3) || !origin) return 0;
+    if (!(pd.user = GetUserH(origin))) return 1;
+    if (IsGagged(pd.user) && !IsOper(pd.user)) return 1;
+    pd.is_notice = 1;
+    pd.text = argv[2];
+    parse_foreach(argv[1], privmsg_chan_helper, NULL, privmsg_user_helper, privmsg_invalid, &pd);
+    return 1;
+}
+
+static CMD_FUNC(cmd_privmsg) {
+    struct privmsg_desc pd;
+    if ((argc < 3) || !origin) return 0;
+    if (!(pd.user = GetUserH(origin))) return 1;
+    if (IsGagged(pd.user) && !IsOper(pd.user)) return 1;
+    pd.is_notice = 0;
+    pd.text = argv[2];
+    parse_foreach(argv[1], privmsg_chan_helper, NULL, privmsg_user_helper, privmsg_invalid, &pd);
+    return 1;
+}
+
+static CMD_FUNC(cmd_capab) {
+    static const struct {
+        const char *name;
+        unsigned int mask;
+    } capabs[] = {
+        { "TS3", CAPAB_TS3 },
+        { "NOQUIT", CAPAB_NOQUIT },
+        { "SSJOIN", CAPAB_SSJOIN },
+        { "BURST", CAPAB_BURST },
+        { "UNCONNECT", CAPAB_UNCONNECT },
+        { "NICKIP", CAPAB_NICKIP },
+        { "TSMODE", CAPAB_TSMODE },
+        { "ZIP", CAPAB_ZIP },
+        { NULL, 0 }
+    };
+    unsigned int nn, mm;
+
+    uplink_capab = 0;
+    for(nn=1; nn<argc; nn++) {
+        for (mm=0; capabs[mm].name && irccasecmp(capabs[mm].name, argv[nn]); mm++) ;
+        if (capabs[mm].name) {
+            uplink_capab |= capabs[mm].mask;
+        } else {
+            log_module(MAIN_LOG, LOG_INFO, "Saw unrecognized/unhandled capability %s.  Please notify srvx developers so they can add it.", argv[nn]);
+        }
+    }
+    return 1;
+}
+
+static void burst_channel(struct chanNode *chan) {
+    char line[510];
+    int pos, base_len, len, queued;
+    unsigned int nn;
+
+    if (!chan->members.used) return;
+    /* send list of users in the channel.. */
+    base_len = sprintf(line, ":%s SJOIN "FMT_TIME_T" %s ", self->name, chan->timestamp, chan->name);
+    len = irc_make_chanmode(chan, line+base_len);
+    pos = base_len + len;
+    line[pos++] = ' ';
+    line[pos++] = ':';
+    for (nn=0; nn<chan->members.used; nn++) {
+        struct modeNode *mn = chan->members.list[nn];
+        len = strlen(mn->user->nick);
+        if (pos + len > 500) {
+            line[pos-1] = 0;
+            putsock("%s", line);
+            pos = base_len;
+            line[pos++] = '0';
+            line[pos++] = ' ';
+            line[pos++] = ':';
+        }
+        if (mn->modes & MODE_CHANOP) line[pos++] = '@';
+        if (mn->modes & MODE_VOICE) line[pos++] = '+';
+        memcpy(line+pos, mn->user->nick, len);
+        pos = pos + len;
+        line[pos++] = ' ';
+    }
+    /* print the last line */
+    line[pos] = 0;
+    putsock("%s", line);
+    /* now send the bans.. */
+    base_len = sprintf(line, ":%s MODE "FMT_TIME_T" %s +", self->name, chan->timestamp, chan->name);
+    pos = sizeof(line)-1;
+    line[pos] = 0;
+    for (nn=queued=0; nn<chan->banlist.used; nn++) {
+        struct banNode *bn = chan->banlist.list[nn];
+        len = strlen(bn->ban);
+        if (pos < base_len+queued+len+4) {
+            while (queued) {
+                line[--pos] = 'b';
+                queued--;
+            }
+            putsock("%s%s", line, line+pos);
+            pos = sizeof(line)-1;
+        }
+        pos -= len;
+        memcpy(line+pos, bn->ban, len);
+        line[--pos] = ' ';
+        queued++;
+    }
+    if (queued) {
+        while (queued) {
+            line[--pos] = 'b';
+            queued--;
+        }
+        putsock("%s%s", line, line+pos);
+    }
+}
+
+static void send_burst() {
+    dict_iterator_t it;
+    for (it = dict_first(servers); it; it = iter_next(it)) {
+        struct server *serv = iter_data(it);
+        if ((serv != self) && (serv != self->uplink)) irc_server(serv);
+    }
+    putsock("BURST");
+    for (it = dict_first(clients); it; it = iter_next(it)) {
+        irc_user(iter_data(it));
+    }
+    for (it = dict_first(channels); it; it = iter_next(it)) {
+        burst_channel(iter_data(it));
+    }
+    /* Uplink will do AWAY and TOPIC bursts before sending BURST 0, but we don't */
+    putsock("BURST 0");
+}
+
+static CMD_FUNC(cmd_server) {
+    if (argc < 4) return 0;
+    if (origin) {
+        AddServer(GetServerH(origin), argv[1], atoi(argv[2]), 0, now, 0, argv[3]);
+    } else {
+        self->uplink = AddServer(self, argv[1], atoi(argv[2]), 0, now, 0, argv[3]);
+        send_burst();
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_svinfo) {
+    if (argc < 5) return 0;
+    if ((atoi(argv[1]) < 3) || (atoi(argv[2]) > 3)) return 0;
+    /* TODO: something with the timestamp we get from the other guy */
+    return 1;
+}
+
+static CMD_FUNC(cmd_ping)
+{
+    irc_pong(self->name, argc > 1 ? argv[1] : origin);
+    timeq_del(0, timed_send_ping, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+    timeq_del(0, timed_ping_timeout, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+    timeq_add(now + ping_freq, timed_send_ping, 0);
+    received_ping();
+    return 1;
+}
+
+static CMD_FUNC(cmd_burst) {
+    struct server *sender = GetServerH(origin);
+    if (!sender) return 0;
+    if (argc == 1) return 1;
+    if (sender == self->uplink) {
+        cManager.uplink->state = CONNECTED;
+    }
+    sender->self_burst = 0;
+    recalc_bursts(sender);
+    return 1;
+}
+
+static CMD_FUNC(cmd_nick) {
+    struct userNode *un;
+    if ((un = GetUserH(origin))) {
+        /* nick change */
+        if (argc < 2) return 0;
+        NickChange(un, argv[1], 1);
+    } else {
+        /* new nick from a server */
+        char id[8];
+        unsigned long stamp;
+        struct in_addr ip;
+
+        if (argc < 10) return 0;
+        stamp = strtoul(argv[8], NULL, 0);
+        if (stamp) inttobase64(id, stamp, IDLEN);
+        ip.s_addr = (argc > 10) ? atoi(argv[9]) : 0;
+        un = AddUser(GetServerH(argv[7]), argv[1], argv[5], argv[6], argv[4], argv[argc-1], atoi(argv[3]), ip, (stamp ? id : 0));
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_sjoin) {
+    struct chanNode *cNode;
+    struct userNode *uNode;
+    struct modeNode *mNode;
+    unsigned int next = 4, last;
+    char *nick, *nickend;
+
+    if ((argc == 3) && (uNode = GetUserH(origin))) {
+        /* normal JOIN */
+        if (!(cNode = GetChannel(argv[2]))) {
+            log_module(MAIN_LOG, LOG_ERROR, "Unable to find SJOIN target %s", argv[2]);
+            return 0;
+        }
+        AddChannelUser(uNode, cNode);
+        return 1;
+    }
+    if (argc < 5) return 0;
+    if (argv[3][0] == '+') {
+        char modes[MAXLEN], *pos;
+        int n_modes;
+        for (pos = argv[3], n_modes = 1; *pos; pos++) {
+            if ((*pos == 'k') || (*pos == 'l')) n_modes++;
+        }
+        unsplit_string(argv+3, n_modes, modes);
+        cNode = AddChannel(argv[2], atoi(argv[1]), modes, NULL);
+    } else if (argv[3][0] == '0') {
+        cNode = GetChannel(argv[2]);
+    } else {
+        log_module(MAIN_LOG, LOG_ERROR, "Unsure how to handle SJOIN when arg 3 is %s", argv[3]);
+        return 0;
+    }
+
+    /* argv[next] is now the space-delimited, @+-prefixed list of
+     * nicks in the channel.  Split it and add the users. */
+    for (last = 0, nick = argv[next]; !last; nick = nickend + 1) {
+        int mode = 0;
+        for (nickend = nick; *nickend && (*nickend != ' '); nickend++) ;
+        if (!*nickend) last = 1;
+        *nickend = 0;
+        if (*nick == '@') { mode |= MODE_CHANOP; nick++; }
+        if (*nick == '+') { mode |= MODE_VOICE; nick++; }
+        if ((uNode = GetUserH(nick)) && (mNode = AddChannelUser(uNode, cNode))) {
+            mNode->modes = mode;
+        }
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_mode) {
+    struct userNode *un;
+
+    if (argc < 2) {
+        log_module(MAIN_LOG, LOG_ERROR, "Illegal MODE from %s (no arguments).", origin);
+        return 0;
+    } else if (IsChannelName(argv[1])) {
+        struct chanNode *cn;
+        struct modeNode *mn;
+
+        if (!(cn = GetChannel(argv[1]))) {
+            log_module(MAIN_LOG, LOG_ERROR, "Unable to find channel %s whose mode is changing", argv[1]);
+            return 0;
+        }
+
+        if ((un = GetUserH(origin))) {
+            /* Update idle time for the user */
+            if ((mn = GetUserMode(cn, un)))
+                mn->idle_since = now;
+        } else {
+            /* Must be a server in burst or something.  Make sure we're using the right timestamp. */
+            cn->timestamp = atoi(argv[2]);
+        }
+
+        return mod_chanmode(un, cn, argv+3, argc-3, MCP_ALLOW_OVB|MCP_FROM_SERVER|MC_ANNOUNCE);
+    } else if ((un = GetUserH(argv[1]))) {
+        mod_usermode(un, argv[2]);
+        return 1;
+    } else {
+        log_module(MAIN_LOG, LOG_ERROR, "Not sure what MODE %s is affecting (not a channel name and no such user)", argv[1]);
+        return 0;
+    }
+}
+
+static CMD_FUNC(cmd_topic) {
+    struct chanNode *cn;
+    if (argc < 5) return 0;
+    if (!(cn = GetChannel(argv[1]))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find channel %s whose topic is being set", argv[1]);
+        return 0;
+    }
+    if (irccasecmp(origin, argv[2])) {
+        /* coming from a topic burst; the origin is a server */
+        safestrncpy(cn->topic, argv[4], sizeof(cn->topic));
+        safestrncpy(cn->topic_nick, argv[2], sizeof(cn->topic_nick));
+        cn->topic_time = atoi(argv[3]);
+    } else {
+        SetChannelTopic(cn, GetUserH(argv[2]), argv[4], 0);
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_part) {
+    if (argc < 2) return 0;
+    parse_foreach(argv[1], part_helper, NULL, NULL, NULL, GetUserH(origin));
+    return 1;
+}
+
+static CMD_FUNC(cmd_away) {
+    struct userNode *un;
+
+    if (!(un = GetUserH(origin))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find user %s sending AWAY", origin);
+        return 0;
+    }
+    if (argc > 1) {
+        un->modes |= FLAGS_AWAY;
+    } else {
+        un->modes &= ~FLAGS_AWAY;
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_kick) {
+    if (argc < 3) return 0;
+    ChannelUserKicked(GetUserH(origin), GetUserH(argv[2]), GetChannel(argv[1]));
+    return 1;
+}
+
+static CMD_FUNC(cmd_kill) {
+    struct userNode *user;
+    if (argc < 3) return 0;
+    if (!(user = GetUserH(argv[1]))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find kill victim %s", argv[1]);
+        return 0;
+    }
+    if (IsLocal(user) && IsService(user)) {
+        ReintroduceUser(user);
+    } else {
+        DelUser(user, GetUserH(origin), false, ((argc >= 3) ? argv[2] : NULL));
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_pong)
+{
+    if (argc < 3) return 0;
+    if (!strcmp(argv[2], self->name)) {
+       timeq_del(0, timed_send_ping, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+       timeq_del(0, timed_ping_timeout, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+       timeq_add(now + ping_freq, timed_send_ping, 0);
+       received_ping();
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_num_topic)
+{
+    static struct chanNode *cn;
+
+    if (!argv[0])
+        return 0; /* huh? */
+    if (argv[2]) {
+        cn = GetChannel(argv[2]);
+        if (!cn) {
+            log_module(MAIN_LOG, LOG_ERROR, "Unable to find channel %s in topic reply", argv[2]);
+            return 0;
+        }
+    } else
+        return 0;
+
+    switch (atoi(argv[0])) {
+    case 331:
+        cn->topic_time = 0;
+        break;  /* no topic */
+    case 332:
+        if (argc < 4)
+            return 0;
+        safestrncpy(cn->topic, unsplit_string(argv+3, argc-3, NULL), sizeof(cn->topic));
+        break;
+    case 333:
+        if (argc < 5)
+            return 0;
+        safestrncpy(cn->topic_nick, argv[3], sizeof(cn->topic_nick));
+        cn->topic_time = atoi(argv[4]);
+        break;
+    default:
+        return 0; /* should never happen */
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_quit)
+{
+    struct userNode *user;
+    if (argc < 2) return 0;
+    /* Sometimes we get a KILL then a QUIT or the like, so we don't want to
+     * call DelUser unless we have the user in our grasp. */
+    if ((user = GetUserH(origin))) {
+        DelUser(user, NULL, false, argv[1]);
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_squit)
+{
+    struct server *server;
+    if (argc < 3)
+        return 0;
+    if (!(server = GetServerH(argv[1])))
+        return 0;
+    if (server == self->uplink) {
+        /* Force a reconnect to the currently selected server. */
+        cManager.uplink->tries = 0;
+        log_module(MAIN_LOG, LOG_INFO, "Squitting from uplink: %s", argv[3]);
+        close_socket();
+        return 1;
+    }
+
+    DelServer(server, 0, argv[3]);
+    return 1;
+}
+
+static CMD_FUNC(cmd_svsnick)
+{
+    struct userNode *target, *dest;
+    if (argc < 4) return 0;
+    if (!(target = GetUserH(argv[1]))) return 0;
+    if (!IsLocal(target)) return 0;
+    if ((dest = GetUserH(argv[2]))) return 0; /* Note: Bahamut will /KILL instead. */
+    NickChange(target, argv[2], 0);
+    return 1;
+}
+
+static oper_func_t *of_list;
+static unsigned int of_size = 0, of_used = 0;
+
+void parse_cleanup(void) {
+    unsigned int nn;
+    if (of_list) free(of_list);
+    dict_delete(irc_func_dict);
+    dict_delete(service_msginfo_dict);
+    free(mcf_list);
+    for (nn=0; nn<dead_users.used; nn++) free_user(dead_users.list[nn]);
+    userList_clean(&dead_users);
+}
+
+void init_parse(void) {
+    const char *str, *desc;
+
+    str = conf_get_data("server/ping_freq", RECDB_QSTRING);
+    ping_freq = str ? ParseInterval(str) : 120;
+    str = conf_get_data("server/ping_timeout", RECDB_QSTRING);
+    ping_timeout = str ? ParseInterval(str) : 30;
+    str = conf_get_data("server/hostname", RECDB_QSTRING);
+    desc = conf_get_data("server/description", RECDB_QSTRING);
+    if (!str || !desc) {
+        log_module(MAIN_LOG, LOG_ERROR, "No server/hostname entry in config file.");
+        exit(1);
+    }
+    self = AddServer(NULL, str, 0, boot_time, now, NULL, desc);
+
+    str = conf_get_data("server/ping_freq", RECDB_QSTRING);
+    ping_freq = str ? ParseInterval(str) : 120;
+    str = conf_get_data("server/ping_timeout", RECDB_QSTRING);
+    ping_timeout = str ? ParseInterval(str) : 30;
+
+    service_msginfo_dict = dict_new();
+    dict_set_free_data(service_msginfo_dict, free);
+    irc_func_dict = dict_new();
+    dict_insert(irc_func_dict, "ADMIN", cmd_admin);
+    dict_insert(irc_func_dict, "AWAY", cmd_away);
+    dict_insert(irc_func_dict, "BURST", cmd_burst);
+    dict_insert(irc_func_dict, "CAPAB", cmd_capab);
+    dict_insert(irc_func_dict, "ERROR", cmd_error);
+    dict_insert(irc_func_dict, "GNOTICE", cmd_dummy);
+    dict_insert(irc_func_dict, "INVITE", cmd_dummy);
+    dict_insert(irc_func_dict, "KICK", cmd_kick);
+    dict_insert(irc_func_dict, "KILL", cmd_kill);
+    dict_insert(irc_func_dict, "LUSERSLOCK", cmd_dummy);
+    dict_insert(irc_func_dict, "MODE", cmd_mode);
+    dict_insert(irc_func_dict, "NICK", cmd_nick);
+    dict_insert(irc_func_dict, "NOTICE", cmd_notice);
+    dict_insert(irc_func_dict, "PART", cmd_part);
+    dict_insert(irc_func_dict, "PASS", cmd_pass);
+    dict_insert(irc_func_dict, "PING", cmd_ping);
+    dict_insert(irc_func_dict, "PONG", cmd_pong);
+    dict_insert(irc_func_dict, "PRIVMSG", cmd_privmsg);
+    dict_insert(irc_func_dict, "QUIT", cmd_quit);
+    dict_insert(irc_func_dict, "SERVER", cmd_server);
+    dict_insert(irc_func_dict, "SJOIN", cmd_sjoin);
+    dict_insert(irc_func_dict, "SQUIT", cmd_squit);
+    dict_insert(irc_func_dict, "STATS", cmd_stats);
+    dict_insert(irc_func_dict, "SVSNICK", cmd_svsnick);
+    dict_insert(irc_func_dict, "SVINFO", cmd_svinfo);
+    dict_insert(irc_func_dict, "TOPIC", cmd_topic);
+    dict_insert(irc_func_dict, "VERSION", cmd_version);
+    dict_insert(irc_func_dict, "WHOIS", cmd_whois);
+    dict_insert(irc_func_dict, "331", cmd_num_topic);
+    dict_insert(irc_func_dict, "332", cmd_num_topic);
+    dict_insert(irc_func_dict, "333", cmd_num_topic);
+    dict_insert(irc_func_dict, "413", cmd_num_topic);
+
+    userList_init(&dead_users);
+    reg_exit_func(parse_cleanup);
+}
+
+int parse_line(char *line, int recursive) {
+    char *argv[MAXNUMPARAMS];
+    int argc, cmd, res;
+    cmd_func_t *func;
+
+    argc = split_line(line, true, ArrayLength(argv), argv);
+    cmd = line[0] == ':';
+    if ((argc > cmd) && (func = dict_find(irc_func_dict, argv[cmd], NULL))) {
+        char *origin;
+        if (cmd) {
+            origin = argv[0] + 1;
+        } else if (self->uplink) {
+            origin = self->uplink->name;
+        } else {
+            origin = NULL;
+        }
+        res = func(origin, argc-cmd, argv+cmd);
+    } else {
+        res = 0;
+    }
+    if (!res) {
+       log_module(MAIN_LOG, LOG_ERROR, "PARSE ERROR on line: %s", unsplit_string(argv, argc, NULL));
+    } else if (!recursive) {
+        unsigned int i;
+        for (i=0; i<dead_users.used; i++) {
+            free_user(dead_users.list[i]);
+        }
+        dead_users.used = 0;
+    }
+    return res;
+}
+
+static void
+privmsg_user_helper(struct userNode *un, void *data)
+{
+    struct privmsg_desc *pd = data;
+    struct service_message_info *info = dict_find(service_msginfo_dict, un->nick, 0);
+    if (info) {
+        if (pd->is_notice) {
+            if (info->on_notice) info->on_notice(pd->user, un, pd->text, pd->is_qualified);
+        } else {
+            if (info->on_privmsg) info->on_privmsg(pd->user, un, pd->text, pd->is_qualified);
+        }
+    }
+}
+
+void
+reg_privmsg_func(struct userNode *user, privmsg_func_t handler) {
+    struct service_message_info *info = dict_find(service_msginfo_dict, user->nick, NULL);
+    if (!info) {
+        info = calloc(1, sizeof(*info));
+        dict_insert(service_msginfo_dict, user->nick, info);
+    }
+    info->on_privmsg = handler;
+}
+
+void
+reg_notice_func(struct userNode *user, privmsg_func_t handler) {
+    struct service_message_info *info = dict_find(service_msginfo_dict, user->nick, NULL);
+    if (!info) {
+        info = calloc(1, sizeof(*info));
+        dict_insert(service_msginfo_dict, user->nick, info);
+    }
+    info->on_notice = handler;
+}
+
+void
+reg_oper_func(oper_func_t handler)
+{
+    if (of_used == of_size) {
+       if (of_size) {
+           of_size <<= 1;
+           of_list = realloc(of_list, of_size*sizeof(oper_func_t));
+       } else {
+           of_size = 8;
+           of_list = malloc(of_size*sizeof(oper_func_t));
+       }
+    }
+    of_list[of_used++] = handler;
+}
+
+static void
+call_oper_funcs(struct userNode *user)
+{
+    unsigned int n;
+    if (IsLocal(user)) return;
+    for (n=0; n<of_used; n++)
+    {
+       of_list[n](user);
+    }
+}
+
+void mod_usermode(struct userNode *user, const char *mode_change) {
+    int add = 1;
+
+    if (!user || !mode_change) return;
+    while (1) {
+#define do_user_mode(FLAG) do { if (add) user->modes |= FLAG; else user->modes &= ~FLAG; } while (0)
+       switch (*mode_change++) {
+       case 0: return;
+       case '+': add = 1; break;
+       case '-': add = 0; break;
+       case 'o':
+           do_user_mode(FLAGS_OPER);
+           if (add) {
+               userList_append(&curr_opers, user);
+               call_oper_funcs(user);
+           } else {
+               userList_remove(&curr_opers, user);
+           }
+           break;
+       case 'O': do_user_mode(FLAGS_LOCOP); break;
+       case 'i': do_user_mode(FLAGS_INVISIBLE);
+           if (add) invis_clients++; else invis_clients--;
+           break;
+       case 'w': do_user_mode(FLAGS_WALLOP); break;
+       case 's': do_user_mode(FLAGS_SERVNOTICE); break;
+       case 'd': do_user_mode(FLAGS_DEAF); break;
+       case 'r': do_user_mode(FLAGS_REGNICK); break;
+       case 'k': do_user_mode(FLAGS_SERVICE); break;
+       case 'g': do_user_mode(FLAGS_GLOBAL); break;
+       case 'h': do_user_mode(FLAGS_HELPER); break;
+       }
+#undef do_user_mode
+    }
+}
+
+struct mod_chanmode *
+mod_chanmode_parse(struct chanNode *channel, char **modes, unsigned int argc, unsigned int flags)
+{
+    struct mod_chanmode *change;
+    unsigned int ii, in_arg, ch_arg, add;
+
+    if (argc == 0)
+        return NULL;
+    if (!(change = mod_chanmode_alloc(argc)))
+        return NULL;
+
+    for (ii = ch_arg = 0, in_arg = add = 1;
+         (modes[0][ii] != '\0') && (modes[0][ii] != ' ');
+         ++ii) {
+        switch (modes[0][ii]) {
+        case '+':
+            add = 1;
+            break;
+        case '-':
+            add = 0;
+            break;
+#define do_chan_mode(FLAG) do { if (add) change->modes_set |= FLAG, change->modes_clear &= ~FLAG; else change->modes_clear |= FLAG, change->modes_set &= ~FLAG; } while(0)
+        case 'R': do_chan_mode(MODE_REGONLY); break;
+        case 'D': do_chan_mode(MODE_DELAYJOINS); break;
+        case 'c': do_chan_mode(MODE_NOCOLORS); break;
+        case 'i': do_chan_mode(MODE_INVITEONLY); break;
+        case 'm': do_chan_mode(MODE_MODERATED); break;
+        case 'n': do_chan_mode(MODE_NOPRIVMSGS); break;
+        case 'p': do_chan_mode(MODE_PRIVATE); break;
+        case 's': do_chan_mode(MODE_SECRET); break;
+        case 't': do_chan_mode(MODE_TOPICLIMIT); break;
+#undef do_chan_mode
+        case 'l':
+            if (add) {
+                if (in_arg >= argc)
+                    goto error;
+                change->modes_set |= MODE_LIMIT;
+                change->new_limit = atoi(modes[in_arg++]);
+            } else {
+                change->modes_clear |= MODE_LIMIT;
+            }
+            break;
+        case 'k':
+            if (add) {
+                if (in_arg >= argc)
+                    goto error;
+                change->modes_set |= MODE_KEY;
+                safestrncpy(change->new_key, modes[in_arg++], sizeof(change->new_key));
+            } else {
+                change->modes_clear |= MODE_KEY;
+                if (!(flags & MCP_KEY_FREE)) {
+                    if (in_arg >= argc)
+                        goto error;
+                    in_arg++;
+                }
+            }
+            break;
+        case 'b':
+            if (!(flags & MCP_ALLOW_OVB))
+                goto error;
+            if (in_arg >= argc)
+                goto error;
+            change->args[ch_arg].mode = MODE_BAN;
+            if (!add)
+                change->args[ch_arg].mode |= MODE_REMOVE;
+            change->args[ch_arg++].hostmask = modes[in_arg++];
+            break;
+        case 'o': case 'v':
+        {
+            struct userNode *victim;
+            if (!(flags & MCP_ALLOW_OVB))
+                goto error;
+            if (in_arg >= argc)
+                goto error;
+            change->args[ch_arg].mode = (modes[0][ii] == 'o') ? MODE_CHANOP : MODE_VOICE;
+            if (!add)
+                change->args[ch_arg].mode |= MODE_REMOVE;
+            victim = GetUserH(modes[in_arg++]);
+            if ((change->args[ch_arg].member = GetUserMode(channel, victim)))
+                ch_arg++;
+            break;
+        }
+        }
+    }
+    change->argc = argc; /* in case any turned out to be ignored */
+    return change;
+  error:
+    mod_chanmode_free(change);
+    return NULL;
+}
+
+struct chanmode_buffer {
+    char modes[MAXLEN];
+    char args[MAXLEN];
+    struct chanNode *channel;
+    struct userNode *actor;
+    unsigned int modes_used;
+    unsigned int args_used;
+    size_t chname_len;
+    unsigned int is_add : 1;
+};
+
+static void
+mod_chanmode_append(struct chanmode_buffer *buf, char ch, const char *arg)
+{
+    size_t arg_len = strlen(arg);
+    if (buf->modes_used + buf->args_used + buf->chname_len + arg_len > 450) {
+        memcpy(buf->modes + buf->modes_used, buf->args, buf->args_used);
+        buf->modes[buf->modes_used + buf->args_used] = '\0';
+        irc_mode(buf->actor, buf->channel, buf->modes);
+        buf->modes[0] = buf->is_add ? '+' : '-';
+        buf->modes_used = 1;
+        buf->args_used = 0;
+    }
+    buf->modes[buf->modes_used++] = ch;
+    buf->args[buf->args_used++] = ' ';
+    memcpy(buf->args + buf->args_used, arg, arg_len);
+    buf->args_used += arg_len;
+}
+
+void
+mod_chanmode_announce(struct userNode *who, struct chanNode *channel, struct mod_chanmode *change)
+{
+    struct chanmode_buffer chbuf;
+    char int_buff[32];
+    unsigned int arg;
+
+    memset(&chbuf, 0, sizeof(chbuf));
+    chbuf.channel = channel;
+    chbuf.actor = who;
+    chbuf.chname_len = strlen(channel->name);
+
+    /* First remove modes */
+    chbuf.is_add = 0;
+    if (change->modes_clear) {
+        chbuf.modes[chbuf.modes_used++] = '-';
+#define DO_MODE_CHAR(BIT, CHAR) if (change->modes_clear & MODE_##BIT) chbuf.modes[chbuf.modes_used++] = CHAR;
+        DO_MODE_CHAR(PRIVATE, 'p');
+        DO_MODE_CHAR(SECRET, 's');
+        DO_MODE_CHAR(MODERATED, 'm');
+        DO_MODE_CHAR(TOPICLIMIT, 't');
+        DO_MODE_CHAR(INVITEONLY, 'i');
+        DO_MODE_CHAR(NOPRIVMSGS, 'n');
+        DO_MODE_CHAR(LIMIT, 'l');
+        DO_MODE_CHAR(DELAYJOINS, 'D');
+        DO_MODE_CHAR(REGONLY, 'R');
+        DO_MODE_CHAR(NOCOLORS, 'c');
+#undef DO_MODE_CHAR
+        if (change->modes_clear & channel->modes & MODE_KEY)
+            mod_chanmode_append(&chbuf, 'k', channel->key);
+    }
+    for (arg = 0; arg < change->argc; ++arg) {
+        if (!(change->args[arg].mode & MODE_REMOVE))
+            continue;
+        switch (change->args[arg].mode & ~MODE_REMOVE) {
+        case MODE_BAN:
+            mod_chanmode_append(&chbuf, 'b', change->args[arg].hostmask);
+            break;
+        default:
+            if (change->args[arg].mode & MODE_CHANOP)
+                mod_chanmode_append(&chbuf, 'o', change->args[arg].member->user->nick);
+            if (change->args[arg].mode & MODE_VOICE)
+                mod_chanmode_append(&chbuf, 'v', change->args[arg].member->user->nick);
+            break;
+        }
+    }
+
+    /* Then set them */
+    chbuf.is_add = 1;
+    if (change->modes_set) {
+        chbuf.modes[chbuf.modes_used++] = '+';
+#define DO_MODE_CHAR(BIT, CHAR) if (change->modes_set & MODE_##BIT) chbuf.modes[chbuf.modes_used++] = CHAR;
+        DO_MODE_CHAR(PRIVATE, 'p');
+        DO_MODE_CHAR(SECRET, 's');
+        DO_MODE_CHAR(MODERATED, 'm');
+        DO_MODE_CHAR(TOPICLIMIT, 't');
+        DO_MODE_CHAR(INVITEONLY, 'i');
+        DO_MODE_CHAR(NOPRIVMSGS, 'n');
+        DO_MODE_CHAR(DELAYJOINS, 'D');
+        DO_MODE_CHAR(REGONLY, 'R');
+        DO_MODE_CHAR(NOCOLORS, 'c');
+#undef DO_MODE_CHAR
+        if (change->modes_set & MODE_KEY)
+            mod_chanmode_append(&chbuf, 'k', change->new_key);
+        if (change->modes_set & MODE_LIMIT)
+        {
+            sprintf(int_buff, "%d", change->new_limit);
+            mod_chanmode_append(&chbuf, 'l', int_buff);
+        }
+    }
+    for (arg = 0; arg < change->argc; ++arg) {
+        if (change->args[arg].mode & MODE_REMOVE)
+            continue;
+        switch (change->args[arg].mode) {
+        case MODE_BAN:
+            mod_chanmode_append(&chbuf, 'b', change->args[arg].hostmask);
+            break;
+        default:
+            if (change->args[arg].mode & MODE_CHANOP)
+                mod_chanmode_append(&chbuf, 'o', change->args[arg].member->user->nick);
+            if (change->args[arg].mode & MODE_VOICE)
+                mod_chanmode_append(&chbuf, 'v', change->args[arg].member->user->nick);
+            break;
+        }
+    }
+
+    /* Flush the buffer and apply changes locally */
+    if (chbuf.modes_used > 0) {
+        memcpy(chbuf.modes + chbuf.modes_used, chbuf.args, chbuf.args_used);
+        chbuf.modes[chbuf.modes_used + chbuf.args_used] = '\0';
+        irc_mode(chbuf.actor, chbuf.channel, chbuf.modes);
+    }
+    mod_chanmode_apply(who, channel, change);
+}
+
+char *
+mod_chanmode_format(struct mod_chanmode *change, char *outbuff)
+{
+    unsigned int used = 0;
+    if (change->modes_clear) {
+        outbuff[used++] = '-';
+#define DO_MODE_CHAR(BIT, CHAR) if (change->modes_clear & MODE_##BIT) outbuff[used++] = CHAR
+        DO_MODE_CHAR(PRIVATE, 'p');
+        DO_MODE_CHAR(SECRET, 's');
+        DO_MODE_CHAR(MODERATED, 'm');
+        DO_MODE_CHAR(TOPICLIMIT, 't');
+        DO_MODE_CHAR(INVITEONLY, 'i');
+        DO_MODE_CHAR(NOPRIVMSGS, 'n');
+        DO_MODE_CHAR(LIMIT, 'l');
+        DO_MODE_CHAR(KEY, 'k');
+        DO_MODE_CHAR(DELAYJOINS, 'D');
+        DO_MODE_CHAR(REGONLY, '$');
+        DO_MODE_CHAR(NOCOLORS, 'c');
+#undef DO_MODE_CHAR
+    }
+    if (change->modes_set) {
+        outbuff[used++] = '+';
+#define DO_MODE_CHAR(BIT, CHAR) if (change->modes_set & MODE_##BIT) outbuff[used++] = CHAR
+        DO_MODE_CHAR(PRIVATE, 'p');
+        DO_MODE_CHAR(SECRET, 's');
+        DO_MODE_CHAR(MODERATED, 'm');
+        DO_MODE_CHAR(TOPICLIMIT, 't');
+        DO_MODE_CHAR(INVITEONLY, 'i');
+        DO_MODE_CHAR(NOPRIVMSGS, 'n');
+        DO_MODE_CHAR(DELAYJOINS, 'D');
+        DO_MODE_CHAR(REGONLY, 'R');
+        DO_MODE_CHAR(NOCOLORS, 'c');
+#undef DO_MODE_CHAR
+        switch (change->modes_set & (MODE_KEY|MODE_LIMIT)) {
+        case MODE_KEY|MODE_LIMIT:
+            used += sprintf(outbuff+used, "lk %d %s", change->new_limit, change->new_key);
+            break;
+        case MODE_KEY:
+            used += sprintf(outbuff+used, "k %s", change->new_key);
+            break;
+        case MODE_LIMIT:
+            used += sprintf(outbuff+used, "l %d", change->new_limit);
+            break;
+        }
+    }
+    outbuff[used] = 0;
+    return outbuff;
+}
diff --git a/src/proto-common.c b/src/proto-common.c
new file mode 100644 (file)
index 0000000..d5ebc8a
--- /dev/null
@@ -0,0 +1,735 @@
+/* proto-common.c - common IRC protocol parsing/sending support
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "conf.h"
+#include "gline.h"
+#include "ioset.h"
+#include "log.h"
+#include "nickserv.h"
+#include "timeq.h"
+#ifdef HAVE_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif
+#ifdef HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#endif
+#ifdef HAVE_ARPA_INET_H
+#include <arpa/inet.h>
+#endif
+
+unsigned int lines_processed;
+FILE *replay_file;
+struct io_fd *socket_io_fd;
+int force_n2k;
+const char *hidden_host_suffix;
+
+static char replay_line[MAXLEN+80];
+static int ping_freq;
+static int ping_timeout;
+static int replay_connected;
+static unsigned int nicklen = NICKLEN; /* how long do we think servers allow nicks to be? */
+static struct userList dead_users;
+
+extern struct cManagerNode cManager;
+extern unsigned long burst_length;
+extern struct cManagerNode cManager;
+extern struct policer_params *oper_policer_params, *luser_policer_params;
+extern server_link_func_t *slf_list;
+extern unsigned int slf_size, slf_used;
+extern new_user_func_t *nuf_list;
+extern unsigned int nuf_size, nuf_used;
+extern del_user_func_t *duf_list;
+extern unsigned int duf_size, duf_used;
+extern time_t boot_time;
+
+void received_ping(void);
+
+static int replay_read(void);
+static dict_t irc_func_dict;
+
+typedef void (*foreach_chanfunc) (struct chanNode *chan, void *data);
+typedef void (*foreach_nonchan) (char *name, void *data);
+typedef void (*foreach_userfunc) (struct userNode *user, void *data);
+typedef void (*foreach_nonuser) (char *name, void *data);
+static void parse_foreach(char *target_list, foreach_chanfunc cf, foreach_nonchan nc, foreach_userfunc uf, foreach_nonuser nu, void *data);
+
+static void
+uplink_readable(struct io_fd *fd) {
+    static char buffer[MAXLEN];
+    char *eol;
+    int pos;
+
+    pos = ioset_line_read(fd, buffer, sizeof(buffer));
+    if (pos <= 0) {
+        close_socket();
+        return;
+    }
+    if ((eol = strpbrk(buffer, "\r\n"))) *eol = 0;
+    log_replay(MAIN_LOG, false, buffer);
+    if (cManager.uplink->state != DISCONNECTED)
+        parse_line(buffer, 0);
+    lines_processed++;
+}
+
+void
+socket_destroyed(struct io_fd *fd)
+{
+    if (fd && fd->eof)
+        log_module(MAIN_LOG, LOG_ERROR, "Connection to server lost.");
+    socket_io_fd = NULL;
+    cManager.uplink->state = DISCONNECTED;
+    if (self->uplink) DelServer(self->uplink, 0, NULL);
+}
+
+void replay_event_loop(void)
+{
+    while (!quit_services) {
+        if (!replay_connected) {
+            /* this time fudging is to get some of the logging right */
+            self->link = self->boot = now;
+            cManager.uplink->state = AUTHENTICATING;
+            irc_introduce(cManager.uplink->password);
+            replay_connected = 1;
+        } else if (!replay_read()) {
+            log_module(MAIN_LOG, LOG_ERROR, "Connection to server lost.");
+            close_socket();
+        }
+        timeq_run();
+    }
+}
+
+int
+create_socket_client(struct uplinkNode *target)
+{
+    int port = target->port;
+    const char *addr = target->host;
+
+    if (replay_file)
+        return feof(replay_file) ? 0 : 1;
+
+    if (socket_io_fd) {
+        /* Leave the existing socket open, say we failed. */
+        log_module(MAIN_LOG, LOG_ERROR, "Refusing to create second connection to %s:%d.", addr, port);
+        return 0;
+    }
+
+    log_module(MAIN_LOG, LOG_INFO, "Connecting to %s:%i...", addr, port);
+
+    socket_io_fd = ioset_connect((struct sockaddr*)cManager.uplink->bind_addr, sizeof(struct sockaddr), addr, port, 1, 0, NULL);
+    if (!socket_io_fd) {
+        log_module(MAIN_LOG, LOG_ERROR, "Connection to uplink failed: %s (%d)", strerror(errno), errno);
+        target->state = DISCONNECTED;
+        target->tries++;
+        return 0;
+    }
+    socket_io_fd->readable_cb = uplink_readable;
+    socket_io_fd->destroy_cb = socket_destroyed;
+    socket_io_fd->line_reads = 1;
+    socket_io_fd->wants_reads = 1;
+    log_module(MAIN_LOG, LOG_INFO, "Connection to server established.");
+    cManager.uplink = target;
+    target->state = AUTHENTICATING;
+    target->tries = 0;
+    return 1;
+}
+
+void
+replay_read_line(void)
+{
+    struct tm timestamp;
+    time_t new_time;
+
+    if (replay_line[0]) return;
+  read_line:
+    if (!fgets(replay_line, sizeof(replay_line), replay_file)) {
+        if (feof(replay_file)) {
+            quit_services = 1;
+            memset(replay_line, 0, sizeof(replay_line));
+            return;
+        }
+    }
+    if ((replay_line[0] != '[')
+        || (replay_line[3] != ':')
+        || (replay_line[6] != ':')
+        || (replay_line[9] != ' ')
+        || (replay_line[12] != '/')
+        || (replay_line[15] != '/')
+        || (replay_line[20] != ']')
+        || (replay_line[21] != ' ')) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unrecognized timestamp in replay file: %s", replay_line);
+        goto read_line;
+    }
+    timestamp.tm_hour = strtoul(replay_line+1, NULL, 10);
+    timestamp.tm_min = strtoul(replay_line+4, NULL, 10);
+    timestamp.tm_sec = strtoul(replay_line+7, NULL, 10);
+    timestamp.tm_mon = strtoul(replay_line+10, NULL, 10) - 1;
+    timestamp.tm_mday = strtoul(replay_line+13, NULL, 10);
+    timestamp.tm_year = strtoul(replay_line+16, NULL, 10) - 1900;
+    timestamp.tm_isdst = 0;
+    new_time = mktime(&timestamp);
+    if (new_time == -1) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to parse time struct tm_sec=%d tm_min=%d tm_hour=%d tm_mday=%d tm_mon=%d tm_year=%d", timestamp.tm_sec, timestamp.tm_min, timestamp.tm_hour, timestamp.tm_mday, timestamp.tm_mon, timestamp.tm_year);
+    } else {
+        now = new_time;
+    }
+
+    if (strncmp(replay_line+22, "(info) ", 7))
+        goto read_line;
+    return;
+}
+
+static int
+replay_read(void)
+{
+    size_t len;
+    char read_line[MAXLEN];
+    while (1) {
+        replay_read_line();
+        /* if it's a sent line, break out to handle it */
+        if (!strncmp(replay_line+29, "   ", 3))
+            break;
+        if (!strncmp(replay_line+29, "W: ", 3)) {
+            log_module(MAIN_LOG, LOG_ERROR, "Expected response from services: %s", replay_line+32);
+            replay_line[0] = 0;
+        } else {
+            return 0;
+        }
+    }
+    log_replay(MAIN_LOG, false, replay_line+32);
+    safestrncpy(read_line, replay_line+32, sizeof(read_line));
+    len = strlen(read_line);
+    if (read_line[len-1] == '\n')
+        read_line[--len] = 0;
+    replay_line[0] = 0;
+    parse_line(read_line, 0);
+    lines_processed++;
+    return 1;
+}
+
+static void
+replay_write(char *text)
+{
+    replay_read_line();
+    if (strncmp(replay_line+29, "W: ", 3)) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unexpected output during replay: %s", text);
+        return;
+    } else {
+        if (strcmp(replay_line+32, text)) {
+            log_module(MAIN_LOG, LOG_ERROR, "Incorrect output during replay:\nReceived: %sExpected: %s", text, replay_line+32);
+        } else {
+            log_replay(MAIN_LOG, true, text);
+        }
+        replay_line[0] = 0;
+    }
+}
+
+void putsock(const char *text, ...) PRINTF_LIKE(1, 2);
+
+void
+putsock(const char *text, ...)
+{
+    va_list arg_list;
+    char buffer[MAXLEN];
+    int pos;
+
+    if (!cManager.uplink || cManager.uplink->state == DISCONNECTED) return;
+    buffer[0] = '\0';
+    va_start(arg_list, text);
+    pos = vsnprintf(buffer, MAXLEN - 2, text, arg_list);
+    va_end(arg_list);
+    if (pos < 0 || pos > (MAXLEN - 2)) pos = MAXLEN - 2;
+    buffer[pos] = 0;
+    if (!replay_file) {
+        log_replay(MAIN_LOG, true, buffer);
+        buffer[pos++] = '\n';
+        buffer[pos] = 0;
+        ioset_write(socket_io_fd, buffer, pos);
+    } else {
+        replay_write(buffer);
+    }
+}
+
+void
+close_socket(void)
+{
+    if (replay_file) {
+        replay_connected = 0;
+        socket_destroyed(socket_io_fd);
+    } else {
+        ioset_close(socket_io_fd->fd, 1);
+    }
+}
+
+#define CMD_FUNC(NAME) int NAME(UNUSED_ARG(const char *origin), UNUSED_ARG(unsigned int argc), UNUSED_ARG(char **argv))
+typedef CMD_FUNC(cmd_func_t);
+
+static void timed_ping_timeout(void *data);
+
+/* Ping state is kept in the timeq (only one of these two can be in
+ * the queue at any given time). */
+void
+timed_send_ping(UNUSED_ARG(void *data))
+{
+    irc_ping(self->name);
+    timeq_add(now + ping_timeout, timed_ping_timeout, 0);
+}
+
+static void
+timed_ping_timeout(UNUSED_ARG(void *data))
+{
+    /* Uplink "health" tracking could be accomplished by counting the
+       number of ping timeouts that happen for each uplink. After the
+       timeouts per time period exceeds some amount, the uplink could
+       be marked as unavalable.*/
+    irc_squit(self, "Ping timeout.", NULL);
+}
+
+static CMD_FUNC(cmd_pass)
+{
+    const char *true_pass;
+
+    if (argc < 2)
+        return 0;
+    true_pass = cManager.uplink->their_password;
+    if (true_pass && strcmp(true_pass, argv[1])) {
+       /* It might be good to mark the uplink as unavailable when
+          this happens, though there should be a way of resetting
+          the flag. */
+       irc_squit(self, "Incorrect password received.", NULL);
+       return 1;
+    }
+
+    cManager.uplink->state = BURSTING;
+    return 1;
+}
+
+static CMD_FUNC(cmd_dummy)
+{
+    /* we don't care about these messages */
+    return 1;
+}
+
+static CMD_FUNC(cmd_error)
+{
+    if (argv[1]) log_module(MAIN_LOG, LOG_ERROR, "Error: %s", argv[1]);
+    log_module(MAIN_LOG, LOG_ERROR, "Error received from uplink, squitting.");
+
+    if (cManager.uplink->state != CONNECTED) {
+       /* Error messages while connected should be fine. */
+       cManager.uplink->flags |= UPLINK_UNAVAILABLE;
+       log_module(MAIN_LOG, LOG_ERROR, "Disabling uplink.");
+    }
+
+    close_socket();
+    return 1;
+}
+
+static CMD_FUNC(cmd_stats)
+{
+    struct userNode *un;
+
+    if (argc < 2)
+        return 0;
+    if (!(un = GetUserH(origin)))
+        return 0;
+    switch (argv[1][0]) {
+    case 'u': {
+        unsigned int uptime = now - boot_time;
+        irc_numeric(un, RPL_STATSUPTIME, ":Server Up %d days %d:%02d:%02d",
+                    uptime/(24*60*60), (uptime/(60*60))%24, (uptime/60)%60, uptime%60);
+        irc_numeric(un, RPL_MAXCONNECTIONS, ":Highest connection count: %d (%d clients)",
+                    self->max_clients+1, self->max_clients);
+        break;
+    }
+    default: /* unrecognized/unhandled stats output */ break;
+    }
+    irc_numeric(un, 219, "%s :End of /STATS report", argv[1]);
+    return 1;
+}
+
+static CMD_FUNC(cmd_whois)
+{
+    struct userNode *from;
+    struct userNode *who;
+
+    if (argc < 3)
+        return 0;
+    if (!(from = GetUserH(origin))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Could not find WHOIS origin user %s", origin);
+        return 0;
+    }
+    if(!(who = GetUserH(argv[2]))) {
+        irc_numeric(from, ERR_NOSUCHNICK, "%s@%s :No such nick", argv[2], self->name);
+        return 1;
+    }
+    if (IsHiddenHost(who) && !IsOper(from)) {
+        /* Just stay quiet. */
+        return 1;
+    }
+    irc_numeric(from, RPL_WHOISUSER, "%s %s %s * :%s", who->nick, who->ident, who->hostname, who->info);
+    irc_numeric(from, RPL_WHOISSERVER, "%s %s :%s", who->nick, who->uplink->name, who->uplink->description);
+    if (IsOper(who)) {
+        irc_numeric(from, RPL_WHOISOPERATOR, "%s :is a megalomaniacal power hungry tyrant", who->nick);
+    }
+    irc_numeric(from, RPL_ENDOFWHOIS, "%s :End of /WHOIS list", who->nick);
+    return 1;
+}
+
+static CMD_FUNC(cmd_version)
+{
+    struct userNode *user;
+    if (!(user = GetUserH(origin))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Could not find VERSION origin user %s", origin);
+        return 0;
+    }
+    irc_numeric(user, 351, "%s.%s %s :%s", PACKAGE_TARNAME, PACKAGE_VERSION, self->name, CODENAME);
+    return 1;
+}
+
+static CMD_FUNC(cmd_admin)
+{
+    struct userNode *user;
+    struct string_list *slist;
+
+    if (!(user = GetUserH(origin))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Could not find ADMIN origin user %s", origin);
+        return 0;
+    }
+    if ((slist = conf_get_data("server/admin", RECDB_STRING_LIST)) && slist->used) {
+        unsigned int i;
+
+        irc_numeric(user, 256, ":Administrative info about %s", self->name);
+        for (i = 0; i < slist->used && i < 3; i++)
+            irc_numeric(user, 257 + i, ":%s", slist->list[i]);
+    } else {
+        irc_numeric(user, 423, ":No administrative info available");
+    }
+    return 1;
+}
+
+static void
+recalc_bursts(struct server *eob_server)
+{
+    unsigned int nn;
+    eob_server->burst = eob_server->self_burst;
+    if (eob_server->uplink != self)
+        eob_server->burst = eob_server->burst || eob_server->uplink->burst;
+    for (nn=0; nn < eob_server->children.used; nn++)
+        recalc_bursts(eob_server->children.list[nn]);
+}
+
+static struct chanmsg_func {
+    chanmsg_func_t func;
+    struct userNode *service;
+} chanmsg_funcs[256]; /* indexed by trigger character */
+
+struct privmsg_desc {
+    struct userNode *user;
+    char *text;
+    unsigned int is_notice : 1;
+    unsigned int is_qualified : 1;
+};
+
+static void
+privmsg_chan_helper(struct chanNode *cn, void *data)
+{
+    struct privmsg_desc *pd = data;
+    struct modeNode *mn;
+    struct chanmsg_func *cf = &chanmsg_funcs[(unsigned char)pd->text[0]];
+
+    /* Don't complain if it can't find the modeNode because the channel might
+     * be -n */
+    if ((mn = GetUserMode(cn, pd->user)))
+        mn->idle_since = now;
+
+    /* Never send a NOTICE to a channel to one of the services */
+    if (!pd->is_notice && cf->func && GetUserMode(cn, cf->service))
+        cf->func(pd->user, cn, pd->text+1, cf->service);
+}
+
+static void
+privmsg_invalid(char *name, void *data)
+{
+    struct privmsg_desc *pd = data;
+
+    if (*name == '$')
+        return;
+    irc_numeric(pd->user, ERR_NOSUCHNICK, "%s@%s :No such nick", name, self->name);
+}
+
+static void
+part_helper(struct chanNode *cn, void *data)
+{
+    DelChannelUser(data, cn, false, 0);
+}
+
+void
+reg_chanmsg_func(unsigned char prefix, struct userNode *service, chanmsg_func_t handler)
+{
+    if (chanmsg_funcs[prefix].func)
+       log_module(MAIN_LOG, LOG_WARNING, "Re-registering new chanmsg handler for character `%c'.", prefix);
+    chanmsg_funcs[prefix].func = handler;
+    chanmsg_funcs[prefix].service = service;
+}
+
+struct userNode *
+get_chanmsg_bot(unsigned char prefix)
+{
+    return chanmsg_funcs[prefix].service;
+}
+
+static mode_change_func_t *mcf_list;
+static unsigned int mcf_size = 0, mcf_used = 0;
+
+void
+reg_mode_change_func(mode_change_func_t handler)
+{
+    if (mcf_used == mcf_size) {
+       if (mcf_size) {
+           mcf_size <<= 1;
+           mcf_list = realloc(mcf_list, mcf_size*sizeof(mode_change_func_t));
+       } else {
+           mcf_size = 8;
+           mcf_list = malloc(mcf_size*sizeof(mode_change_func_t));
+       }
+    }
+    mcf_list[mcf_used++] = handler;
+}
+
+struct mod_chanmode *
+mod_chanmode_alloc(unsigned int argc)
+{
+    struct mod_chanmode *res;
+    if (argc > 1)
+        res = calloc(1, sizeof(*res) + (argc-1)*sizeof(res->args[0]));
+    else
+        res = calloc(1, sizeof(*res));
+    if (res)
+        res->argc = argc;
+    return res;
+}
+
+struct mod_chanmode *
+mod_chanmode_dup(struct mod_chanmode *orig, unsigned int extra)
+{
+    struct mod_chanmode *res;
+    res = mod_chanmode_alloc(orig->argc + extra);
+    if (res) {
+        res->modes_set = orig->modes_set;
+        res->modes_clear = orig->modes_clear;
+        res->argc = orig->argc;
+        memcpy(res->args, orig->args, orig->argc*sizeof(orig->args[0]));
+    }
+    return res;
+}
+
+void
+mod_chanmode_apply(struct userNode *who, struct chanNode *channel, struct mod_chanmode *change)
+{
+    struct banNode *bn;
+    unsigned int ii, jj;
+
+    channel->modes = (channel->modes & ~change->modes_clear) | change->modes_set;
+    if (change->modes_set & MODE_LIMIT)
+        channel->limit = change->new_limit;
+    if (change->modes_set & MODE_KEY)
+        strcpy(channel->key, change->new_key);
+    for (ii = 0; ii < change->argc; ++ii) {
+        switch (change->args[ii].mode) {
+        case MODE_BAN:
+            /* If any existing ban is a subset of the new ban,
+             * silently remove it.  The new ban is not allowed
+             * to be more specific than an existing ban.
+             */
+            for (jj=0; jj<channel->banlist.used; ++jj) {
+                if (match_ircglobs(change->args[ii].hostmask, channel->banlist.list[jj]->ban)) {
+                    banList_remove(&channel->banlist, channel->banlist.list[jj]);
+                    free(channel->banlist.list[jj]);
+                    jj--;
+                }
+            }
+            bn = calloc(1, sizeof(*bn));
+            safestrncpy(bn->ban, change->args[ii].hostmask, sizeof(bn->ban));
+            safestrncpy(bn->who, who->nick, sizeof(bn->who));
+            bn->set = now;
+            banList_append(&channel->banlist, bn);
+            break;
+        case MODE_REMOVE|MODE_BAN:
+            for (jj=0; jj<channel->banlist.used; ++jj) {
+                if (strcmp(channel->banlist.list[jj]->ban, change->args[ii].hostmask))
+                    continue;
+                free(channel->banlist.list[jj]);
+                banList_remove(&channel->banlist, channel->banlist.list[jj]);
+                break;
+            }
+            break;
+        default:
+            assert((change->args[ii].mode & (MODE_REMOVE|MODE_CHANOP|MODE_VOICE)) != 0);
+            if (change->args[ii].mode & MODE_REMOVE)
+                change->args[ii].member->modes &= ~change->args[ii].mode;
+            else
+                change->args[ii].member->modes |= change->args[ii].mode;
+            break;
+        }
+    }
+}
+
+void
+mod_chanmode_free(struct mod_chanmode *change)
+{
+    free(change);
+}
+
+int
+mod_chanmode(struct userNode *who, struct chanNode *channel, char **modes, unsigned int argc, unsigned int flags)
+{
+    struct mod_chanmode *change;
+    unsigned int ii;
+
+    if (!modes || !modes[0])
+        return 0;
+    if (!(change = mod_chanmode_parse(channel, modes, argc, flags)))
+        return 0;
+    if (flags & MC_ANNOUNCE)
+        mod_chanmode_announce(who, channel, change);
+    else
+        mod_chanmode_apply(who, channel, change);
+    if (flags & MC_NOTIFY)
+        for (ii = 0; ii < mcf_used; ++ii)
+            mcf_list[ii](channel, who, change);
+    mod_chanmode_free(change);
+    return 1;
+}
+
+int
+irc_make_chanmode(struct chanNode *chan, char *out) {
+    struct mod_chanmode change;
+    change.modes_set = chan->modes;
+    change.modes_clear = change.argc = 0;
+    change.new_limit = chan->limit;
+    safestrncpy(change.new_key, chan->key, sizeof(change.new_key));
+    return strlen(mod_chanmode_format(&change, out));
+}
+
+char *
+generate_hostmask(struct userNode *user, int options)
+{
+    char *nickname, *ident, *hostname;
+    char *mask;
+    int len, ii;
+
+    /* figure out string parts */
+    if (options & GENMASK_OMITNICK)
+        nickname = NULL;
+    else if (options & GENMASK_USENICK)
+        nickname = user->nick;
+    else
+        nickname = "*";
+    if (options & GENMASK_STRICT_IDENT)
+        ident = user->ident;
+    else if (options & GENMASK_ANY_IDENT)
+        ident = "*";
+    else {
+        ident = alloca(strlen(user->ident)+2);
+        ident[0] = '*';
+        strcpy(ident+1, user->ident + ((*user->ident == '~')?1:0));
+    }
+    hostname = user->hostname;
+    if (IsHiddenHost(user) && user->handle_info && hidden_host_suffix && !(options & GENMASK_NO_HIDING)) {
+        hostname = alloca(strlen(user->handle_info->handle) + strlen(hidden_host_suffix) + 2);
+        sprintf(hostname, "%s.%s", user->handle_info->handle, hidden_host_suffix);
+    } else if (options & GENMASK_STRICT_HOST) {
+        if (options & GENMASK_BYIP)
+            hostname = inet_ntoa(user->ip);
+    } else if ((options & GENMASK_BYIP) || !hostname[strspn(hostname, "0123456789.")]) {
+        /* Should generate an IP-based hostmask.  By popular acclaim, a /16
+         * hostmask is used by default. */
+        unsigned masked_ip, mask, masklen;
+        masklen = 16;
+        mask = ~0 << masklen;
+        masked_ip = ntohl(user->ip.s_addr) & mask;
+        hostname = alloca(32);
+        if (options & GENMASK_SRVXMASK) {
+            sprintf(hostname, "%d.%d.%d.%d/%d", (masked_ip>>24)&0xFF, (masked_ip>>16)&0xFF, (masked_ip>>8)&0xFF, masked_ip&0xFF, masklen);
+        } else {
+            int ofs = 0;
+            for (ii=0; ii<4; ii++) {
+                if (masklen) {
+                    ofs += sprintf(hostname+ofs, "%d.", (masked_ip>>24)&0xFF);
+                    masklen -= 8;
+                    masked_ip <<= 8;
+                } else {
+                    ofs += sprintf(hostname+ofs, "*.");
+                }
+            }
+            /* Truncate the last . */
+            hostname[ofs-1] = 0;
+        }
+    } else {
+        int cnt;
+        /* This heuristic could be made smarter.  Is it worth the effort? */
+        for (ii=cnt=0; hostname[ii]; ii++)
+            if (hostname[ii] == '.')
+                cnt++;
+        if (cnt == 1) {
+            /* only a two-level domain name; leave hostname */
+        } else if (cnt == 2) {
+            for (ii=0; user->hostname[ii] != '.'; ii++) ;
+            /* Add 3 to account for the *. and \0. */
+            hostname = alloca(strlen(user->hostname+ii)+3);
+            sprintf(hostname, "*.%s", user->hostname+ii+1);
+        } else {
+            for (cnt=3, ii--; cnt; ii--)
+                if (user->hostname[ii] == '.')
+                    cnt--;
+            /* The loop above will overshoot the dot one character;
+               we skip forward two (the one character and the dot)
+               when printing, so we only add one for the \0. */
+            hostname = alloca(strlen(user->hostname+ii)+1);
+            sprintf(hostname, "*.%s", user->hostname+ii+2);
+        }
+    }
+    /* Emit hostmask */
+    len = strlen(ident) + strlen(hostname) + 2;
+    if (nickname) {
+        len += strlen(nickname) + 1;
+        mask = malloc(len);
+        sprintf(mask, "%s!%s@%s", nickname, ident, hostname);
+    } else {
+        mask = malloc(len);
+        sprintf(mask, "%s@%s", ident, hostname);
+    }
+    return mask;
+}
+
+int
+IsChannelName(const char *name) {
+    unsigned int ii;
+
+    if (*name !='#')
+        return 0;
+    for (ii=1; name[ii]; ++ii) {
+        if ((name[ii] > 0) && (name[ii] <= 32))
+            return 0;
+        if (name[ii] == ',')
+            return 0;
+        if (name[ii] == '\xa0')
+            return 0;
+    }
+    return 1;
+}
diff --git a/src/proto-p10.c b/src/proto-p10.c
new file mode 100644 (file)
index 0000000..2fc7fac
--- /dev/null
@@ -0,0 +1,2379 @@
+/* proto-p10.c - IRC protocol output
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "proto-common.c"
+
+/* Full commands. */
+#define CMD_ACCOUNT            "ACCOUNT"
+#define CMD_ADMIN               "ADMIN"
+#define CMD_ASLL               "ASLL"
+#define CMD_AWAY                "AWAY"
+#define CMD_BURST               "BURST"
+#define CMD_CLEARMODE           "CLEARMODE"
+#define CMD_CLOSE               "CLOSE"
+#define CMD_CNOTICE             "CNOTICE"
+#define CMD_CONNECT             "CONNECT"
+#define CMD_CPRIVMSG            "CPRIVMSG"
+#define CMD_CREATE              "CREATE"
+#define CMD_DESTRUCT            "DESTRUCT"
+#define CMD_DESYNCH             "DESYNCH"
+#define CMD_DIE                 "DIE"
+#define CMD_DNS                 "DNS"
+#define CMD_EOB                 "END_OF_BURST"
+#define CMD_EOB_ACK             "EOB_ACK"
+#define CMD_ERROR               "ERROR"
+#define CMD_GET                        "GET"
+#define CMD_GLINE               "GLINE"
+#define CMD_HASH                "HASH"
+#define CMD_HELP                "HELP"
+#define CMD_INFO                "INFO"
+#define CMD_INVITE              "INVITE"
+#define CMD_ISON                "ISON"
+#define CMD_JOIN                "JOIN"
+#define CMD_JUPE                "JUPE"
+#define CMD_KICK                "KICK"
+#define CMD_KILL                "KILL"
+#define CMD_LINKS               "LINKS"
+#define CMD_LIST                "LIST"
+#define CMD_LUSERS              "LUSERS"
+#define CMD_MAP                 "MAP"
+#define CMD_MODE                "MODE"
+#define CMD_MOTD                "MOTD"
+#define CMD_NAMES               "NAMES"
+#define CMD_NICK                "NICK"
+#define CMD_NOTICE              "NOTICE"
+#define CMD_OPER                "OPER"
+#define CMD_OPMODE              "OPMODE"
+#define CMD_PART                "PART"
+#define CMD_PASS                "PASS"
+#define CMD_PING                "PING"
+#define CMD_PONG                "PONG"
+#define CMD_POST                "POST"
+#define CMD_PRIVMSG             "PRIVMSG"
+#define CMD_PRIVS              "PRIVS"
+#define CMD_PROTO               "PROTO"
+#define CMD_QUIT                "QUIT"
+#define CMD_REHASH              "REHASH"
+#define CMD_RESET              "RESET"
+#define CMD_RESTART             "RESTART"
+#define CMD_RPING               "RPING"
+#define CMD_RPONG               "RPONG"
+#define CMD_SERVER              "SERVER"
+#define CMD_SERVLIST            "SERVLIST"
+#define CMD_SERVSET             "SERVSET"
+#define CMD_SET                        "SET"
+#define CMD_SETTIME             "SETTIME"
+#define CMD_SILENCE             "SILENCE"
+#define CMD_SQUERY              "SQUERY"
+#define CMD_SQUIT               "SQUIT"
+#define CMD_STATS               "STATS"
+#define CMD_SVSNICK             "SVSNICK"
+#define CMD_TIME                "TIME"
+#define CMD_TOPIC               "TOPIC"
+#define CMD_TRACE               "TRACE"
+#define CMD_UPING               "UPING"
+#define CMD_USER                "USER"
+#define CMD_USERHOST            "USERHOST"
+#define CMD_USERIP              "USERIP"
+#define CMD_VERSION             "VERSION"
+#define CMD_WALLCHOPS           "WALLCHOPS"
+#define CMD_WALLOPS             "WALLOPS"
+#define CMD_WALLUSERS           "WALLUSERS"
+#define CMD_WALLVOICES          "WALLVOICES"
+#define CMD_WHO                 "WHO"
+#define CMD_WHOIS               "WHOIS"
+#define CMD_WHOWAS              "WHOWAS"
+
+/* Tokenized commands. */
+#define TOK_ACCOUNT            "AC"
+#define TOK_ADMIN               "AD"
+#define TOK_ASLL               "LL"
+#define TOK_AWAY                "A"
+#define TOK_BURST               "B"
+#define TOK_CLEARMODE           "CM"
+#define TOK_CLOSE               "CLOSE"
+#define TOK_CNOTICE             "CN"
+#define TOK_CONNECT             "CO"
+#define TOK_CPRIVMSG            "CP"
+#define TOK_CREATE              "C"
+#define TOK_DESTRUCT            "DE"
+#define TOK_DESYNCH             "DS"
+#define TOK_DIE                 "DIE"
+#define TOK_DNS                 "DNS"
+#define TOK_EOB                 "EB"
+#define TOK_EOB_ACK             "EA"
+#define TOK_ERROR               "Y"
+#define TOK_GET                        "GET"
+#define TOK_GLINE               "GL"
+#define TOK_HASH                "HASH"
+#define TOK_HELP                "HELP"
+#define TOK_INFO                "F"
+#define TOK_INVITE              "I"
+#define TOK_ISON                "ISON"
+#define TOK_JOIN                "J"
+#define TOK_JUPE                "JU"
+#define TOK_KICK                "K"
+#define TOK_KILL                "D"
+#define TOK_LINKS               "LI"
+#define TOK_LIST                "LIST"
+#define TOK_LUSERS              "LU"
+#define TOK_MAP                 "MAP"
+#define TOK_MODE                "M"
+#define TOK_MOTD                "MO"
+#define TOK_NAMES               "E"
+#define TOK_NICK                "N"
+#define TOK_NOTICE              "O"
+#define TOK_OPER                "OPER"
+#define TOK_OPMODE              "OM"
+#define TOK_PART                "L"
+#define TOK_PASS                "PA"
+#define TOK_PING                "G"
+#define TOK_PONG                "Z"
+#define TOK_POST                "POST"
+#define TOK_PRIVMSG             "P"
+#define TOK_PRIVS              "PRIVS"
+#define TOK_PROTO               "PROTO"
+#define TOK_QUIT                "Q"
+#define TOK_REHASH              "REHASH"
+#define TOK_RESET              "RESET"
+#define TOK_RESTART             "RESTART"
+#define TOK_RPING               "RI"
+#define TOK_RPONG               "RO"
+#define TOK_SERVER              "S"
+#define TOK_SERVLIST            "SERVSET"
+#define TOK_SERVSET             "SERVSET"
+#define TOK_SET                        "SET"
+#define TOK_SETTIME             "SE"
+#define TOK_SILENCE             "U"
+#define TOK_SQUERY              "SQUERY"
+#define TOK_SQUIT               "SQ"
+#define TOK_STATS               "R"
+#define TOK_SVSNICK             "SN"
+#define TOK_TIME                "TI"
+#define TOK_TOPIC               "T"
+#define TOK_TRACE               "TR"
+#define TOK_UPING               "UP"
+#define TOK_USER                "USER"
+#define TOK_USERHOST            "USERHOST"
+#define TOK_USERIP              "USERIP"
+#define TOK_VERSION             "V"
+#define TOK_WALLCHOPS           "WC"
+#define TOK_WALLOPS             "WA"
+#define TOK_WALLUSERS           "WU"
+#define TOK_WALLVOICES          "WV"
+#define TOK_WHO                 "H"
+#define TOK_WHOIS               "W"
+#define TOK_WHOWAS              "X"
+
+/* Protocol messages; aliased to full commands or tokens depending
+   on compile-time configuration. ircu prefers tokens WITH THE
+   EXCEPTION OF THE SERVER AND PASS COMMANDS, which cannot be
+   tokenized, because clients' (ie. a linking server) commands are
+   only checked against the full command list.
+*/
+#if defined(ENABLE_TOKENS)
+#define TYPE(NAME)              TOK_ ## NAME
+#else /* !ENABLE_TOKENS */
+#define TYPE(NAME)              CMD_ ## NAME
+#endif /* ENABLE_TOKENS */
+
+#define P10_ACCOUNT            TYPE(ACCOUNT)
+#define P10_ADMIN               TYPE(ADMIN)
+#define P10_ASLL               TYPE(ASLL)
+#define P10_AWAY                TYPE(AWAY)
+#define P10_BURST               TYPE(BURST)
+#define P10_CLEARMODE           TYPE(CLEARMODE)
+#define P10_CLOSE               TYPE(CLOSE)
+#define P10_CNOTICE             TYPE(CNOTICE)
+#define P10_CONNECT             TYPE(CONNECT)
+#define P10_CPRIVMSG            TYPE(CPRIVMSG)
+#define P10_CREATE              TYPE(CREATE)
+#define P10_DESTRUCT            TYPE(DESTRUCT)
+#define P10_DESYNCH             TYPE(DESYNCH)
+#define P10_DIE                 TYPE(DIE)
+#define P10_DNS                 TYPE(DNS)
+#define P10_EOB                 TYPE(EOB)
+#define P10_EOB_ACK             TYPE(EOB_ACK)
+#define P10_ERROR               TYPE(ERROR)
+#define P10_GET                        TYPE(GET)
+#define P10_GLINE               TYPE(GLINE)
+#define P10_HASH                TYPE(HASH)
+#define P10_HELP                TYPE(HELP)
+#define P10_INFO                TYPE(INFO)
+#define P10_INVITE              TYPE(INVITE)
+#define P10_ISON                TYPE(ISON)
+#define P10_JOIN                TYPE(JOIN)
+#define P10_JUPE                TYPE(JUPE)
+#define P10_KICK                TYPE(KICK)
+#define P10_KILL                TYPE(KILL)
+#define P10_LINKS               TYPE(LINKS)
+#define P10_LIST                TYPE(LIST)
+#define P10_LUSERS              TYPE(LUSERS)
+#define P10_MAP                 TYPE(MAP)
+#define P10_MODE                TYPE(MODE)
+#define P10_MOTD                TYPE(MOTD)
+#define P10_NAMES               TYPE(NAMES)
+#define P10_NICK                TYPE(NICK)
+#define P10_NOTICE              TYPE(NOTICE)
+#define P10_OPER                TYPE(OPER)
+#define P10_OPMODE              TYPE(OPMODE)
+#define P10_PART                TYPE(PART)
+#define P10_PASS                CMD_PASS
+#define P10_PING                TYPE(PING)
+#define P10_PONG                TYPE(PONG)
+#define P10_POST                TYPE(POST)
+#define P10_PRIVMSG             TYPE(PRIVMSG)
+#define P10_PRIVS              TYPE(PRIVS)
+#define P10_PROTO               TYPE(PROTO)
+#define P10_QUIT                TYPE(QUIT)
+#define P10_REHASH              TYPE(REHASH)
+#define P10_RESET              TYPE(RESET)
+#define P10_RESTART             TYPE(RESTART)
+#define P10_RPING               TYPE(RPING)
+#define P10_RPONG               TYPE(RPONG)
+#define P10_SERVER              CMD_SERVER
+#define P10_SERVLIST            TYPE(SERVLIST)
+#define P10_SERVSET             TYPE(SERVSET)
+#define P10_SET                        TYPE(SET)
+#define P10_SETTIME             TYPE(SETTIME)
+#define P10_SILENCE             TYPE(SILENCE)
+#define P10_SQUERY              TYPE(SQUERY)
+#define P10_SQUIT               TYPE(SQUIT)
+#define P10_STATS               TYPE(STATS)
+#define P10_SVSNICK             TYPE(SVSNICK)
+#define P10_TIME                TYPE(TIME)
+#define P10_TOPIC               TYPE(TOPIC)
+#define P10_TRACE               TYPE(TRACE)
+#define P10_UPING               TYPE(UPING)
+#define P10_USER                TYPE(USER)
+#define P10_USERHOST            TYPE(USERHOST)
+#define P10_USERIP              TYPE(USERIP)
+#define P10_VERSION             TYPE(VERSION)
+#define P10_WALLCHOPS           TYPE(WALLCHOPS)
+#define P10_WALLOPS             TYPE(WALLOPS)
+#define P10_WALLUSERS           TYPE(WALLUSERS)
+#define P10_WALLVOICES          TYPE(WALLVOICES)
+#define P10_WHO                 TYPE(WHO)
+#define P10_WHOIS               TYPE(WHOIS)
+#define P10_WHOWAS              TYPE(WHOWAS)
+
+/* Servers claiming to have a boot or link time before PREHISTORY
+ * trigger errors to the log.  We hope no server has been running
+ * constantly since September 1994.  :)
+ */
+#define PREHISTORY 780000000
+
+static struct server *servers_num[64*64];
+static privmsg_func_t *privmsg_funcs;
+static unsigned int num_privmsg_funcs;
+static privmsg_func_t *notice_funcs;
+static unsigned int num_notice_funcs;
+static struct dict *unbursted_channels;
+
+static struct userNode *AddUser(struct server* uplink, const char *nick, const char *ident, const char *hostname, const char *modes, const char *numeric, const char *userinfo, time_t timestamp, const char *realip);
+
+/* Numerics can be XYY, XYYY, or XXYYY; with X's identifying the
+ * server and Y's indentifying the client on that server. */
+struct server*
+GetServerN(const char *numeric)
+{
+    switch (strlen(numeric)) {
+    default:
+        return servers_num[base64toint(numeric, 2)];
+    case 4:
+    case 3:
+    case 1:
+        return servers_num[base64toint(numeric, 1)];
+    case 0:
+        return NULL;
+    }
+}
+
+struct userNode*
+GetUserN(const char *numeric) /* using numeric */
+{
+    struct userNode *un;
+    struct server *s;
+    int n, slen, ulen;
+
+    switch (strlen(numeric)) {
+    default:
+        log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too long!", numeric);
+        return NULL;
+    case 5: slen = 2; ulen = 3; break;
+    case 4: slen = 1; ulen = 3; break;
+    case 3: slen = 1; ulen = 2; break;
+    case 2: case 1: case 0:
+        log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): numeric too short!", numeric);
+        return NULL;
+    }
+    if (!(s = servers_num[base64toint(numeric, slen)])) {
+        log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s): couldn't find server (len=%d)!", numeric, slen);
+        return NULL;
+    }
+    n = base64toint(numeric+slen, ulen) & s->num_mask;
+    if (!(un = s->users[n])) {
+        log_module(MAIN_LOG, LOG_WARNING, "GetUserN(%s) couldn't find user!", numeric);
+    }
+    return un;
+}
+
+static void
+privmsg_user_helper(struct userNode *un, void *data)
+{
+    struct privmsg_desc *pd = data;
+    unsigned int num = un->num_local;
+    if (!pd->is_notice) {
+        if ((num < num_privmsg_funcs) && privmsg_funcs[num]) {
+            privmsg_funcs[num](pd->user, un, pd->text, pd->is_qualified);
+        }
+    } else {
+        if ((num < num_notice_funcs) && notice_funcs[num]) {
+            notice_funcs[num](pd->user, un, pd->text, pd->is_qualified);
+        }
+    }
+}
+
+void
+irc_server(struct server *srv)
+{
+    char extranum[COMBO_NUMERIC_LEN+1];
+
+    inttobase64(extranum, srv->num_mask, (srv->numeric[1] || (srv->num_mask >= 64*64)) ? 3 : 2);
+    if (srv == self) {
+        /* The +s, ignored by Run's ircu, means "service" to Undernet's ircu */
+        putsock(P10_SERVER " %s %d %li %li J10 %s%s +s :%s",
+                srv->name, srv->hops+1, srv->boot, srv->link, srv->numeric, extranum, srv->description);
+    } else {
+        putsock("%s " P10_SERVER " %s %d %li %li %c10 %s%s +s :%s",
+                self->numeric, srv->name, srv->hops+1, srv->boot, srv->link, (srv->self_burst ? 'J' : 'P'), srv->numeric, extranum, srv->description);
+    }
+}
+
+void
+irc_user(struct userNode *user)
+{
+    char b64ip[7];
+    if (!user)
+        return;
+    inttobase64(b64ip, ntohl(user->ip.s_addr), 6);
+    if (user->modes) {
+        int modelen;
+        char modes[32];
+
+        modelen = 0;
+        if (IsOper(user))
+            modes[modelen++] = 'o';
+        if (IsInvisible(user))
+            modes[modelen++] = 'i';
+        if (IsWallOp(user))
+            modes[modelen++] = 'w';
+        if (IsService(user))
+            modes[modelen++] = 'k';
+        if (IsServNotice(user))
+            modes[modelen++] = 's';
+        if (IsDeaf(user))
+            modes[modelen++] = 'd';
+        if (IsGlobal(user))
+            modes[modelen++] = 'g';
+        if (IsHelperIrcu(user))
+            modes[modelen++] = 'h';
+        if (IsHiddenHost(user))
+            modes[modelen++] = 'x';
+        modes[modelen] = 0;
+
+        /* we don't need to put the + in modes because it's in the format string. */
+        putsock("%s " P10_NICK " %s %d %li %s %s +%s %s %s :%s",
+                user->uplink->numeric, user->nick, user->uplink->hops+1, user->timestamp, user->ident, user->hostname, modes, b64ip, user->numeric, user->info);
+    } else {
+        putsock("%s " P10_NICK " %s %d %li %s %s %s %s :%s",
+                user->uplink->numeric, user->nick, user->uplink->hops+1, user->timestamp, user->ident, user->hostname, b64ip, user->numeric, user->info);
+    }
+}
+
+void
+irc_account(struct userNode *user, const char *stamp)
+{
+    putsock("%s " P10_ACCOUNT " %s %s", self->numeric, user->numeric, stamp);
+}
+
+void
+irc_regnick(UNUSED_ARG(struct userNode *user))
+{
+    /* no operation here */
+}
+
+void
+irc_nick(struct userNode *user, UNUSED_ARG(const char *old_nick))
+{
+    putsock("%s " P10_NICK " %s "FMT_TIME_T, user->numeric, user->nick, now);
+}
+
+void
+irc_fetchtopic(struct userNode *from, const char *to)
+{
+    if (!from || !to)
+        return;
+    putsock("%s " P10_TOPIC " %s", from->numeric, to);
+}
+
+void
+irc_squit(struct server *srv, const char *message, const char *service_message)
+{
+    if (!service_message)
+        service_message = message;
+
+    /* Are we leaving the network? */
+    if (srv == self && cManager.uplink->state == CONNECTED) {
+        unsigned int i;
+
+        /* Quit all clients linked to me. */
+        for (i = 0; i <= self->num_mask; i++) {
+            if (!self->users[i])
+                continue;
+            irc_quit(self->users[i], service_message);
+        }
+    }
+
+    putsock("%s " P10_SQUIT " %s %d :%s", self->numeric, srv->name, 0, message);
+
+    if (srv == self) {
+        /* Force a reconnect to the currently selected server. */
+        cManager.uplink->tries = 0;
+        log_module(MAIN_LOG, LOG_INFO, "Squitting from uplink: %s", message);
+        close_socket();
+    }
+}
+
+void
+irc_wallchops(struct userNode *from, const char *to, const char *message)
+{
+    putsock("%s " P10_WALLCHOPS " %s :%s", from->numeric, to, message);
+}
+
+void
+irc_notice(struct userNode *from, const char *to, const char *message)
+{
+    putsock("%s " P10_NOTICE " %s :%s", from->numeric, to, message);
+}
+
+void
+irc_privmsg(struct userNode *from, const char *to, const char *message)
+{
+    putsock("%s " P10_PRIVMSG " %s :%s", from->numeric, to, message);
+}
+
+void
+irc_eob(void)
+{
+    putsock("%s " P10_EOB, self->numeric);
+}
+
+void
+irc_eob_ack(void)
+{
+    putsock("%s " P10_EOB_ACK, self->numeric);
+}
+
+void
+irc_ping(const char *payload)
+{
+    putsock("%s " P10_PING " :%s", self->numeric, payload);
+}
+
+void
+irc_pong(const char *who, const char *data)
+{
+    putsock("%s " P10_PONG " %s :%s", self->numeric, who, data);
+}
+
+void
+irc_pass(const char *passwd)
+{
+    putsock(P10_PASS " :%s", passwd);
+}
+
+void
+irc_introduce(const char *passwd)
+{
+    void timed_send_ping(void *data);
+
+    self->self_burst = self->burst = 1;
+    irc_pass(passwd);
+    irc_server(self);
+    burst_length = 0;
+    timeq_add(now + ping_freq, timed_send_ping, 0);
+}
+
+void
+irc_gline(struct server *srv, struct gline *gline)
+{
+    putsock("%s " P10_GLINE " %s +%s %ld :%s",
+            self->numeric, (srv ? srv->numeric : "*"), gline->target, gline->expires-now, gline->reason);
+}
+
+void
+irc_settime(const char *srv_name_mask, time_t new_time)
+{
+    ioset_set_time(new_time);
+    if (!strcmp(srv_name_mask, "*"))
+        srv_name_mask = "";
+    putsock("%s " P10_SETTIME " " FMT_TIME_T " %s", self->numeric, new_time, srv_name_mask);
+}
+
+void
+irc_ungline(const char *mask)
+{
+    putsock("%s " P10_GLINE " * -%s", self->numeric, mask);
+}
+
+static void
+irc_burst(struct chanNode *chan)
+{
+    char burst_line[512];
+    int pos, base_len, len;
+    struct modeNode *mn;
+    struct banNode *bn;
+    long last_mode=-1;
+    unsigned int n;
+
+    if (!chan->members.used)
+        return;
+    base_len = sprintf(burst_line, "%s " P10_BURST " %s " FMT_TIME_T " ",
+                       self->numeric, chan->name, chan->timestamp);
+    len = irc_make_chanmode(chan, burst_line+base_len);
+    pos = base_len + len;
+    if (len)
+        burst_line[pos++] = ' ';
+
+    /* dump the users */
+    for (n=0; n<chan->members.used; n++) {
+        mn = chan->members.list[n];
+        if (pos > 500) {
+            burst_line[pos-1] = 0; /* -1 to back up over the space or comma */
+            putsock("%s", burst_line);
+            pos = base_len;
+            last_mode = -1;
+        }
+        memcpy(burst_line+pos, mn->user->numeric, strlen(mn->user->numeric));
+        pos += strlen(mn->user->numeric);
+        if (mn->modes && (mn->modes != last_mode)) {
+            last_mode = mn->modes;
+            burst_line[pos++] = ':';
+            if (last_mode & MODE_CHANOP)
+                burst_line[pos++] = 'o';
+            if (last_mode & MODE_VOICE)
+                burst_line[pos++] = 'v';
+        }
+        if ((n+1)<chan->members.used)
+            burst_line[pos++] = ',';
+    }
+    if (chan->banlist.used) {
+        /* dump the bans */
+        if (pos+2+strlen(chan->banlist.list[0]->ban) > 505) {
+            burst_line[pos-1] = 0;
+            putsock("%s", burst_line);
+            pos = base_len;
+        } else {
+            burst_line[pos++] = ' ';
+        }
+
+        burst_line[pos++] = ':';
+        burst_line[pos++] = '%';
+        base_len = pos;
+        for (n=0; n<chan->banlist.used; n++) {
+            bn = chan->banlist.list[n];
+            len = strlen(bn->ban);
+            if (pos+len+1 > 510) {
+                burst_line[pos-1] = 0; /* -1 to back up over the space or comma */
+                putsock("%s", burst_line);
+                pos = base_len;
+            }
+            memcpy(burst_line+pos, bn->ban, len);
+            pos += len;
+            burst_line[pos++] = ' ';
+        }
+    }
+    /* print the last line */
+    burst_line[pos] = 0;
+    putsock("%s", burst_line);
+}
+
+void
+irc_quit(struct userNode *user, const char *message)
+{
+    putsock("%s " P10_QUIT " :%s", user->numeric, message);
+}
+
+void
+irc_error(const char *to, const char *message)
+{
+    if (to) {
+        putsock("%s " P10_ERROR " :%s", to, message);
+    } else {
+        putsock(":%s " P10_ERROR " :%s", self->name, message);
+    }
+}
+
+void
+irc_kill(struct userNode *from, struct userNode *target, const char *message)
+{
+    if (from) {
+        putsock("%s " P10_KILL " %s :%s!%s (%s)",
+                from->numeric, target->numeric, self->name, from->nick, message);
+    } else {
+        putsock("%s " P10_KILL " %s :%s (%s)",
+                self->numeric, target->numeric, self->name, message);
+    }
+}
+
+void
+irc_mode(struct userNode *from, struct chanNode *target, const char *modes)
+{
+    putsock("%s " P10_MODE " %s %s "FMT_TIME_T,
+            (from ? from->numeric : self->numeric),
+            target->name, modes, target->timestamp);
+}
+
+void
+irc_invite(struct userNode *from, struct userNode *who, struct chanNode *to)
+{
+    putsock("%s " P10_INVITE " %s %s", from->numeric, who->nick, to->name);
+}
+
+void
+irc_join(struct userNode *who, struct chanNode *what)
+{
+    if (what->members.used == 1) {
+        putsock("%s " P10_CREATE " %s %lu",
+                who->numeric, what->name, what->timestamp);
+    } else {
+        putsock("%s " P10_JOIN " %s %lu", who->numeric, what->name, what->timestamp);
+    }
+}
+
+void
+irc_kick(struct userNode *who, struct userNode *target, struct chanNode *channel, const char *msg)
+{
+    const char *numeric;
+    struct modeNode *mn = GetUserMode(channel, who);
+    numeric = (mn && (mn->modes & MODE_CHANOP)) ? who->numeric : self->numeric;
+    putsock("%s " P10_KICK " %s %s :%s",
+            numeric, channel->name, target->numeric, msg);
+}
+
+void
+irc_stats(struct userNode *from, struct server *target, char type)
+{
+    putsock("%s " P10_STATS " %c :%s", from->numeric, type, target->numeric);
+}
+
+void
+irc_svsnick(struct userNode *from, struct userNode *target, const char *newnick)
+{
+    putsock("%s " P10_SVSNICK " %s %s "FMT_TIME_T, from->uplink->numeric, target->numeric, newnick, now);
+}
+
+void
+irc_part(struct userNode *who, struct chanNode *what, const char *reason)
+{
+    if (reason) {
+        putsock("%s " P10_PART " %s :%s", who->numeric, what->name, reason);
+    } else {
+        putsock("%s " P10_PART " %s", who->numeric, what->name);
+    }
+}
+
+void
+irc_topic(struct userNode *who, struct chanNode *what, const char *topic)
+{
+    putsock("%s " P10_TOPIC " %s :%s", who->numeric, what->name, topic);
+}
+
+void
+irc_raw(const char *what)
+{
+    putsock("%s", what);
+}
+
+void
+irc_numeric(struct userNode *user, unsigned int num, const char *format, ...)
+{
+    va_list arg_list;
+    char buffer[MAXLEN];
+    va_start(arg_list, format);
+    vsnprintf(buffer, MAXLEN-2, format, arg_list);
+    buffer[MAXLEN-1] = 0;
+    putsock(":%s %03d %s %s", self->name, num, user->nick, buffer);
+}
+
+static void send_burst(void);
+
+static void
+change_nicklen(int new_nicklen)
+{
+    unsigned int nn;
+    char new_nick[NICKLEN+1];
+    struct userNode *user;
+
+    nicklen = new_nicklen;
+    /* fix up any users we have here */
+    for (nn=0; nn<=self->num_mask; nn++) {
+        if (!(user = self->users[nn]))
+            continue;
+        safestrncpy(new_nick, user->nick, sizeof(new_nick));
+        new_nick[nicklen] = 0;
+        NickChange(user, new_nick, 1);
+    }
+}
+
+static CMD_FUNC(cmd_server)
+{
+    struct server *srv;
+    const char *str;
+
+    if (argc < 8)
+        return 0;
+    if (origin) {
+        /* another server introduced us */
+        srv = AddServer(GetServerH(origin), argv[1], atoi(argv[2]), atoi(argv[3]), atoi(argv[4]), argv[6], argv[argc-1]);
+        if (!srv)
+            return 0;
+        srv->self_burst = argv[5][0] == 'J';
+        srv->burst = 1;
+    } else {
+        /* this must be our uplink */
+        srv = self->uplink = AddServer(self, argv[1], atoi(argv[2]), atoi(argv[3]), atoi(argv[4]), argv[6], argv[argc-1]);
+        if (!srv)
+            return 0;
+        srv->self_burst = argv[5][0] == 'J';
+        srv->burst = 1;
+        if ((argv[7][0] == '+') && !force_n2k) {
+            log_module(MAIN_LOG, LOG_WARNING, "Got Undernet-style SERVER message but \"force_n2k\" not on.");
+        }
+        send_burst();
+    }
+
+    /* Fix up our timestamps if necessary. */
+    if (srv->boot <= PREHISTORY) {
+        /* Server from the mists of time.. */
+        if (srv->hops == 1) {
+            log_module(MAIN_LOG, LOG_ERROR, "Server %s claims to have booted at time "FMT_TIME_T".  This is absurd.", srv->name, srv->boot);
+        }
+    } else if ((str = conf_get_data("server/reliable_clock", RECDB_QSTRING))
+               && enabled_string(str)) {
+        /* If we have a reliable clock, we just keep our current time. */
+    } else {
+        if (srv->boot <= self->boot) {
+            /* The other server is older than us.  Accept their timestamp.
+             * Alternately, we are same age, but we accept their time
+             * since we are linking to them. */
+            self->boot = srv->boot;
+            ioset_set_time(srv->link);
+        }
+    }
+    if (srv == self->uplink) {
+        extern time_t burst_begin;
+        burst_begin = now;
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_eob)
+{
+    struct server *sender;
+    dict_iterator_t it;
+    unsigned int ii;
+
+    if (!(sender = GetServerH(origin)))
+        return 0;
+    if (sender == self->uplink) {
+        cManager.uplink->state = CONNECTED;
+        for (it = dict_first(unbursted_channels); it; it = iter_next(it))
+            irc_burst(iter_data(it));
+        dict_delete(unbursted_channels);
+        unbursted_channels = NULL;
+        irc_eob();
+        irc_eob_ack();
+    }
+    sender->self_burst = 0;
+    recalc_bursts(sender);
+    for (ii=0; ii<slf_used; ii++)
+        slf_list[ii](sender);
+    return 1;
+}
+
+static CMD_FUNC(cmd_eob_ack)
+{
+    extern time_t burst_begin;
+
+    if (GetServerH(origin) == self->uplink) {
+        burst_length = now - burst_begin;
+        self->self_burst = self->burst = 0;
+    }
+    cManager.uplink->state = CONNECTED;
+    return 1;
+}
+
+static CMD_FUNC(cmd_ping)
+{
+    if(argc > 3)
+    {
+        irc_pong(argv[2], argv[1]);
+    }
+    else
+    {
+        irc_pong(self->name, origin);
+    }
+    timeq_del(0, timed_send_ping, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+    timeq_del(0, timed_ping_timeout, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+    timeq_add(now + ping_freq, timed_send_ping, 0);
+    received_ping();
+    return 1;
+}
+
+static CMD_FUNC(cmd_error_nick)
+{
+    /* Go back to original IRC length .. and try to reconnect :/ */
+    change_nicklen(9);
+    irc_squit(self, "Got erroneous nickname, truncating nicks.", NULL);
+    return 1;
+}
+
+struct create_desc {
+    struct userNode *user;
+    time_t when;
+};
+
+static void
+join_helper(struct chanNode *chan, void *data)
+{
+    struct create_desc *cd = data;
+    AddChannelUser(cd->user, chan);
+}
+
+static void
+create_helper(char *name, void *data)
+{
+    struct create_desc *cd = data;
+    /* We can't assume the channel create was allowed because of the
+     * bad-word channel checking.
+     */
+    struct chanNode *cn;
+    struct modeNode *mn;
+    if (!strcmp(name, "0")) {
+        while (cd->user->channels.used > 0)
+            DelChannelUser(cd->user, cd->user->channels.list[0]->channel, 0, 0);
+        return;
+    }
+    cn = AddChannel(name, cd->when, NULL, NULL);
+    mn = AddChannelUser(cd->user, cn);
+    if (mn && (cn->members.used == 1))
+        mn->modes = MODE_CHANOP;
+}
+
+static CMD_FUNC(cmd_create)
+{
+    struct create_desc cd;
+    struct userNode *user;
+
+    if ((argc < 3) || !(user = GetUserH(origin)))
+        return 0;
+    cd.user = user;
+    cd.when = atoi(argv[2]);
+    parse_foreach(argv[1], join_helper, create_helper, NULL, NULL, &cd);
+    return 1;
+}
+
+static CMD_FUNC(cmd_join)
+{
+    struct create_desc cd;
+
+    if (!(cd.user = GetUserH(origin)))
+        return 0;
+    if (argc < 2)
+        return 0;
+    else if (argc < 3)
+        cd.when = now;
+    else
+        cd.when = atoi(argv[2]);
+    parse_foreach(argv[1], join_helper, create_helper, NULL, NULL, &cd);
+    return 1;
+}
+
+static CMD_FUNC(cmd_pong)
+{
+    if (argc < 3)
+        return 0;
+    if (!strcmp(argv[2], self->name)) {
+        timeq_del(0, timed_send_ping, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+        timeq_del(0, timed_ping_timeout, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
+        timeq_add(now + ping_freq, timed_send_ping, 0);
+        received_ping();
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_nick)
+{
+    struct userNode *user;
+    if ((user = GetUserH(origin))) {
+        /* nick change (since the source is a user numeric) */
+        if (argc < 2)
+            return 0;
+        NickChange(user, argv[1], 1);
+    } else {
+        struct server *serv;
+        char modes[MAXLEN];
+        /* new nick */
+        if (argc < 9)
+            return 0;
+        serv = GetServerH(origin);
+        if (argc > 9)
+            unsplit_string(argv+6, argc-9, modes);
+        else
+            strcpy(modes, "+");
+        AddUser(serv, argv[1], argv[4], argv[5], modes, argv[argc-2], argv[argc-1], atoi(argv[3]), argv[argc-3]);
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_account)
+{
+    struct userNode *user;
+
+    if ((argc < 3) || !origin || !GetServerH(origin))
+        return 0; /* Origin must be server. */
+    user = GetUserN(argv[1]);
+    if (!user)
+        return 1; /* A QUIT probably passed the ACCOUNT. */
+    call_account_func(user, argv[2]);
+    return 1;
+}
+
+static CMD_FUNC(cmd_burst)
+{
+    extern int rel_age;
+    char modes[MAXLEN], *members = "", *banlist = NULL;
+    unsigned int next = 3, res = 1;
+    struct chanNode *cNode;
+    struct userNode *un;
+    struct modeNode *mNode;
+    long mode;
+    char *user, *end, sep;
+    time_t in_timestamp;
+
+    if (argc < 3)
+        return 0;
+    modes[0] = 0;
+    while (next < argc) {
+        switch (argv[next][0]) {
+        case '+': {
+            const char *pos;
+            int n_modes;
+            for (pos=argv[next], n_modes = 1; *pos; pos++)
+                if ((*pos == 'k') || (*pos == 'l'))
+                    n_modes++;
+            unsplit_string(argv+next, n_modes, modes);
+            next += n_modes;
+            break;
+        }
+        case '%': banlist = argv[next++]+1; break;
+        default: members = argv[next++]; break;
+        }
+    }
+
+    in_timestamp = atoi(argv[2]);
+    if ((cNode = dict_find(unbursted_channels, argv[1], NULL))) {
+        cNode->timestamp = in_timestamp;
+        dict_remove(unbursted_channels, cNode->name);
+        irc_burst(cNode);
+    }
+    cNode = AddChannel(argv[1], in_timestamp, modes, banlist);
+
+    /* Burst channel members in now. */
+    for (user = members, sep = *members, mode = 0; sep; user = end) {
+        for (end = user + 3; isalnum(*end) || *end == '[' || *end == ']'; end++) ;
+        sep = *end++; end[-1] = 0;
+        if (sep == ':') {
+            mode = 0;
+            while ((sep = *end++)) {
+                if (sep == 'o')
+                    mode |= MODE_CHANOP;
+                else if (sep == 'v')
+                    mode |= MODE_VOICE;
+                else
+                    break;
+            }
+            if (rel_age < 0)
+                mode = 0;
+        }
+        if (!(un = GetUserN(user))) {
+            res = 0;
+            continue;
+        }
+        if ((mNode = AddChannelUser(un, cNode)))
+            mNode->modes = mode;
+    }
+
+    return res;
+}
+
+static CMD_FUNC(cmd_mode)
+{
+    struct chanNode *cn;
+    struct userNode *un;
+
+    if (argc < 3)
+        return 0;
+    if (!IsChannelName(argv[1])) {
+        un = GetUserH(argv[1]);
+        if (!un) {
+            log_module(MAIN_LOG, LOG_ERROR, "Unable to find user %s whose mode is changing.", argv[1]);
+            return 0;
+        }
+        mod_usermode(un, argv[2]);
+        return 1;
+    }
+
+    if (!(cn = GetChannel(argv[1]))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find channel %s whose mode is changing.", argv[1]);
+        return 0;
+    }
+    if ((un = GetUserH(origin))) {
+        struct modeNode *mn;
+        /* Update idle time for person setting the mode */
+        if ((mn = GetUserMode(cn, un)))
+            mn->idle_since = now;
+    } else {
+        /* If it came from a server, reset timestamp to re-sync. */
+        cn->timestamp = atoi(argv[argc-1]);
+    }
+
+    return mod_chanmode(un, cn, argv+2, argc-2, MCP_ALLOW_OVB|MCP_FROM_SERVER|(un ? MC_NOTIFY : 0));
+}
+
+static CMD_FUNC(cmd_opmode)
+{
+    struct chanNode *cn;
+    struct userNode *un;
+
+    if (argc < 3)
+        return 0;
+
+    if (!(cn = GetChannel(argv[1]))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find channel %s whose mode is changing.", argv[1]);
+        return 0;
+    }
+    if (!(un = GetUserH(origin))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find user %s requesting OPMODE.", origin);
+        return 0;
+    }
+    if (!IsOper(un)) {
+        log_module(MAIN_LOG, LOG_ERROR, "Non-privileged user %s using OPMODE.", un->nick);
+        return 0;
+    }
+
+    return mod_chanmode(un, cn, argv+2, argc-2, MCP_ALLOW_OVB|MCP_FROM_SERVER); /* do NOT announce opmode locally */
+}
+
+static int clear_chanmode(struct chanNode *channel, const char *modes);
+
+static CMD_FUNC(cmd_clearmode)
+{
+    struct chanNode *cn;
+    struct userNode *un;
+
+    if (argc < 3)
+        return 0;
+
+    if (!(cn = GetChannel(argv[1]))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find channel %s whose mode is changing.", argv[1]);
+        return 0;
+    }
+    if (!(un = GetUserH(origin))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find user %s requesting CLEARMODE.", origin);
+        return 0;
+    }
+    if (!IsOper(un)) {
+        log_module(MAIN_LOG, LOG_ERROR, "Non-privileged user %s using CLEARMODE.", un->nick);
+        return 0;
+    }
+
+    return clear_chanmode(cn, argv[2]);
+}
+
+static CMD_FUNC(cmd_topic)
+{
+    static struct chanNode *cn;
+
+    if (argc < 3)
+        return 0;
+    if (!(cn = GetChannel(argv[1]))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to find channel %s whose topic is being set", argv[1]);
+        return 0;
+    }
+    SetChannelTopic(cn, GetUserH(origin), argv[2], 0);
+    return 1;
+}
+
+static CMD_FUNC(cmd_num_topic)
+{
+    static struct chanNode *cn;
+
+    if (!argv[0])
+        return 0; /* huh? */
+    if (argv[2]) {
+        cn = GetChannel(argv[2]);
+        if (!cn) {
+            log_module(MAIN_LOG, LOG_ERROR, "Unable to find channel %s in topic reply", argv[2]);
+            return 0;
+        }
+    } else
+        return 0;
+
+    switch (atoi(argv[0])) {
+    case 331:
+        cn->topic_time = 0;
+        break;  /* no topic */
+    case 332:
+        if (argc < 4)
+            return 0;
+        safestrncpy(cn->topic, unsplit_string(argv+3, argc-3, NULL), sizeof(cn->topic));
+        break;
+    case 333:
+        if (argc < 5)
+            return 0;
+        safestrncpy(cn->topic_nick, argv[3], sizeof(cn->topic_nick));
+        cn->topic_time = atoi(argv[4]);
+        break;
+    default:
+        return 0; /* should never happen */
+    }
+    return 1;
+}
+
+static CMD_FUNC(cmd_num_gline)
+{
+    if (argc < 6)
+        return 0;
+    gline_add(origin, argv[3], atoi(argv[4])-now, argv[5], now, 0);
+    return 1;
+}
+
+static CMD_FUNC(cmd_quit)
+{
+    struct userNode *user;
+    if (argc < 2)
+        return 0;
+    /* Sometimes we get a KILL then a QUIT or the like, so we don't want to
+     * call DelUser unless we have the user in our grasp. */
+    if ((user = GetUserH(origin)))
+        DelUser(user, NULL, false, argv[1]);
+    return 1;
+}
+
+static CMD_FUNC(cmd_kill)
+{
+    struct userNode *user;
+    if (argc < 2)
+        return 0;
+    user = GetUserN(argv[1]);
+    if (!user) {
+        /* If we get a KILL for a non-existent user, it could be a
+         * Ghost response to a KILL we sent out earlier.  So we only
+         * whine if the target is local.
+         */
+        if (!strncmp(argv[1], self->numeric, strlen(self->numeric)))
+            log_module(MAIN_LOG, LOG_ERROR, "Unable to find kill victim %s", argv[1]);
+        return 0;
+    }
+
+    if (IsLocal(user) && IsService(user)) {
+        /* TODO: rate limit this so silly things don't happen. */
+        ReintroduceUser(user);
+        return 1;
+    }
+
+    DelUser(user, NULL, false, argv[2]);
+    return 1;
+}
+
+static CMD_FUNC(cmd_part)
+{
+    struct userNode *user;
+
+    if (argc < 2)
+        return 0;
+    user = GetUserH(origin);
+    if (!user)
+        return 0;
+    parse_foreach(argv[1], part_helper, NULL, NULL, NULL, user);
+    return 1;
+}
+
+static CMD_FUNC(cmd_kick)
+{
+    if (argc < 3)
+        return 0;
+    ChannelUserKicked(GetUserH(origin), GetUserN(argv[2]), GetChannel(argv[1]));
+    return 1;
+}
+
+static CMD_FUNC(cmd_squit)
+{
+    struct server *server;
+
+    if (argc < 4)
+        return 0;
+    if (!(server = GetServerH(argv[1])))
+        return 0;
+
+    if (server == self->uplink) {
+        /* Force a reconnect to the currently selected server. */
+        cManager.uplink->tries = 0;
+        log_module(MAIN_LOG, LOG_INFO, "Squitting from uplink: %s", argv[3]);
+        close_socket();
+        return 1;
+    }
+
+    DelServer(server, 0, argv[3]);
+    return 1;
+}
+
+static CMD_FUNC(cmd_privmsg)
+{
+    struct privmsg_desc pd;
+    if (argc != 3)
+        return 0;
+    pd.user = GetUserH(origin);
+    if (!pd.user || (IsGagged(pd.user) && !IsOper(pd.user)))
+        return 1;
+    pd.is_notice = 0;
+    pd.text = argv[2];
+    parse_foreach(argv[1], privmsg_chan_helper, NULL, privmsg_user_helper, privmsg_invalid, &pd);
+    return 1;
+}
+
+static CMD_FUNC(cmd_notice)
+{
+    struct privmsg_desc pd;
+    if (argc != 3)
+        return 0;
+    pd.user = GetUserH(origin);
+    if (!pd.user || (IsGagged(pd.user) && !IsOper(pd.user)))
+        return 1;
+    pd.is_notice = 1;
+    pd.text = argv[2];
+    parse_foreach(argv[1], privmsg_chan_helper, NULL, privmsg_user_helper, privmsg_invalid, &pd);
+    return 1;
+}
+
+static CMD_FUNC(cmd_away)
+{
+    struct userNode *uNode;
+
+    uNode = GetUserH(origin);
+    if (!uNode)
+        return 1;
+    if (argc < 2)
+        uNode->modes &= ~FLAGS_AWAY;
+    else
+        uNode->modes |= FLAGS_AWAY;
+    return 1;
+}
+
+static CMD_FUNC(cmd_gline)
+{
+    if (argc < 3)
+        return 0;
+    if (argv[2][0] == '+') {
+        if (argc < 5)
+            return 0;
+        gline_add(origin, argv[2]+1, strtoul(argv[3], NULL, 0), argv[argc-1], now, 0);
+        return 1;
+    } else if (argv[2][0] == '-') {
+        gline_remove(argv[2]+1, 0);
+        return 1;
+    } else
+        return 0;
+}
+
+static CMD_FUNC(cmd_svsnick)
+{
+    struct userNode *target, *dest;
+    if ((argc < 4)
+        || !(target = GetUserN(argv[1]))
+        || !IsLocal(target)
+        || (dest = GetUserH(argv[2])))
+        return 0;
+    NickChange(target, argv[2], 0);
+    return 1;
+}
+
+static oper_func_t *of_list;
+static unsigned int of_size = 0, of_used = 0;
+
+void
+free_user(struct userNode *user)
+{
+    free(user->nick);
+    free(user);
+}
+
+static void
+parse_cleanup(void)
+{
+    unsigned int nn;
+    free(of_list);
+    free(privmsg_funcs);
+    free(notice_funcs);
+    free(mcf_list);
+    dict_delete(irc_func_dict);
+    for (nn=0; nn<dead_users.used; nn++)
+        free_user(dead_users.list[nn]);
+    userList_clean(&dead_users);
+}
+
+static void
+p10_conf_reload(void) {
+    hidden_host_suffix = conf_get_data("server/hidden_host", RECDB_QSTRING);
+}
+
+static void
+remove_unbursted_channel(struct chanNode *cNode) {
+    if (unbursted_channels)
+        dict_remove(unbursted_channels, cNode->name);
+}
+
+void
+init_parse(void)
+{
+    const char *str, *desc;
+    int numnick, usermask, max_users;
+    char numer[COMBO_NUMERIC_LEN+1];
+
+    /* read config items */
+    str = conf_get_data("server/ping_freq", RECDB_QSTRING);
+    ping_freq = str ? ParseInterval(str) : 120;
+    str = conf_get_data("server/ping_timeout", RECDB_QSTRING);
+    ping_timeout = str ? ParseInterval(str) : 30;
+    str = conf_get_data("server/force_n2k", RECDB_QSTRING);
+    force_n2k = str ? enabled_string(str) : 1;
+    str = conf_get_data("server/numeric", RECDB_QSTRING);
+    if (!str) {
+        log_module(MAIN_LOG, LOG_ERROR, "No server/numeric entry in config file.");
+        exit(1);
+    }
+    numnick = atoi(str);
+    str = conf_get_data("server/max_users", RECDB_QSTRING);
+    max_users = str ? atoi(str) : 4096;
+    for (usermask = 4; usermask < max_users; usermask <<= 1) ;
+    usermask--;
+    if ((numnick < 64) && (usermask < 4096) && !force_n2k)
+        inttobase64(numer, (numnick << 12) + (usermask & 0x00fff), 3);
+    else
+        inttobase64(numer, (numnick << 18) + (usermask & 0x3ffff), 5);
+    str = conf_get_data("server/hostname", RECDB_QSTRING);
+    desc = conf_get_data("server/description", RECDB_QSTRING);
+    if (!str || !desc) {
+        log_module(MAIN_LOG, LOG_ERROR, "No server/hostname entry in config file.");
+        exit(1);
+    }
+    self = AddServer(NULL, str, 0, boot_time, now, numer, desc);
+    conf_register_reload(p10_conf_reload);
+
+    irc_func_dict = dict_new();
+    dict_insert(irc_func_dict, CMD_BURST, cmd_burst);
+    dict_insert(irc_func_dict, TOK_BURST, cmd_burst);
+    dict_insert(irc_func_dict, CMD_CREATE, cmd_create);
+    dict_insert(irc_func_dict, TOK_CREATE, cmd_create);
+    dict_insert(irc_func_dict, CMD_EOB, cmd_eob);
+    dict_insert(irc_func_dict, TOK_EOB, cmd_eob);
+    dict_insert(irc_func_dict, CMD_EOB_ACK, cmd_eob_ack);
+    dict_insert(irc_func_dict, TOK_EOB_ACK, cmd_eob_ack);
+    dict_insert(irc_func_dict, CMD_MODE, cmd_mode);
+    dict_insert(irc_func_dict, TOK_MODE, cmd_mode);
+    dict_insert(irc_func_dict, CMD_NICK, cmd_nick);
+    dict_insert(irc_func_dict, TOK_NICK, cmd_nick);
+    dict_insert(irc_func_dict, CMD_ACCOUNT, cmd_account);
+    dict_insert(irc_func_dict, TOK_ACCOUNT, cmd_account);
+    dict_insert(irc_func_dict, CMD_PASS, cmd_pass);
+    dict_insert(irc_func_dict, TOK_PASS, cmd_pass);
+    dict_insert(irc_func_dict, CMD_PING, cmd_ping);
+    dict_insert(irc_func_dict, TOK_PING, cmd_ping);
+    dict_insert(irc_func_dict, CMD_PRIVMSG, cmd_privmsg);
+    dict_insert(irc_func_dict, TOK_PRIVMSG, cmd_privmsg);
+    dict_insert(irc_func_dict, CMD_PONG, cmd_pong);
+    dict_insert(irc_func_dict, TOK_PONG, cmd_pong);
+    dict_insert(irc_func_dict, CMD_QUIT, cmd_quit);
+    dict_insert(irc_func_dict, TOK_QUIT, cmd_quit);
+    dict_insert(irc_func_dict, CMD_SERVER, cmd_server);
+    dict_insert(irc_func_dict, TOK_SERVER, cmd_server);
+    dict_insert(irc_func_dict, CMD_JOIN, cmd_join);
+    dict_insert(irc_func_dict, TOK_JOIN, cmd_join);
+    dict_insert(irc_func_dict, CMD_PART, cmd_part);
+    dict_insert(irc_func_dict, TOK_PART, cmd_part);
+    dict_insert(irc_func_dict, CMD_ERROR, cmd_error);
+    dict_insert(irc_func_dict, TOK_ERROR, cmd_error);
+    dict_insert(irc_func_dict, CMD_TOPIC, cmd_topic);
+    dict_insert(irc_func_dict, TOK_TOPIC, cmd_topic);
+    dict_insert(irc_func_dict, CMD_AWAY, cmd_away);
+    dict_insert(irc_func_dict, TOK_AWAY, cmd_away);
+    dict_insert(irc_func_dict, CMD_SILENCE, cmd_dummy);
+    dict_insert(irc_func_dict, TOK_SILENCE, cmd_dummy);
+    dict_insert(irc_func_dict, CMD_KICK, cmd_kick);
+    dict_insert(irc_func_dict, TOK_KICK, cmd_kick);
+    dict_insert(irc_func_dict, CMD_SQUIT, cmd_squit);
+    dict_insert(irc_func_dict, TOK_SQUIT, cmd_squit);
+    dict_insert(irc_func_dict, CMD_KILL, cmd_kill);
+    dict_insert(irc_func_dict, TOK_KILL, cmd_kill);
+    dict_insert(irc_func_dict, CMD_NOTICE, cmd_notice);
+    dict_insert(irc_func_dict, TOK_NOTICE, cmd_notice);
+    dict_insert(irc_func_dict, CMD_STATS, cmd_stats);
+    dict_insert(irc_func_dict, TOK_STATS, cmd_stats);
+    dict_insert(irc_func_dict, CMD_SVSNICK, cmd_svsnick);
+    dict_insert(irc_func_dict, TOK_SVSNICK, cmd_svsnick);
+    dict_insert(irc_func_dict, CMD_WHOIS, cmd_whois);
+    dict_insert(irc_func_dict, TOK_WHOIS, cmd_whois);
+    dict_insert(irc_func_dict, CMD_GLINE, cmd_gline);
+    dict_insert(irc_func_dict, TOK_GLINE, cmd_gline);
+    dict_insert(irc_func_dict, CMD_OPMODE, cmd_opmode);
+    dict_insert(irc_func_dict, TOK_OPMODE, cmd_opmode);
+    dict_insert(irc_func_dict, CMD_CLEARMODE, cmd_clearmode);
+    dict_insert(irc_func_dict, TOK_CLEARMODE, cmd_clearmode);
+    dict_insert(irc_func_dict, CMD_VERSION, cmd_version);
+    dict_insert(irc_func_dict, TOK_VERSION, cmd_version);
+    dict_insert(irc_func_dict, CMD_ADMIN, cmd_admin);
+    dict_insert(irc_func_dict, TOK_ADMIN, cmd_admin);
+
+    /* In P10, DESTRUCT doesn't do anything except be broadcast to servers.
+     * Apparently to obliterate channels from any servers that think they
+     * exist?
+     */
+    dict_insert(irc_func_dict, CMD_DESTRUCT, cmd_dummy);
+    dict_insert(irc_func_dict, TOK_DESTRUCT, cmd_dummy);
+    /* Ignore invites */
+    dict_insert(irc_func_dict, CMD_INVITE, cmd_dummy);
+    dict_insert(irc_func_dict, TOK_INVITE, cmd_dummy);
+    /* DESYNCH is just informational, so ignore it */
+    dict_insert(irc_func_dict, CMD_DESYNCH, cmd_dummy);
+    dict_insert(irc_func_dict, TOK_DESYNCH, cmd_dummy);
+    /* Ignore channel operator notices. */
+    dict_insert(irc_func_dict, CMD_WALLCHOPS, cmd_dummy);
+    dict_insert(irc_func_dict, TOK_WALLCHOPS, cmd_dummy);
+    dict_insert(irc_func_dict, CMD_WALLVOICES, cmd_dummy);
+    dict_insert(irc_func_dict, TOK_WALLVOICES, cmd_dummy);
+    /* Ignore opers being silly. */
+    dict_insert(irc_func_dict, CMD_WALLOPS, cmd_dummy);
+    dict_insert(irc_func_dict, TOK_WALLOPS, cmd_dummy);
+    /* We have reliable clock!  Always!  Wraaa! */
+    dict_insert(irc_func_dict, CMD_SETTIME, cmd_dummy);
+    dict_insert(irc_func_dict, TOK_SETTIME, cmd_dummy);
+    /* handle topics */
+    dict_insert(irc_func_dict, "331", cmd_num_topic);
+    dict_insert(irc_func_dict, "332", cmd_num_topic);
+    dict_insert(irc_func_dict, "333", cmd_num_topic);
+    dict_insert(irc_func_dict, "432", cmd_error_nick); /* Erroneus [sic] nickname */
+    /* ban list resetting */
+    /* "stats g" responses */
+    dict_insert(irc_func_dict, "247", cmd_num_gline);
+    dict_insert(irc_func_dict, "219", cmd_dummy); /* "End of /STATS report" */
+    /* other numeric responses we might get */
+    dict_insert(irc_func_dict, "401", cmd_dummy); /* target left network */
+    dict_insert(irc_func_dict, "403", cmd_dummy); /* no such channel */
+    dict_insert(irc_func_dict, "404", cmd_dummy); /* cannot send to channel */
+    dict_insert(irc_func_dict, "441", cmd_dummy); /* target isn't on that channel */
+    dict_insert(irc_func_dict, "442", cmd_dummy); /* you aren't on that channel */
+    dict_insert(irc_func_dict, "443", cmd_dummy); /* is already on channel (after invite?) */
+    dict_insert(irc_func_dict, "461", cmd_dummy); /* Not enough parameters (after TOPIC w/ 0 args) */
+
+    num_privmsg_funcs = 16;
+    privmsg_funcs = malloc(sizeof(privmsg_func_t)*num_privmsg_funcs);
+    memset(privmsg_funcs, 0, sizeof(privmsg_func_t)*num_privmsg_funcs);
+
+    num_notice_funcs = 16;
+    notice_funcs = malloc(sizeof(privmsg_func_t)*num_notice_funcs);
+    memset(notice_funcs, 0, sizeof(privmsg_func_t)*num_notice_funcs);
+
+    userList_init(&dead_users);
+    reg_del_channel_func(remove_unbursted_channel);
+    reg_exit_func(parse_cleanup);
+}
+
+int
+parse_line(char *line, int recursive)
+{
+    char *argv[MAXNUMPARAMS], *origin;
+    int argc, cmd, res=0;
+    cmd_func_t *func;
+
+    argc = split_line(line, true, MAXNUMPARAMS, argv);
+    cmd = self->uplink || !argv[0][1] || !argv[0][2];
+    if (argc > cmd) {
+        if (cmd) {
+            if (argv[0][0] == ':') {
+                origin = argv[0]+1;
+            } else if (!argv[0][1] || !argv[0][2]) {
+                struct server *sNode = GetServerN(argv[0]);
+                origin = sNode ? sNode->name : 0;
+            } else {
+                struct userNode *uNode = GetUserN(argv[0]);
+                origin = uNode ? uNode->nick : 0;
+            }
+        } else
+            origin = 0;
+        if ((func = dict_find(irc_func_dict, argv[cmd], NULL)))
+            res = func(origin, argc-cmd, argv+cmd);
+    }
+    if (!res) {
+        log_module(MAIN_LOG, LOG_ERROR, "PARSE ERROR on line: %s", unsplit_string(argv, argc, NULL));
+    } else if (!recursive) {
+        unsigned int i;
+        for (i=0; i<dead_users.used; i++)
+            free_user(dead_users.list[i]);
+        dead_users.used = 0;
+    }
+    return res;
+}
+
+static void
+parse_foreach(char *target_list, foreach_chanfunc cf, foreach_nonchan nc, foreach_userfunc uf, foreach_nonuser nu, void *data)
+{
+    char *j, old;
+    do {
+        j = target_list;
+        while (*j != 0 && *j != ',')
+            j++;
+        old = *j;
+        *j = 0;
+        if (IsChannelName(target_list)
+            || (target_list[0] == '0' && target_list[1] == '\0')) {
+            struct chanNode *chan = GetChannel(target_list);
+            if (chan) {
+                if (cf)
+                    cf(chan, data);
+            } else {
+                if (nc)
+                    nc(target_list, data);
+            }
+        } else {
+            struct userNode *user;
+            struct privmsg_desc *pd = data;
+
+            pd->is_qualified = 0;
+            if (*target_list == '@') {
+                user = NULL;
+            } else if (strchr(target_list, '@')) {
+                struct server *server;
+
+                pd->is_qualified = 1;
+                user = GetUserH(strtok(target_list, "@"));
+                server = GetServerH(strtok(NULL, "@"));
+
+                if (user && (user->uplink != server)) {
+                    /* Don't attempt to index into any arrays
+                       using a user's numeric on another server. */
+                    user = NULL;
+                }
+            } else {
+                user = GetUserN(target_list);
+            }
+
+            if (user) {
+                if (uf)
+                    uf(user, data);
+            } else {
+                if (nu)
+                    nu(target_list, data);
+            }
+        }
+        target_list = j+1;
+    } while (old == ',');
+}
+
+static int
+get_local_numeric(void)
+{
+    static unsigned int next_numeric = 0;
+    if (self->clients > self->num_mask)
+        return -1;
+    while (self->users[next_numeric])
+        if (++next_numeric > self->num_mask)
+            next_numeric = 0;
+    return next_numeric;
+}
+
+static void
+make_numeric(struct server *svr, int local_num, char *outbuf)
+{
+    int slen, llen;
+
+    if (force_n2k || svr->numeric[1]) {
+        slen = 2;
+        llen = 3;
+    } else {
+        slen = 1;
+        llen = (local_num < 64*64) ? 2 : 3;
+    }
+    strncpy(outbuf, svr->numeric, slen);
+    inttobase64(outbuf+slen, local_num, llen);
+    outbuf[slen+llen] = 0;
+}
+
+struct server *
+AddServer(struct server *uplink, const char *name, int hops, time_t boot, time_t link, const char *numeric, const char *description)
+{
+    struct server* sNode;
+    int slen, mlen;
+
+    if ((sNode = GetServerN(numeric))) {
+        /* This means we're trying to re-add an existant server.
+         * To be safe, we should forget the previous incarnation.
+         * (And all its linked servers.)
+         *
+         * It usually only happens in replays when the original
+         * had a ping timeout and the replay didn't (because
+         * replaying a ping timeout invariably gets things wrong).
+         */
+        DelServer(sNode, 0, NULL);
+    }
+
+    switch (strlen(numeric)) {
+    case 5: slen = 2; mlen = 3; break;
+    case 4: slen = 1; mlen = 3; break;
+    case 3: slen = 1; mlen = 2; break;
+    default:
+        log_module(MAIN_LOG, LOG_ERROR, "AddServer(\"%s\", \"%s\", ...): Numeric %s has invalid length.", uplink->name, name, numeric);
+        return NULL;
+    }
+
+    sNode = calloc(1, sizeof(*sNode));
+    sNode->uplink = uplink;
+    safestrncpy(sNode->name, name, sizeof(sNode->name));
+    sNode->num_mask = base64toint(numeric+slen, mlen);
+    sNode->hops = hops;
+    sNode->boot = boot;
+    sNode->link = link;
+    strncpy(sNode->numeric, numeric, slen);
+    safestrncpy(sNode->description, description, sizeof(sNode->description));
+    sNode->users = calloc(sNode->num_mask+1, sizeof(*sNode->users));
+    serverList_init(&sNode->children);
+    if (sNode->uplink) {
+        /* uplink may be NULL if we're just building ourself */
+        serverList_append(&sNode->uplink->children, sNode);
+    }
+    servers_num[base64toint(numeric, slen)] = sNode;
+    dict_insert(servers, sNode->name, sNode);
+    return sNode;
+}
+
+void DelServer(struct server* serv, int announce, const char *message)
+{
+    unsigned int i;
+
+    /* If we receive an ERROR command before the SERVER
+     * command a NULL server can be passed */
+    if (!serv)
+        return;
+
+    /* Hrm, what's the right way to SQUIT some other server?
+     * (This code is only to handle killing juped servers.) */
+    if (announce && (serv->uplink == self) && (serv != self->uplink))
+        irc_squit(serv, message, NULL);
+
+    /* must recursively remove servers linked to this one first */
+    for (i=serv->children.used;i>0;)
+        if (serv->children.list[--i] != self)
+            DelServer(serv->children.list[i], false, NULL);
+
+    /* clean up server's user hash tables */
+    for (i=0;i<=serv->num_mask;i++)
+        if (serv->users[i])
+            DelUser(serv->users[i], NULL, false, "server delinked");
+
+    /* delete server */
+    if (serv->uplink)
+        serverList_remove(&serv->uplink->children, serv);
+    if (serv == self->uplink)
+        self->uplink = NULL;
+    servers_num[base64toint(serv->numeric, strlen(serv->numeric))] = NULL;
+    dict_remove(servers, serv->name);
+    serverList_clean(&serv->children);
+    free(serv->users);
+    free(serv);
+}
+
+struct userNode *
+AddService(const char *nick, const char *desc)
+{
+    char numeric[COMBO_NUMERIC_LEN+1];
+    int local_num = get_local_numeric();
+    time_t timestamp = now;
+    struct userNode *old_user = GetUserH(nick);
+
+    if (old_user) {
+        if (IsLocal(old_user))
+            return old_user;
+        timestamp = old_user->timestamp - 1;
+    }
+    if (local_num == -1) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to allocate numnick for service %s", nick);
+        return 0;
+    }
+    make_numeric(self, local_num, numeric);
+    return AddUser(self, nick, nick, self->name, "+oik", numeric, desc, now, "AAAAAA");
+}
+
+struct userNode *
+AddClone(const char *nick, const char *ident, const char *hostname, const char *desc)
+{
+    char numeric[COMBO_NUMERIC_LEN+1];
+    int local_num = get_local_numeric();
+    time_t timestamp = now;
+    struct userNode *old_user = GetUserH(nick);
+
+    if (old_user) {
+        if (IsLocal(old_user))
+            return old_user;
+        timestamp = old_user->timestamp - 1;
+    }
+    if (local_num == -1) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to allocate numnick for clone %s", nick);
+        return 0;
+    }
+    make_numeric(self, local_num, numeric);
+    return AddUser(self, nick, ident, hostname, "+i", numeric, desc, timestamp, "AAAAAA");
+}
+
+int
+is_valid_nick(const char *nick) {
+    /* IRC has some of The Most Fucked-Up ideas about character sets
+     * in the world.. */
+    if (!isalpha(*nick) && !strchr("{|}~[\\]^_`", *nick))
+        return 0;
+    for (++nick; *nick; ++nick)
+        if (!isalnum(*nick) && !strchr("{|}~[\\]^-_`", *nick))
+            return 0;
+    if (strlen(nick) > nicklen)
+        return 0;
+    return 1;
+}
+
+static struct userNode*
+AddUser(struct server* uplink, const char *nick, const char *ident, const char *hostname, const char *modes, const char *numeric, const char *userinfo, time_t timestamp, const char *realip)
+{
+    struct userNode *oldUser, *uNode;
+    unsigned int n, ignore_user;
+
+    if ((strlen(numeric) < 3) || (strlen(numeric) > 5)) {
+        log_module(MAIN_LOG, LOG_WARNING, "AddUser(%p, %s, ...): numeric %s wrong length!", uplink, nick, numeric);
+        return NULL;
+    }
+
+    if (!uplink) {
+        log_module(MAIN_LOG, LOG_WARNING, "AddUser(%p, %s, ...): server for numeric %s doesn't exist!", uplink, nick, numeric);
+        return NULL;
+    }
+
+    if (uplink != GetServerN(numeric)) {
+        log_module(MAIN_LOG, LOG_WARNING, "AddUser(%p, %s, ...): server for numeric %s differs from nominal uplink %s.", uplink, nick, numeric, uplink->name);
+        return NULL;
+    }
+
+    if (!is_valid_nick(nick)) {
+        log_module(MAIN_LOG, LOG_WARNING, "AddUser(%p, %s, ...): invalid nickname detected.", uplink, nick);
+        return NULL;
+    }
+
+    ignore_user = 0;
+    if ((oldUser = GetUserH(nick))) {
+        if (IsLocal(oldUser) && (IsService(oldUser) || IsPersistent(oldUser))) {
+            /* The service should collide the new user off. */
+            oldUser->timestamp = timestamp - 1;
+            irc_user(oldUser);
+        }
+        if (oldUser->timestamp > timestamp) {
+            /* "Old" user is really newer; remove them */
+            DelUser(oldUser, 0, 1, "Overruled by older nick");
+        } else {
+            /* User being added is too new; do not add them to
+             * clients, but do add them to the server's list, since it
+             * will send a KILL and QUIT soon. */
+            ignore_user = 1;
+        }
+    }
+
+    /* create new usernode and set all values */
+    uNode = calloc(1, sizeof(*uNode));
+    uNode->nick = strdup(nick);
+    safestrncpy(uNode->ident, ident, sizeof(uNode->ident));
+    safestrncpy(uNode->info, userinfo, sizeof(uNode->info));
+    safestrncpy(uNode->hostname, hostname, sizeof(uNode->hostname));
+    safestrncpy(uNode->numeric, numeric, sizeof(uNode->numeric));
+    uNode->ip.s_addr = htonl(base64toint(realip, 6));
+    uNode->timestamp = timestamp;
+    modeList_init(&uNode->channels);
+    uNode->uplink = uplink;
+    if (++uNode->uplink->clients > uNode->uplink->max_clients) {
+        uNode->uplink->max_clients = uNode->uplink->clients;
+    }
+    uNode->num_local = base64toint(numeric+strlen(uNode->uplink->numeric), 3) & uNode->uplink->num_mask;
+    uNode->uplink->users[uNode->num_local] = uNode;
+    mod_usermode(uNode, modes);
+    if (ignore_user)
+        return uNode;
+
+    dict_insert(clients, uNode->nick, uNode);
+    if (dict_size(clients) > max_clients) {
+        max_clients = dict_size(clients);
+        max_clients_time = now;
+    }
+    if (IsLocal(uNode))
+        irc_user(uNode);
+    for (n=0; n<nuf_used; n++)
+        if (nuf_list[n](uNode))
+            break;
+    return uNode;
+}
+
+/* removes user from it's server's hash table and nick hash table */
+void
+DelUser(struct userNode* user, struct userNode *killer, int announce, const char *why)
+{
+    unsigned int n;
+
+    /* mark them as dead, in case anybody cares */
+    user->dead = 1;
+
+    /* remove user from all channels */
+    while (user->channels.used > 0)
+        DelChannelUser(user, user->channels.list[user->channels.used-1]->channel, false, 0);
+
+    /* Call these in reverse order so ChanServ can update presence
+       information before NickServ nukes the handle_info. */
+    for (n = duf_used; n > 0; )
+        duf_list[--n](user, killer, why);
+
+    user->uplink->clients--;
+    user->uplink->users[user->num_local] = NULL;
+    if (IsOper(user))
+        userList_remove(&curr_opers, user);
+    /* remove from global dictionary, but not if after a collide */
+    if (user == dict_find(clients, user->nick, NULL))
+        dict_remove(clients, user->nick);
+
+    if (IsInvisible(user))
+        invis_clients--;
+
+    if (announce) {
+        if (IsLocal(user))
+            irc_quit(user, why);
+        else
+            irc_kill(killer, user, why);
+    }
+
+    modeList_clean(&user->channels);
+    /* We don't free them, in case we try to privmsg them or something
+     * (like when a stupid oper kills themself).  We just put them onto
+     * a list of clients that get freed after processing each line.
+     */
+    if (dead_users.size)
+        userList_append(&dead_users, user);
+    else
+        free_user(user);
+}
+
+void mod_usermode(struct userNode *user, const char *mode_change) {
+    static void call_oper_funcs(struct userNode *user);
+    int add = 1;
+    const char *word = mode_change;
+
+    if (!user || !mode_change)
+        return;
+    while (*word != ' ' && *word) word++;\
+    while (*word == ' ') word++; \
+    while (1) {
+#define do_user_mode(FLAG) do { if (add) user->modes |= FLAG; else user->modes &= ~FLAG; } while (0)
+       switch (*mode_change++) {
+       case 0: case ' ': return;
+       case '+': add = 1; break;
+       case '-': add = 0; break;
+       case 'o':
+           do_user_mode(FLAGS_OPER);
+           if (add) {
+               userList_append(&curr_opers, user);
+               call_oper_funcs(user);
+           } else {
+               userList_remove(&curr_opers, user);
+           }
+           break;
+       case 'O': do_user_mode(FLAGS_LOCOP); break;
+       case 'i': do_user_mode(FLAGS_INVISIBLE);
+           if (add)
+                invis_clients++;
+            else
+                invis_clients--;
+           break;
+       case 'w': do_user_mode(FLAGS_WALLOP); break;
+       case 's': do_user_mode(FLAGS_SERVNOTICE); break;
+       case 'd': do_user_mode(FLAGS_DEAF); break;
+       case 'k': do_user_mode(FLAGS_SERVICE); break;
+       case 'g': do_user_mode(FLAGS_GLOBAL); break;
+       case 'h': do_user_mode(FLAGS_HELPER); break;
+        case 'x': do_user_mode(FLAGS_HIDDEN_HOST); break;
+        case 'r':
+            if (*word) {
+                char tag[MAXLEN];
+                unsigned int ii;
+                for (ii=0; (*word != ' ') && (*word != '\0'); )
+                    tag[ii++] = *word++;
+                tag[ii] = 0;
+                while (*word == ' ')
+                    word++;
+                call_account_func(user, tag);
+            }
+            break;
+       }
+#undef do_user_mode
+    }
+}
+
+struct mod_chanmode *
+mod_chanmode_parse(struct chanNode *channel, char **modes, unsigned int argc, unsigned int flags)
+{
+    struct mod_chanmode *change;
+    unsigned int ii, in_arg, ch_arg, add;
+
+    if (argc == 0)
+        return NULL;
+    if (!(change = mod_chanmode_alloc(argc - 1)))
+        return NULL;
+
+    for (ii = ch_arg = 0, in_arg = add = 1;
+         (modes[0][ii] != '\0') && (modes[0][ii] != ' ');
+         ++ii) {
+        switch (modes[0][ii]) {
+        case '+':
+            add = 1;
+            break;
+        case '-':
+            add = 0;
+            break;
+#define do_chan_mode(FLAG) do { if (add) change->modes_set |= FLAG, change->modes_clear &= ~FLAG; else change->modes_clear |= FLAG, change->modes_set &= ~FLAG; } while(0)
+        case 'C': do_chan_mode(MODE_NOCTCPS); break;
+        case 'D': do_chan_mode(MODE_DELAYJOINS); break;
+        case 'c': do_chan_mode(MODE_NOCOLORS); break;
+        case 'i': do_chan_mode(MODE_INVITEONLY); break;
+        case 'm': do_chan_mode(MODE_MODERATED); break;
+        case 'n': do_chan_mode(MODE_NOPRIVMSGS); break;
+        case 'p': do_chan_mode(MODE_PRIVATE); break;
+        case 'r': do_chan_mode(MODE_REGONLY); break;
+        case 's': do_chan_mode(MODE_SECRET); break;
+        case 't': do_chan_mode(MODE_TOPICLIMIT); break;
+#undef do_chan_mode
+        case 'l':
+            if (add) {
+                if (in_arg >= argc)
+                    goto error;
+                change->modes_set |= MODE_LIMIT;
+                change->new_limit = atoi(modes[in_arg++]);
+            } else {
+                change->modes_set &= ~MODE_LIMIT;
+                change->modes_clear |= MODE_LIMIT;
+            }
+            break;
+        case 'k':
+            if (add) {
+                if (in_arg >= argc)
+                    goto error;
+                change->modes_set |= MODE_KEY;
+                safestrncpy(change->new_key, modes[in_arg++], sizeof(change->new_key));
+            } else {
+                change->modes_clear |= MODE_KEY;
+                if (!(flags & MCP_KEY_FREE)) {
+                    if (in_arg >= argc)
+                        goto error;
+                    in_arg++;
+                }
+            }
+            break;
+        case 'b':
+            if (!(flags & MCP_ALLOW_OVB))
+                goto error;
+            if (in_arg >= argc)
+                goto error;
+            change->args[ch_arg].mode = MODE_BAN;
+            if (!add)
+                change->args[ch_arg].mode |= MODE_REMOVE;
+            change->args[ch_arg++].hostmask = modes[in_arg++];
+            break;
+        case 'o': case 'v':
+        {
+            struct userNode *victim;
+            if (!(flags & MCP_ALLOW_OVB))
+                goto error;
+            if (in_arg >= argc)
+                goto error;
+            change->args[ch_arg].mode = (modes[0][ii] == 'o') ? MODE_CHANOP : MODE_VOICE;
+            if (!add)
+                change->args[ch_arg].mode |= MODE_REMOVE;
+            if (flags & MCP_FROM_SERVER)
+                victim = GetUserN(modes[in_arg++]);
+            else
+                victim = GetUserH(modes[in_arg++]);
+            if ((change->args[ch_arg].member = GetUserMode(channel, victim)))
+                ch_arg++;
+            break;
+        }
+        }
+    }
+    change->argc = ch_arg; /* in case any turned out to be ignored */
+    return change;
+  error:
+    mod_chanmode_free(change);
+    return NULL;
+}
+
+struct chanmode_buffer {
+    char modes[MAXLEN];
+    char args[MAXLEN];
+    struct chanNode *channel;
+    struct userNode *actor;
+    unsigned int modes_used;
+    unsigned int args_used;
+    size_t chname_len;
+    unsigned int is_add : 1;
+    unsigned int is_chanop : 1;
+};
+
+static void
+mod_chanmode_append(struct chanmode_buffer *buf, char ch, const char *arg)
+{
+    size_t arg_len = strlen(arg);
+    if (buf->modes_used + buf->args_used + buf->chname_len + arg_len > 450) {
+        memcpy(buf->modes + buf->modes_used, buf->args, buf->args_used);
+        buf->modes[buf->modes_used + buf->args_used] = '\0';
+        irc_mode((buf->is_chanop ? buf->actor : NULL), buf->channel, buf->modes);
+        buf->modes[0] = buf->is_add ? '+' : '-';
+        buf->modes_used = 1;
+        buf->args_used = 0;
+    }
+    buf->modes[buf->modes_used++] = ch;
+    buf->args[buf->args_used++] = ' ';
+    memcpy(buf->args + buf->args_used, arg, arg_len);
+    buf->args_used += arg_len;
+}
+
+void
+mod_chanmode_announce(struct userNode *who, struct chanNode *channel, struct mod_chanmode *change)
+{
+    struct chanmode_buffer chbuf;
+    unsigned int arg;
+    struct modeNode *mn;
+    char int_buff[32], mode = '\0';
+
+    memset(&chbuf, 0, sizeof(chbuf));
+    chbuf.channel = channel;
+    chbuf.actor = who;
+    chbuf.chname_len = strlen(channel->name);
+    if ((mn = GetUserMode(channel, who)) && (mn->modes & MODE_CHANOP))
+        chbuf.is_chanop = 1;
+
+    /* First remove modes */
+    chbuf.is_add = 0;
+    if (change->modes_clear) {
+        if (mode != '-')
+            chbuf.modes[chbuf.modes_used++] = mode = '-';
+#define DO_MODE_CHAR(BIT, CHAR) if (change->modes_clear & MODE_##BIT) chbuf.modes[chbuf.modes_used++] = CHAR
+        DO_MODE_CHAR(PRIVATE, 'p');
+        DO_MODE_CHAR(SECRET, 's');
+        DO_MODE_CHAR(MODERATED, 'm');
+        DO_MODE_CHAR(TOPICLIMIT, 't');
+        DO_MODE_CHAR(INVITEONLY, 'i');
+        DO_MODE_CHAR(NOPRIVMSGS, 'n');
+        DO_MODE_CHAR(LIMIT, 'l');
+        DO_MODE_CHAR(DELAYJOINS, 'D');
+        DO_MODE_CHAR(REGONLY, 'r');
+        DO_MODE_CHAR(NOCOLORS, 'c');
+        DO_MODE_CHAR(NOCTCPS, 'C');
+#undef DO_MODE_CHAR
+        if (change->modes_clear & channel->modes & MODE_KEY)
+            mod_chanmode_append(&chbuf, 'k', channel->key);
+    }
+    for (arg = 0; arg < change->argc; ++arg) {
+        if (!(change->args[arg].mode & MODE_REMOVE))
+            continue;
+        if (mode != '-')
+            chbuf.modes[chbuf.modes_used++] = mode = '-';
+        switch (change->args[arg].mode & ~MODE_REMOVE) {
+        case MODE_BAN:
+            mod_chanmode_append(&chbuf, 'b', change->args[arg].hostmask);
+            break;
+        default:
+            if (change->args[arg].mode & MODE_CHANOP)
+                mod_chanmode_append(&chbuf, 'o', change->args[arg].member->user->numeric);
+            if (change->args[arg].mode & MODE_VOICE)
+                mod_chanmode_append(&chbuf, 'v', change->args[arg].member->user->numeric);
+            break;
+        }
+    }
+
+    /* Then set them */
+    chbuf.is_add = 1;
+    if (change->modes_set) {
+        if (mode != '+')
+            chbuf.modes[chbuf.modes_used++] = mode = '+';
+#define DO_MODE_CHAR(BIT, CHAR) if (change->modes_set & MODE_##BIT) chbuf.modes[chbuf.modes_used++] = CHAR
+        DO_MODE_CHAR(PRIVATE, 'p');
+        DO_MODE_CHAR(SECRET, 's');
+        DO_MODE_CHAR(MODERATED, 'm');
+        DO_MODE_CHAR(TOPICLIMIT, 't');
+        DO_MODE_CHAR(INVITEONLY, 'i');
+        DO_MODE_CHAR(NOPRIVMSGS, 'n');
+        DO_MODE_CHAR(DELAYJOINS, 'D');
+        DO_MODE_CHAR(REGONLY, 'r');
+        DO_MODE_CHAR(NOCOLORS, 'c');
+        DO_MODE_CHAR(NOCTCPS, 'C');
+#undef DO_MODE_CHAR
+        if(change->modes_set & MODE_KEY)
+            mod_chanmode_append(&chbuf, 'k', change->new_key);
+        if(change->modes_set & MODE_LIMIT) {
+            sprintf(int_buff, "%d", change->new_limit);
+            mod_chanmode_append(&chbuf, 'l', int_buff);
+        }
+    }
+    for (arg = 0; arg < change->argc; ++arg) {
+        if (change->args[arg].mode & MODE_REMOVE)
+            continue;
+        if (mode != '+')
+            chbuf.modes[chbuf.modes_used++] = mode = '+';
+        switch (change->args[arg].mode) {
+        case MODE_BAN:
+            mod_chanmode_append(&chbuf, 'b', change->args[arg].hostmask);
+            break;
+        default:
+            if (change->args[arg].mode & MODE_CHANOP)
+                mod_chanmode_append(&chbuf, 'o', change->args[arg].member->user->numeric);
+            if (change->args[arg].mode & MODE_VOICE)
+                mod_chanmode_append(&chbuf, 'v', change->args[arg].member->user->numeric);
+            break;
+        }
+    }
+
+    /* Flush the buffer and apply changes locally */
+    if (chbuf.modes_used > 0) {
+        memcpy(chbuf.modes + chbuf.modes_used, chbuf.args, chbuf.args_used);
+        chbuf.modes[chbuf.modes_used + chbuf.args_used] = '\0';
+        irc_mode((chbuf.is_chanop ? chbuf.actor : NULL), chbuf.channel, chbuf.modes);
+    }
+    mod_chanmode_apply(who, channel, change);
+}
+
+char *
+mod_chanmode_format(struct mod_chanmode *change, char *outbuff)
+{
+    unsigned int used = 0;
+    if (change->modes_clear) {
+        outbuff[used++] = '-';
+#define DO_MODE_CHAR(BIT, CHAR) if (change->modes_clear & MODE_##BIT) outbuff[used++] = CHAR
+        DO_MODE_CHAR(PRIVATE, 'p');
+        DO_MODE_CHAR(SECRET, 's');
+        DO_MODE_CHAR(MODERATED, 'm');
+        DO_MODE_CHAR(TOPICLIMIT, 't');
+        DO_MODE_CHAR(INVITEONLY, 'i');
+        DO_MODE_CHAR(NOPRIVMSGS, 'n');
+        DO_MODE_CHAR(LIMIT, 'l');
+        DO_MODE_CHAR(KEY, 'k');
+        DO_MODE_CHAR(DELAYJOINS, 'D');
+        DO_MODE_CHAR(REGONLY, 'r');
+        DO_MODE_CHAR(NOCOLORS, 'c');
+        DO_MODE_CHAR(NOCTCPS, 'C');
+#undef DO_MODE_CHAR
+    }
+    if (change->modes_set) {
+        outbuff[used++] = '+';
+#define DO_MODE_CHAR(BIT, CHAR) if (change->modes_set & MODE_##BIT) outbuff[used++] = CHAR
+        DO_MODE_CHAR(PRIVATE, 'p');
+        DO_MODE_CHAR(SECRET, 's');
+        DO_MODE_CHAR(MODERATED, 'm');
+        DO_MODE_CHAR(TOPICLIMIT, 't');
+        DO_MODE_CHAR(INVITEONLY, 'i');
+        DO_MODE_CHAR(NOPRIVMSGS, 'n');
+        DO_MODE_CHAR(DELAYJOINS, 'D');
+        DO_MODE_CHAR(REGONLY, 'r');
+        DO_MODE_CHAR(NOCOLORS, 'c');
+        DO_MODE_CHAR(NOCTCPS, 'C');
+#undef DO_MODE_CHAR
+        switch (change->modes_set & (MODE_KEY|MODE_LIMIT)) {
+        case MODE_KEY|MODE_LIMIT:
+            used += sprintf(outbuff+used, "lk %d %s", change->new_limit, change->new_key);
+            break;
+        case MODE_KEY:
+            used += sprintf(outbuff+used, "k %s", change->new_key);
+            break;
+        case MODE_LIMIT:
+            used += sprintf(outbuff+used, "l %d", change->new_limit);
+            break;
+        }
+    }
+    outbuff[used] = 0;
+    return outbuff;
+}
+
+static int
+clear_chanmode(struct chanNode *channel, const char *modes)
+{
+    unsigned int remove;
+
+    for (remove = 0; *modes; modes++) {
+        switch (*modes) {
+        case 'o': remove |= MODE_CHANOP; break;
+        case 'v': remove |= MODE_VOICE; break;
+        case 'p': remove |= MODE_PRIVATE; break;
+        case 's': remove |= MODE_SECRET; break;
+        case 'm': remove |= MODE_MODERATED; break;
+        case 't': remove |= MODE_TOPICLIMIT; break;
+        case 'i': remove |= MODE_INVITEONLY; break;
+        case 'n': remove |= MODE_NOPRIVMSGS; break;
+        case 'k':
+            remove |= MODE_KEY;
+            channel->key[0] = '\0';
+            break;
+        case 'l':
+            remove |= MODE_LIMIT;
+            channel->limit = 0;
+            break;
+        case 'b': remove |= MODE_BAN; break;
+        case 'D': remove |= MODE_DELAYJOINS; break;
+        case 'r': remove |= MODE_REGONLY; break;
+        case 'c': remove |= MODE_NOCOLORS;
+        case 'C': remove |= MODE_NOCTCPS; break;
+        }
+    }
+
+    if (!remove)
+        return 1;
+
+    /* Remove simple modes. */
+    channel->modes &= ~remove;
+
+    /* If removing bans, kill 'em all. */
+    if ((remove & MODE_BAN) && channel->banlist.used) {
+        unsigned int i;
+        for (i=0; i<channel->banlist.used; i++)
+            free(channel->banlist.list[i]);
+        channel->banlist.used = 0;
+    }
+
+    /* Remove member modes. */
+    if ((remove & (MODE_CHANOP | MODE_VOICE)) && channel->members.used) {
+        int mask = ~(remove & (MODE_CHANOP | MODE_VOICE));
+        unsigned int i;
+
+        for (i = 0; i < channel->members.used; i++)
+            channel->members.list[i]->modes &= mask;
+    }
+
+    return 1;
+}
+
+void
+reg_privmsg_func(struct userNode *user, privmsg_func_t handler)
+{
+    unsigned int numeric = user->num_local;
+    if (numeric >= num_privmsg_funcs) {
+        int newnum = numeric + 8;
+        privmsg_funcs = realloc(privmsg_funcs, newnum*sizeof(privmsg_func_t));
+        memset(privmsg_funcs+num_privmsg_funcs, 0, (newnum-num_privmsg_funcs)*sizeof(privmsg_func_t));
+        num_privmsg_funcs = newnum;
+    }
+    if (privmsg_funcs[numeric])
+        log_module(MAIN_LOG, LOG_WARNING, "re-registering new privmsg handler for numeric %d", numeric);
+    privmsg_funcs[numeric] = handler;
+}
+
+void
+reg_notice_func(struct userNode *user, privmsg_func_t handler)
+{
+    unsigned int numeric = user->num_local;
+    if (numeric >= num_notice_funcs) {
+        int newnum = numeric + 8;
+        notice_funcs = realloc(notice_funcs, newnum*sizeof(privmsg_func_t));
+        memset(notice_funcs+num_notice_funcs, 0, (newnum-num_notice_funcs)*sizeof(privmsg_func_t));
+        num_notice_funcs = newnum;
+    }
+    if (notice_funcs[numeric])
+        log_module(MAIN_LOG, LOG_WARNING, "re-registering new notice handler for numeric %d", numeric);
+    notice_funcs[numeric] = handler;
+}
+
+void
+reg_oper_func(oper_func_t handler)
+{
+    if (of_used == of_size) {
+       if (of_size) {
+           of_size <<= 1;
+           of_list = realloc(of_list, of_size*sizeof(oper_func_t));
+       } else {
+           of_size = 8;
+           of_list = malloc(of_size*sizeof(oper_func_t));
+       }
+    }
+    of_list[of_used++] = handler;
+}
+
+static void
+call_oper_funcs(struct userNode *user)
+{
+    unsigned int n;
+    if (IsLocal(user))
+        return;
+    for (n=0; n<of_used; n++)
+       of_list[n](user);
+}
+
+static void
+send_burst(void)
+{
+    unsigned int i, hop, max_hop=1;
+    dict_iterator_t it;
+
+    /* burst (juped) servers, closest first (except self, which is sent already) */
+    for (i=0; i<ArrayLength(servers_num); i++)
+        if (servers_num[i] && servers_num[i]->hops > max_hop)
+            max_hop = servers_num[i]->hops;
+    for (hop=1; hop<=max_hop; hop++) {
+        for (i=0;i<ArrayLength(servers_num);i++) {
+            if (servers_num[i]
+                && (servers_num[i]->hops == hop)
+                && (servers_num[i] != self->uplink))
+                irc_server(servers_num[i]);
+        }
+    }
+
+    /* burst local nicks */
+    for (i=0; i<=self->num_mask; i++)
+        if (self->users[i])
+            irc_user(self->users[i]);
+
+    /* build dict of unbursted channel names (just copy existing channels) */
+    unbursted_channels = dict_new();
+    for (it = dict_first(channels); it; it = iter_next(it))
+        dict_insert(unbursted_channels, iter_key(it), iter_data(it));
+}
diff --git a/src/proto.h b/src/proto.h
new file mode 100644 (file)
index 0000000..85f0491
--- /dev/null
@@ -0,0 +1,223 @@
+/* proto.h - IRC protocol output
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#if !defined(PROTO_H)
+#define PROTO_H
+
+/* Warning for those looking at how this code does multi-protocol
+ * support: It's an awful, nasty hack job.  It is intended for short
+ * term use, not long term, since we are already developing srvx2,
+ * which has much nicer interfaces that hide most of the ugly
+ * differences between protocol dialects. */
+
+#define COMBO_NUMERIC_LEN 5   /* 1/2, 1/3 or 2/3 digits for server/client parts */
+#define MAXLEN         512   /* Maximum IRC line length */
+#define MAXNUMPARAMS    200
+
+#ifdef HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#endif
+
+struct gline;
+struct server;
+struct userNode;
+struct chanNode;
+
+/* connection manager state */
+
+enum cState
+{
+    DISCONNECTED,
+    AUTHENTICATING,
+    BURSTING,
+    CONNECTED
+};
+
+#define UPLINK_UNAVAILABLE     0x001
+
+struct uplinkNode
+{
+    char               *name;
+
+    char               *host;
+    int                        port;
+
+    struct sockaddr_in         *bind_addr;
+    int                bind_addr_len;
+
+    char               *password;
+    char               *their_password;
+
+    enum cState                state;
+    int                        tries;
+    int                        max_tries;
+    long               flags;
+
+    struct uplinkNode  *prev;
+    struct uplinkNode  *next;
+};
+
+struct cManagerNode
+{
+    struct uplinkNode  *uplinks;
+    struct uplinkNode  *uplink;
+
+    int                        cycles;
+    int                        enabled;
+};
+
+#ifdef WITH_PROTOCOL_P10
+struct server* GetServerN(const char *numeric);
+struct userNode* GetUserN(const char *numeric);
+#endif
+
+/* Basic protocol parsing support. */
+void init_parse(void);
+int parse_line(char *line, int recursive);
+
+/* Callback notifications for protocol support. */
+typedef void (*chanmsg_func_t) (struct userNode *user, struct chanNode *chan, char *text, struct userNode *bot);
+void reg_chanmsg_func(unsigned char prefix, struct userNode *service, chanmsg_func_t handler);
+struct userNode *get_chanmsg_bot(unsigned char prefix);
+
+typedef void (*privmsg_func_t) (struct userNode *user, struct userNode *target, char *text, int server_qualified);
+void reg_privmsg_func(struct userNode *user, privmsg_func_t handler);
+void reg_notice_func(struct userNode *user, privmsg_func_t handler);
+
+typedef void (*oper_func_t) (struct userNode *user);
+void reg_oper_func(oper_func_t handler);
+
+extern struct userList dead_users;
+
+/* replay silliness */
+void replay_read_line(void);
+void replay_event_loop(void);
+
+/* connection maintenance */
+void irc_server(struct server *srv);
+void irc_user(struct userNode *user);
+void irc_nick(struct userNode *user, const char *old_nick);
+void irc_introduce(const char *passwd);
+void irc_ping(const char *something);
+void irc_pong(const char *who, const char *data);
+void irc_quit(struct userNode *user, const char *message);
+void irc_squit(struct server *srv, const char *message, const char *service_message);
+
+/* messages */
+void irc_privmsg(struct userNode *from, const char *to, const char *message);
+void irc_notice(struct userNode *from, const char *to, const char *message);
+void irc_wallchops(struct userNode *from, const char *to, const char *message);
+
+/* channel maintenance */
+void irc_join(struct userNode *who, struct chanNode *what);
+void irc_invite(struct userNode *from, struct userNode *who, struct chanNode *to);
+void irc_mode(struct userNode *who, struct chanNode *target, const char *modes);
+void irc_kick(struct userNode *who, struct userNode *target, struct chanNode *from, const char *msg);
+void irc_part(struct userNode *who, struct chanNode *what, const char *reason);
+void irc_topic(struct userNode *who, struct chanNode *what, const char *topic);
+void irc_fetchtopic(struct userNode *from, const char *to);
+
+/* network maintenance */
+void irc_gline(struct server *srv, struct gline *gline);
+void irc_settime(const char *srv_name_mask, time_t new_time);
+void irc_ungline(const char *mask);
+void irc_error(const char *to, const char *message);
+void irc_kill(struct userNode *from, struct userNode *target, const char *message);
+void irc_raw(const char *what);
+void irc_stats(struct userNode *from, struct server *target, char type);
+void irc_svsnick(struct userNode *from, struct userNode *target, const char *newnick);
+
+/* account maintenance */
+void irc_account(struct userNode *user, const char *stamp);
+void irc_regnick(struct userNode *user);
+
+/* numeric messages */
+void irc_numeric(struct userNode *user, unsigned int num, const char *format, ...);
+/* RFC1459-compliant numeric responses */
+#define RPL_ENDOFSTATS          219
+#define RPL_STATSUPTIME         242
+#define RPL_MAXCONNECTIONS      250
+#define RPL_WHOISUSER           311
+#define RPL_WHOISSERVER         312
+#define RPL_WHOISOPERATOR       313
+#define RPL_ENDOFWHOIS          318
+#define ERR_NOSUCHNICK          401
+
+/* stuff originally from other headers that is really protocol-specific */
+int IsChannelName(const char *name);
+int is_valid_nick(const char *nick);
+struct userNode *AddService(const char *nick, const char *desc);
+struct userNode *AddClone(const char *nick, const char *ident, const char *hostname, const char *desc);
+struct server* AddServer(struct server* uplink, const char *name, int hops, time_t boot, time_t link, const char *numeric, const char *description);
+void DelServer(struct server* serv, int announce, const char *message);
+void DelUser(struct userNode* user, struct userNode *killer, int announce, const char *why);
+/* Most protocols will want to make an AddUser helper function. */
+
+/* User modes */
+void mod_usermode(struct userNode *user, const char *modes);
+
+/* Channel mode manipulation */
+#define KEYLEN          23
+typedef unsigned long chan_mode_t;
+/* Rules for struct mod_chanmode:
+ * For a membership mode change, args[n].mode can contain more than
+ * one mode bit (e.g. MODE_CHANOP|MODE_VOICE).  Hostmask strings are
+ * "owned" by the caller and are not freed by mod_chanmode_free().
+ */
+struct mod_chanmode {
+    chan_mode_t modes_set, modes_clear;
+    unsigned int new_limit, argc;
+    char new_key[KEYLEN + 1];
+    struct {
+        unsigned int mode;
+        union {
+            struct modeNode *member;
+            const char *hostmask;
+        };
+    } args[1];
+};
+#define MCP_ALLOW_OVB     0x0001 /* allow op, voice, ban manipulation */
+#define MCP_FROM_SERVER   0x0002 /* parse as from a server */
+#define MCP_KEY_FREE      0x0004 /* -k without a key argument */
+#define MC_ANNOUNCE       0x0100 /* send a mod_chanmode() change out */
+#define MC_NOTIFY         0x0200 /* make local callbacks to announce */
+struct mod_chanmode *mod_chanmode_alloc(unsigned int argc);
+struct mod_chanmode *mod_chanmode_dup(struct mod_chanmode *orig, unsigned int extra);
+struct mod_chanmode *mod_chanmode_parse(struct chanNode *channel, char **modes, unsigned int argc, unsigned int flags);
+void mod_chanmode_apply(struct userNode *who, struct chanNode *channel, struct mod_chanmode *change);
+void mod_chanmode_announce(struct userNode *who, struct chanNode *channel, struct mod_chanmode *change);
+char *mod_chanmode_format(struct mod_chanmode *desc, char *buffer);
+void mod_chanmode_free(struct mod_chanmode *change);
+int mod_chanmode(struct userNode *who, struct chanNode *channel, char **modes, unsigned int argc, unsigned int flags);
+typedef void (*mode_change_func_t) (struct chanNode *channel, struct userNode *user, const struct mod_chanmode *change);
+void reg_mode_change_func(mode_change_func_t handler);
+int irc_make_chanmode(struct chanNode *chan, char *out);
+
+/* The "default" for generate_hostmask is to have all of these options off. */
+#define GENMASK_STRICT_HOST   1
+#define GENMASK_STRICT_IDENT  32
+#define GENMASK_ANY_IDENT     64
+#define GENMASK_STRICT   (GENMASK_STRICT_IDENT|GENMASK_STRICT_HOST)
+#define GENMASK_USENICK  2
+#define GENMASK_OMITNICK 4  /* Hurray for Kevin! */
+#define GENMASK_BYIP     8
+#define GENMASK_SRVXMASK 16
+#define GENMASK_NO_HIDING 128
+char *generate_hostmask(struct userNode *user, int options);
+
+#endif /* !defined(PROTO_H) */
diff --git a/src/recdb.c b/src/recdb.c
new file mode 100644 (file)
index 0000000..e873378
--- /dev/null
@@ -0,0 +1,658 @@
+/* recdb.c - recursive/record database implementation
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "recdb.h"
+#include "log.h"
+
+#ifdef HAVE_FCNTL_H
+#include <fcntl.h>
+#endif
+#ifdef HAVE_SYS_MMAN_H
+#include <sys/mman.h>
+#endif
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
+
+/* 4 MiB on x86 */
+#define MMAP_MAP_LENGTH (getpagesize()*1024)
+
+/* file format (grammar in Backus-Naur Form):
+ *
+ * database := record*
+ * record := qstring [ '=' ] ( qstring | object | stringlist ) ';'
+ * qstring := '"' ( [^\\] | ('\\' [\\n]) )* '"'
+ * object := '{' record* '}'
+ * stringlist := '(' [ qstring [',' qstring]* ] ')'
+ *
+ */
+
+/* when a database or object is read from disk, it is represented as
+ * a dictionary object, keys are names (what's left of the '=') and
+ * values are 'struct record_data's
+ */
+
+struct recdb_context {
+    int line;
+    int col;
+};
+
+enum recdb_filetype {
+    RECDB_FILE,
+    RECDB_STRING,
+    RECDB_MMAP
+};
+
+typedef struct recdb_file {
+    const char *source;
+    FILE *f; /* For RECDB_FILE, RECDB_MMAP */
+    char *s; /* For RECDB_STRING, RECDB_MMAP */
+    enum recdb_filetype type;
+    size_t length;
+    off_t pos;
+    struct recdb_context ctx;
+    jmp_buf env;
+} RECDB;
+
+typedef struct recdb_outfile {
+    FILE *f; /* For RECDB_FILE, RECDB_MMAP */
+    char *s; /* For RECDB_STRING, RECDB_MMAP */
+    union {
+        struct { /* For RECDB_STRING */
+            size_t chunksize;
+            size_t alloc_length;
+        } s;
+        struct { /* For RECDB_MMAP */
+            off_t mmap_begin;
+            size_t mmap_length;
+        } m;
+    } state;
+    enum recdb_filetype type;
+    off_t pos;
+    int tablvl;
+#ifdef NDEBUG
+    int need_tab;
+#endif
+} RECDB_OUT;
+
+#ifdef HAVE_MMAP
+static int mmap_error=0;
+#endif
+
+#define EOL '\n'
+
+#if 1
+#define ABORT(recdb, code, ch) longjmp((recdb)->env, ((code) << 8) | (ch))
+#else
+static void
+ABORT(RECDB *recdb, int code, unsigned char ch) {
+    longjmp(recdb->env, code << 8 | ch);
+}
+#endif
+
+enum fail_codes {
+    UNTERMINATED_STRING,
+    EXPECTED_OPEN_QUOTE,
+    EXPECTED_OPEN_BRACE,
+    EXPECTED_OPEN_PAREN,
+    EXPECTED_COMMA,
+    EXPECTED_START_RECORD_DATA,
+    EXPECTED_SEMICOLON,
+    EXPECTED_RECORD_DATA
+};
+
+static void parse_record_int(RECDB *recdb, char **pname, struct record_data **prd);
+
+/* allocation functions */
+
+#define alloc_record_data_int() malloc(sizeof(struct record_data))
+
+struct record_data *
+alloc_record_data_qstring(const char *string)
+{
+    struct record_data *rd;
+    rd = alloc_record_data_int();
+    SET_RECORD_QSTRING(rd, string);
+    return rd;
+}
+
+struct record_data *
+alloc_record_data_object(dict_t obj)
+{
+    struct record_data *rd;
+    rd = alloc_record_data_int();
+    SET_RECORD_OBJECT(rd, obj);
+    return rd;
+}
+
+struct record_data *
+alloc_record_data_string_list(struct string_list *slist)
+{
+    struct record_data *rd;
+    rd = alloc_record_data_int();
+    SET_RECORD_STRING_LIST(rd, slist);
+    return rd;
+}
+
+struct string_list*
+alloc_string_list(int size)
+{
+    struct string_list *slist;
+    slist = malloc(sizeof(struct string_list));
+    slist->used = 0;
+    slist->size = size;
+    slist->list = slist->size ? malloc(size*sizeof(char*)) : NULL;
+    return slist;
+}
+
+dict_t
+alloc_database(void)
+{
+    dict_t db = dict_new();
+    dict_set_free_data(db, free_record_data);
+    return db;
+}
+
+/* misc. operations */
+
+void
+string_list_append(struct string_list *slist, char *string)
+{
+    if (slist->used == slist->size) {
+        if (slist->size) {
+            slist->size <<= 1;
+            slist->list = realloc(slist->list, slist->size*sizeof(char*));
+        } else {
+            slist->size = 4;
+            slist->list = malloc(slist->size*sizeof(char*));
+        }
+    }
+    slist->list[slist->used++] = string;
+}
+
+struct string_list *
+string_list_copy(struct string_list *slist)
+{
+    struct string_list *new_list;
+    unsigned int i;
+    new_list = alloc_string_list(slist->size);
+    new_list->used = slist->used;
+    for (i=0; i<new_list->used; i++) {
+        new_list->list[i] = strdup(slist->list[i]);
+    }
+    return new_list;
+}
+
+int slist_compare_two(const void *pa, const void *pb)
+{
+    return irccasecmp(*(const char**)pa, *(const char **)pb);
+}
+
+void
+string_list_sort(struct string_list *slist)
+{
+    qsort(slist->list, slist->used, sizeof(slist->list[0]), slist_compare_two);
+}
+
+struct record_data*
+database_get_path(dict_t db, const char *path)
+{
+    char *new_path = strdup(path), *orig_path = new_path;
+    char *part;
+    struct record_data *rd;
+
+    for (part=new_path; *new_path; new_path++) {
+        if (*new_path != '/') continue;
+        *new_path = 0;
+
+        rd = dict_find(db, part, NULL);
+        if (!rd || rd->type != RECDB_OBJECT) {
+            free(orig_path);
+            return NULL;
+        }
+
+        db = rd->d.object;
+        part = new_path+1;
+    }
+
+    rd = dict_find(db, part, NULL);
+    free(orig_path);
+    return rd;
+}
+
+void*
+database_get_data(dict_t db, const char *path, enum recdb_type type)
+{
+    struct record_data *rd = database_get_path(db, path);
+    return (rd && rd->type == type) ? rd->d.whatever : NULL;
+}
+
+/* free functions */
+
+void
+free_string_list(struct string_list *slist)
+{
+    unsigned int i;
+    if (!slist)
+        return;
+    for (i=0; i<slist->used; i++)
+        free(slist->list[i]);
+    free(slist->list);
+    free(slist);
+}
+
+void
+free_record_data(void *rdata)
+{
+    struct record_data *r = rdata;
+    switch (r->type) {
+    case RECDB_INVALID: break;
+    case RECDB_QSTRING: free(r->d.qstring); break;
+    case RECDB_OBJECT: dict_delete(r->d.object); break;
+    case RECDB_STRING_LIST: free_string_list(r->d.slist); break;
+    }
+    free(r);
+}
+
+/* parse functions */
+
+static int
+dbeof(RECDB *recdb)
+{
+    switch (recdb->type) {
+        case RECDB_FILE:
+            return feof(recdb->f);
+            break;
+        case RECDB_STRING:
+            return !*recdb->s;
+            break;
+        case RECDB_MMAP:
+            return ((size_t)recdb->pos >= recdb->length);
+            break;
+        default:
+            return 1;
+            break;
+    }
+}
+
+static int
+dbgetc(RECDB *recdb)
+{
+    int res;
+    switch (recdb->type) {
+        case RECDB_FILE:
+            res = fgetc(recdb->f);
+            break;
+        case RECDB_STRING:
+        case RECDB_MMAP:
+            res = dbeof(recdb) ? EOF : recdb->s[recdb->pos++];
+            break;
+        default:
+            res = EOF;
+            break;
+    }
+    if (res == EOL) recdb->ctx.line++, recdb->ctx.col=1;
+    else if (res != EOF) recdb->ctx.col++;
+    return res;
+}
+
+static void
+dbungetc(int c, RECDB *recdb)
+{
+    switch (recdb->type) {
+        case RECDB_FILE:
+            ungetc(c, recdb->f);
+            break;
+        case RECDB_STRING:
+        case RECDB_MMAP:
+            recdb->s[--recdb->pos] = c;
+            break;
+    }
+    if (c == EOL) recdb->ctx.line--, recdb->ctx.col=-1;
+    else recdb->ctx.col--;
+}
+
+/* returns first non-whitespace, non-comment character (-1 for EOF found) */
+int
+parse_skip_ws(RECDB *recdb)
+{
+    int c, d, in_comment = 0;
+    while (!dbeof(recdb)) {
+        c = dbgetc(recdb);
+        if (c == EOF) return EOF;
+        if (isspace(c)) continue;
+        if (c != '/') return c;
+        if ((d = dbgetc(recdb)) == '*') {
+            /* C style comment, with slash star comment star slash */
+            in_comment = 1;
+            do {
+                do {
+                    c = dbgetc(recdb);
+                } while (c != '*' && c != EOF);
+                if ((c = dbgetc(recdb)) == '/') in_comment = 0;
+            } while (in_comment);
+        } else if (d == '/') {
+            /* C++ style comment, with slash slash comment newline */
+            do {
+                c = dbgetc(recdb);
+            } while (c != EOF && c != EOL);
+        } else {
+            if (d != EOF) dbungetc(d, recdb);
+            return c;
+        }
+    }
+    return -1;
+}
+
+char *
+parse_qstring(RECDB *recdb)
+{
+    char *buff;
+    int used=0, size=8, c;
+    struct recdb_context start_ctx;
+    unsigned int i;
+
+    if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
+    start_ctx = recdb->ctx;
+    if (c != '"') ABORT(recdb, EXPECTED_OPEN_QUOTE, c);
+    buff = malloc(size);
+    while (!dbeof(recdb) && (c = dbgetc(recdb)) != '"') {
+        if (c != '\\') {
+            /* There should never be a literal newline, as it is saved as a \n */
+            if (c == EOL) {
+                dbungetc(c, recdb);
+                ABORT(recdb, UNTERMINATED_STRING, ' ');
+            }
+            buff[used++] = c;
+        } else {
+            switch (c = dbgetc(recdb)) {
+                case '0': /* \<octal>, 000 through 377 */
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                case '5':
+                case '6':
+                case '7':
+                    {
+                        char digits[3] = { (char)c, '\0', '\0' };
+                        for (i=1; i < 3; i++) {
+                            /* Maximum of \377, so there's a max of 2 digits
+                             * if digits[0] > '3' (no \400, but \40 is fine) */
+                            if (i == 2 && digits[0] > '3') {
+                                break;
+                            }
+                            if ((c = dbgetc(recdb)) == EOF) {
+                                break;
+                            }
+                            if ((c < '0') || (c > '7')) {
+                                dbungetc(c, recdb);
+                                break;
+                            }
+                            digits[i] = (char)c;
+                        }
+                        if (i) {
+                            c = (int)strtol(digits, NULL, 8);
+                            buff[used++] = c;
+                        } else {
+                            buff[used++] = '\0';
+                        }
+                    }
+                    break;
+                case 'x': /* Hex */
+                    {
+                        char digits[3] = { '\0', '\0', '\0' };
+                        for (i=0; i < 2; i++) {
+                            if ((c = dbgetc(recdb)) == EOF) {
+                                break;
+                            }
+                            if (!isxdigit(c)) {
+                                dbungetc(c, recdb);
+                                break;
+                            }
+                            digits[i] = (char)c;
+                        }
+                        if (i) {
+                            c = (int)strtol(digits, NULL, 16);
+                            buff[used++] = c;
+                        } else {
+                            buff[used++] = '\\';
+                            buff[used++] = 'x';
+                        }
+                    }
+                    break;
+                case 'a': buff[used++] = '\a'; break;
+                case 'b': buff[used++] = '\b'; break;
+                case 't': buff[used++] = '\t'; break;
+                case 'n': buff[used++] = EOL; break;
+                case 'v': buff[used++] = '\v'; break;
+                case 'f': buff[used++] = '\f'; break;
+                case 'r': buff[used++] = '\r'; break;
+                case '\\': buff[used++] = '\\'; break;
+                case '"': buff[used++] = '"'; break;
+                default: buff[used++] = '\\'; buff[used++] = c; break;
+            }
+        }
+        if (used == size) {
+            size <<= 1;
+            buff = realloc(buff, size);
+        }
+    }
+    if (c != '"' && dbeof(recdb)) {
+        free(buff);
+        recdb->ctx = start_ctx;
+        ABORT(recdb, UNTERMINATED_STRING, EOF);
+    }
+    buff[used] = 0;
+    return buff;
+}
+
+dict_t
+parse_object(RECDB *recdb)
+{
+    dict_t obj;
+    char *name;
+    struct record_data *rd;
+    int c;
+    if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
+    if (c != '{') ABORT(recdb, EXPECTED_OPEN_BRACE, c);
+    obj = alloc_object();
+    dict_set_free_keys(obj, free);
+    while (!dbeof(recdb)) {
+        if ((c = parse_skip_ws(recdb)) == '}') break;
+        if (c == EOF) break;
+        dbungetc(c, recdb);
+        parse_record_int(recdb, &name, &rd);
+        dict_insert(obj, name, rd);
+    }
+    return obj;
+}
+
+struct string_list *
+parse_string_list(RECDB *recdb)
+{
+    struct string_list *slist;
+    int c;
+    if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
+    if (c != '(') ABORT(recdb, EXPECTED_OPEN_PAREN, c);
+    slist = alloc_string_list(4);
+    while (true) {
+        c = parse_skip_ws(recdb);
+        if (c == EOF || c == ')') break;
+        dbungetc(c, recdb);
+        string_list_append(slist, parse_qstring(recdb));
+        c = parse_skip_ws(recdb);
+        if (c == EOF || c == ')') break;
+        if (c != ',') ABORT(recdb, EXPECTED_COMMA, c);
+    }
+    return slist;
+}
+
+static void
+parse_record_int(RECDB *recdb, char **pname, struct record_data **prd)
+{
+    int c;
+    *pname = parse_qstring(recdb);
+    c = parse_skip_ws(recdb);
+    if (c == EOF) {
+        if (!*pname) return;
+        free(*pname);
+        ABORT(recdb, EXPECTED_RECORD_DATA, EOF);
+    }
+    if (c == '=') c = parse_skip_ws(recdb);
+    dbungetc(c, recdb);
+    *prd = malloc(sizeof(**prd));
+    switch (c) {
+    case '"':
+        /* Don't use SET_RECORD_QSTRING, since that does an extra strdup() of the string. */
+        (*prd)->type = RECDB_QSTRING;
+        (*prd)->d.qstring = parse_qstring(recdb);
+        break;
+    case '{': SET_RECORD_OBJECT(*prd, parse_object(recdb)); break;
+    case '(': SET_RECORD_STRING_LIST(*prd, parse_string_list(recdb)); break;
+    default: ABORT(recdb, EXPECTED_START_RECORD_DATA, c);
+    }
+    if ((c = parse_skip_ws(recdb)) != ';') ABORT(recdb, EXPECTED_SEMICOLON, c);
+}
+
+static dict_t
+parse_database_int(RECDB *recdb)
+{
+    char *name;
+    struct record_data *rd;
+    dict_t db = alloc_database();
+    dict_set_free_keys(db, free);
+    while (!dbeof(recdb)) {
+        parse_record_int(recdb, &name, &rd);
+        if (name) dict_insert(db, name, rd);
+    }
+    return db;
+}
+
+const char *
+failure_reason(int code)
+{
+    const char *reason;
+    switch (code >> 8) {
+    case UNTERMINATED_STRING: reason = "Unterminated string"; break;
+    case EXPECTED_OPEN_QUOTE: reason = "Expected '\"'"; break;
+    case EXPECTED_OPEN_BRACE: reason = "Expected '{'"; break;
+    case EXPECTED_OPEN_PAREN: reason = "Expected '('"; break;
+    case EXPECTED_COMMA: reason = "Expected ','"; break;
+    case EXPECTED_START_RECORD_DATA: reason = "Expected start of some record data"; break;
+    case EXPECTED_SEMICOLON: reason = "Expected ';'"; break;
+    case EXPECTED_RECORD_DATA: reason = "Expected record data"; break;
+    default: reason = "Unknown error";
+    }
+    if (code == -1) reason = "Premature end of file";
+    return reason;
+}
+
+void
+explain_failure(RECDB *recdb, int code)
+{
+    log_module(MAIN_LOG, LOG_ERROR, "%s (got '%c') at %s line %d column %d.",
+               failure_reason(code), code & 255,
+               recdb->source, recdb->ctx.line, recdb->ctx.col);
+}
+
+const char *
+parse_record(const char *text, char **pname, struct record_data **prd)
+{
+    RECDB recdb;
+    int res;
+    *pname = NULL;
+    *prd = NULL;
+    recdb.source = "<user-supplied text>";
+    recdb.f = NULL;
+    recdb.s = strdup(text);
+    recdb.length = strlen(text);
+    recdb.pos = 0;
+    recdb.type = RECDB_STRING;
+    recdb.ctx.line = recdb.ctx.col = 1;
+    if ((res = setjmp(recdb.env)) == 0) {
+        parse_record_int(&recdb, pname, prd);
+        return 0;
+    } else {
+        free(*pname);
+        free(*prd);
+        return failure_reason(res);
+    }
+}
+
+dict_t
+parse_database(const char *filename)
+{
+    RECDB recdb;
+    int res;
+    dict_t db;
+    struct stat statinfo;
+
+    recdb.source = filename;
+    if (!(recdb.f = fopen(filename, "r"))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to open database file '%s' for reading: %s", filename, strerror(errno));
+        return NULL;
+    }
+
+    if (fstat(fileno(recdb.f), &statinfo)) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to fstat database file '%s': %s", filename, strerror(errno));
+        return NULL;
+    }
+    recdb.length = (size_t)statinfo.st_size;
+
+#ifdef HAVE_MMAP
+    /* Try mmap */
+    if (!mmap_error && (recdb.s = mmap(NULL, recdb.length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fileno(recdb.f), 0)) != MAP_FAILED) {
+        recdb.type = RECDB_MMAP;
+    } else {
+        /* Fall back to stdio */
+        if (!mmap_error) {
+            log_module(MAIN_LOG, LOG_WARNING, "Unable to mmap database file '%s' (falling back to stdio): %s", filename, strerror(errno));
+            mmap_error = 1;
+        }
+#else
+    if (1) {
+#endif
+        recdb.s = NULL;
+        recdb.type = RECDB_FILE;
+    }
+
+    recdb.ctx.line = recdb.ctx.col = 1;
+    recdb.pos = 0;
+
+    if ((res = setjmp(recdb.env)) == 0) {
+        db = parse_database_int(&recdb);
+    } else {
+        explain_failure(&recdb, res);
+        _exit(1);
+    }
+
+    switch (recdb.type) {
+        case RECDB_MMAP:
+#ifdef HAVE_MMAP
+            munmap(recdb.s, recdb.length);
+#endif
+        case RECDB_FILE:
+            fclose(recdb.f);
+            break;
+        /* Appease gcc */
+        default:
+            break;
+    }
+    return db;
+}
diff --git a/src/recdb.h b/src/recdb.h
new file mode 100644 (file)
index 0000000..3c87ae8
--- /dev/null
@@ -0,0 +1,81 @@
+/* recdb.h - recursive/record database implementation
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef _recdb_h
+#define _recdb_h
+
+#include "common.h"
+#include "dict.h"
+
+enum recdb_type {
+    RECDB_INVALID,
+    RECDB_QSTRING,
+    RECDB_OBJECT,
+    RECDB_STRING_LIST
+};
+
+struct record_data {
+    enum recdb_type type;
+    union {
+       char *qstring;
+       dict_t object;
+       struct string_list *slist;
+       void *whatever;
+    } d;
+};
+
+#define SET_RECORD_QSTRING(rec, qs) do { const char *s = (qs); (rec)->type = RECDB_QSTRING; (rec)->d.qstring = (s) ? strdup(s) : NULL; } while (0)
+#define GET_RECORD_QSTRING(rec) (((rec)->type == RECDB_QSTRING) ? (rec)->d.qstring : 0)
+#define SET_RECORD_OBJECT(rec, obj) do { (rec)->type = RECDB_OBJECT; (rec)->d.object = (obj); } while (0)
+#define GET_RECORD_OBJECT(rec) (((rec)->type == RECDB_OBJECT) ? (rec)->d.object : 0)
+#define SET_RECORD_STRING_LIST(rec, sl) do { (rec)->type = RECDB_STRING_LIST; (rec)->d.slist = (sl); } while (0)
+#define GET_RECORD_STRING_LIST(rec) (((rec)->type == RECDB_STRING_LIST) ? (rec)->d.slist : 0)
+
+struct string_list {
+    unsigned int used, size;
+    char **list;
+};
+void string_list_append(struct string_list *slist, char *string);
+struct string_list *string_list_copy(struct string_list *orig);
+void string_list_sort(struct string_list *slist);
+#define string_list_delete(slist, n) (free((slist)->list[n]), (slist)->list[n] = (slist)->list[--(slist)->used])
+
+/* allocation functions */
+struct string_list *alloc_string_list(int size);
+struct record_data *alloc_record_data_qstring(const char *string);
+struct record_data *alloc_record_data_object(dict_t obj);
+struct record_data *alloc_record_data_string_list(struct string_list *slist);
+dict_t alloc_database(void);
+#define alloc_object() alloc_database()
+
+/* misc operations */
+/* note: once you give a string list a string, it frees it automatically */
+struct record_data *database_get_path(dict_t db, const char *path);
+void *database_get_data(dict_t db, const char *path, enum recdb_type type);
+
+/* freeing data */
+void free_string_list(struct string_list *slist);
+void free_record_data(void *rdata);
+#define free_object(obj) dict_delete(obj)
+#define free_database(db) dict_delete(db)
+
+/* parsing stuff from disk */
+const char *parse_record(const char *text, char **pname, struct record_data **prd);
+dict_t parse_database(const char *filename);
+
+#endif
diff --git a/src/saxdb.c b/src/saxdb.c
new file mode 100644 (file)
index 0000000..6d6527c
--- /dev/null
@@ -0,0 +1,553 @@
+/* saxdb.c - srvx database manager
+ * Copyright 2002-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "conf.h"
+#include "hash.h"
+#include "modcmd.h"
+#include "saxdb.h"
+#include "timeq.h"
+
+DECLARE_LIST(int_list, int);
+DEFINE_LIST(int_list, int);
+
+struct saxdb {
+    char *name;
+    char *filename;
+    char *mondo_section;
+    saxdb_reader_func_t *reader;
+    saxdb_writer_func_t *writer;
+    unsigned int write_interval;
+    time_t last_write;
+    unsigned int last_write_duration;
+    struct saxdb *prev;
+};
+
+struct saxdb_context {
+    FILE *output;
+    unsigned int indent;
+    struct int_list complex;
+    jmp_buf jbuf;
+    /* XXX: If jbuf is ever used, places that use saxdb_open_context() and
+     * saxdb_close_context() must be modified to fill it in */
+};
+
+#define COMPLEX(CTX) ((CTX)->complex.used ? ((CTX)->complex.list[(CTX)->complex.used-1]) : 1)
+
+static struct saxdb *last_db;
+static struct dict *saxdbs; /* -> struct saxdb */
+static struct dict *mondo_db;
+static struct module *saxdb_module;
+
+static SAXDB_WRITER(saxdb_mondo_writer);
+static void saxdb_timed_write(void *data);
+
+static void
+saxdb_read_db(struct saxdb *db) {
+    struct dict *data;
+
+    assert(db);
+    assert(db->filename);
+    data = parse_database(db->filename);
+    if (!data) return;
+    if (db->writer == saxdb_mondo_writer) {
+        mondo_db = data;
+    } else {
+        db->reader(data);
+        free_database(data);
+    }
+}
+
+struct saxdb *
+saxdb_register(const char *name, saxdb_reader_func_t *reader, saxdb_writer_func_t *writer) {
+    struct saxdb *db;
+    struct dict *conf;
+    int ii;
+    const char *filename = NULL, *str;
+    char conf_path[MAXLEN];
+
+    db = calloc(1, sizeof(*db));
+    db->name = strdup(name);
+    db->reader = reader;
+    db->writer = writer;
+    /* Look up configuration */
+    sprintf(conf_path, "dbs/%s", name);
+    if ((conf = conf_get_data(conf_path, RECDB_OBJECT))) {
+        if ((str = database_get_data(conf, "mondo_section", RECDB_QSTRING))) {
+            db->mondo_section = strdup(str);
+        }
+        str = database_get_data(conf, "frequency", RECDB_QSTRING);
+        db->write_interval = str ? ParseInterval(str) : 1800;
+        filename = database_get_data(conf, "filename", RECDB_QSTRING);
+    } else {
+        db->write_interval = 1800;
+    }
+    /* Schedule database writes */
+    if (db->write_interval && !db->mondo_section) {
+        timeq_add(now + db->write_interval, saxdb_timed_write, db);
+    }
+    /* Insert filename */
+    if (filename) {
+        db->filename = strdup(filename);
+    } else {
+        db->filename = malloc(strlen(db->name)+4);
+        for (ii=0; db->name[ii]; ++ii) db->filename[ii] = tolower(db->name[ii]);
+        strcpy(db->filename+ii, ".db");
+    }
+    /* Read from disk (or mondo DB) */
+    if (db->mondo_section) {
+        if (mondo_db && (conf = database_get_data(mondo_db, db->mondo_section, RECDB_OBJECT))) {
+            db->reader(conf);
+        }
+    } else {
+        saxdb_read_db(db);
+    }
+    /* Remember the database */
+    dict_insert(saxdbs, db->name, db);
+    db->prev = last_db;
+    last_db = db;
+    return db;
+}
+
+static int
+saxdb_write_db(struct saxdb *db) {
+    struct saxdb_context ctx;
+    char tmp_fname[MAXLEN];
+    int res, res2;
+    time_t start, finish;
+
+    assert(db->filename);
+    sprintf(tmp_fname, "%s.new", db->filename);
+    memset(&ctx, 0, sizeof(ctx));
+    ctx.output = fopen(tmp_fname, "w+");
+    int_list_init(&ctx.complex);
+    if (!ctx.output) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to write to %s: %s", tmp_fname, strerror(errno));
+        int_list_clean(&ctx.complex);
+        return 1;
+    }
+    start = time(NULL);
+    if ((res = setjmp(ctx.jbuf)) || (res2 = db->writer(&ctx))) {
+        if (res) {
+            log_module(MAIN_LOG, LOG_ERROR, "Exception %d caught while writing to %s", res, tmp_fname);
+        } else {
+            log_module(MAIN_LOG, LOG_ERROR, "Internal error %d while writing to %s", res2, tmp_fname);
+        }
+        int_list_clean(&ctx.complex);
+        fclose(ctx.output);
+        remove(tmp_fname);
+        return 2;
+    }
+    finish = time(NULL);
+    assert(ctx.complex.used == 0);
+    int_list_clean(&ctx.complex);
+    fclose(ctx.output);
+    if (rename(tmp_fname, db->filename) < 0) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to rename %s to %s: %s", tmp_fname, db->filename, strerror(errno));
+    }
+    db->last_write = now;
+    db->last_write_duration = finish - start;
+    log_module(MAIN_LOG, LOG_INFO, "Wrote %s database to disk.", db->name);
+    return 0;
+}
+
+static void
+saxdb_timed_write(void *data) {
+    struct saxdb *db = data;
+    saxdb_write_db(db);
+    timeq_add(now + db->write_interval, saxdb_timed_write, db);
+}
+
+void
+saxdb_write(const char *db_name) {
+    struct saxdb *db;
+    db = dict_find(saxdbs, db_name, NULL);
+    if (db) saxdb_write_db(db);
+}
+
+void
+saxdb_write_all(void) {
+    dict_iterator_t it;
+    struct saxdb *db;
+
+    for (it = dict_first(saxdbs); it; it = iter_next(it)) {
+        db = iter_data(it);
+        if (!db->mondo_section) saxdb_write_db(db);
+    }
+}
+
+#define saxdb_put_char(DEST, CH)   fputc(CH, (DEST)->output)
+#define saxdb_put_string(DEST, CH) fputs(CH, (DEST)->output)
+
+static inline void
+saxdb_put_nchars(struct saxdb_context *dest, const char *name, int len) {
+    while (len--) {
+        fputc(*name++, dest->output);
+    }
+}
+
+static void
+saxdb_put_qstring(struct saxdb_context *dest, const char *str) {
+    const char *esc;
+
+    assert(str);
+    saxdb_put_char(dest, '"');
+    while ((esc = strpbrk(str, "\\\a\b\t\n\v\f\r\""))) {
+        if (esc != str) saxdb_put_nchars(dest, str, esc-str);
+        saxdb_put_char(dest, '\\');
+        switch (*esc) {
+        case '\a': saxdb_put_char(dest, 'a'); break;
+        case '\b': saxdb_put_char(dest, 'b'); break;
+        case '\t': saxdb_put_char(dest, 't'); break;
+        case '\n': saxdb_put_char(dest, 'n'); break;
+        case '\v': saxdb_put_char(dest, 'v'); break;
+        case '\f': saxdb_put_char(dest, 'f'); break;
+        case '\r': saxdb_put_char(dest, 'r'); break;
+        case '\\': saxdb_put_char(dest, '\\'); break;
+        case '"': saxdb_put_char(dest, '"'); break;
+        }
+        str = esc + 1;
+    }
+    saxdb_put_string(dest, str);
+    saxdb_put_char(dest, '"');
+}
+
+#ifndef NDEBUG
+static void
+saxdb_pre_object(struct saxdb_context *dest) {
+    unsigned int ii;
+    if (COMPLEX(dest)) {
+        for (ii=0; ii<dest->indent; ++ii) saxdb_put_char(dest, '\t');
+    }
+}
+#else
+#define saxdb_pre_object(DEST) 
+#endif
+
+static inline void
+saxdb_post_object(struct saxdb_context *dest) {
+    saxdb_put_char(dest, ';');
+    saxdb_put_char(dest, COMPLEX(dest) ? '\n' : ' ');
+}
+
+void
+saxdb_start_record(struct saxdb_context *dest, const char *name, int complex) {
+    saxdb_pre_object(dest);
+    saxdb_put_qstring(dest, name);
+    saxdb_put_string(dest, " { ");
+    int_list_append(&dest->complex, complex);
+    if (complex) {
+        dest->indent++;
+        saxdb_put_char(dest, '\n');
+    }
+}
+
+void
+saxdb_end_record(struct saxdb_context *dest) {
+    assert(dest->complex.used > 0);
+    if (COMPLEX(dest)) dest->indent--;
+    saxdb_pre_object(dest);
+    dest->complex.used--;
+    saxdb_put_char(dest, '}');
+    saxdb_post_object(dest);
+}
+
+void
+saxdb_write_string_list(struct saxdb_context *dest, const char *name, struct string_list *list) {
+    unsigned int ii;
+
+    saxdb_pre_object(dest);
+    saxdb_put_qstring(dest, name);
+    saxdb_put_string(dest, " (");
+    if (list->used) {
+        for (ii=0; ii<list->used-1; ++ii) {
+            saxdb_put_qstring(dest, list->list[ii]);
+            saxdb_put_string(dest, ", ");
+        }
+        saxdb_put_qstring(dest, list->list[list->used-1]);
+    }
+    saxdb_put_string(dest, ")");
+    saxdb_post_object(dest);
+}
+
+void
+saxdb_write_string(struct saxdb_context *dest, const char *name, const char *value) {
+    saxdb_pre_object(dest);
+    saxdb_put_qstring(dest, name);
+    saxdb_put_char(dest, ' ');
+    saxdb_put_qstring(dest, value);
+    saxdb_post_object(dest);
+}
+
+void
+saxdb_write_int(struct saxdb_context *dest, const char *name, unsigned long value) {
+    unsigned char buf[16];
+    /* we could optimize this to take advantage of the fact that buf will never need escapes */
+    snprintf(buf, sizeof(buf), "%lu", value);
+    saxdb_write_string(dest, name, buf);
+}
+
+static void
+saxdb_free(void *data) {
+    struct saxdb *db = data;
+    free(db->name);
+    free(db->filename);
+    free(db->mondo_section);
+    free(db);
+}
+
+static int
+saxdb_mondo_read(struct dict *db, struct saxdb *saxdb) {
+    int res;
+    struct dict *subdb;
+
+    if (saxdb->prev && (res = saxdb_mondo_read(db, saxdb->prev))) return res;
+    if (saxdb->mondo_section
+        && (subdb = database_get_data(db, saxdb->mondo_section, RECDB_OBJECT))
+        && (res = saxdb->reader(subdb))) {
+        log_module(MAIN_LOG, LOG_INFO, " mondo section read for %s failed: %d", saxdb->mondo_section, res);
+        return res;
+    }
+    return 0;
+}
+
+static SAXDB_READER(saxdb_mondo_reader) {
+    return saxdb_mondo_read(db, last_db);
+}
+
+static int
+saxdb_mondo_write(struct saxdb_context *ctx, struct saxdb *saxdb) {
+    int res;
+    if (saxdb->prev && (res = saxdb_mondo_write(ctx, saxdb->prev))) return res;
+    if (saxdb->mondo_section) {
+        saxdb_start_record(ctx, saxdb->mondo_section, 1);
+        if ((res = saxdb->writer(ctx))) {
+            log_module(MAIN_LOG, LOG_INFO, " mondo section write for %s failed: %d", saxdb->mondo_section, res);
+            return res;
+        }
+        saxdb_end_record(ctx);
+        /* cheat a little here to put a newline between mondo sections */
+        saxdb_put_char(ctx, '\n');
+    }
+    return 0;
+}
+
+static SAXDB_WRITER(saxdb_mondo_writer) {
+    return saxdb_mondo_write(ctx, last_db);
+}
+
+static MODCMD_FUNC(cmd_write) {
+    struct timeval start, stop;
+    unsigned int ii, written;
+    struct saxdb *db;
+
+    assert(argc >= 2);
+    written = 0;
+    for (ii=1; ii<argc; ++ii) {
+        if (!(db = dict_find(saxdbs, argv[ii], NULL))) {
+            reply("MSG_DB_UNKNOWN", argv[ii]);
+            continue;
+        }
+        if (db->mondo_section) {
+            reply("MSG_DB_IS_MONDO", db->name);
+            continue;
+        }
+        gettimeofday(&start, NULL);
+        if (saxdb_write_db(db)) {
+            reply("MSG_DB_WRITE_ERROR", db->name);
+        } else {
+            gettimeofday(&stop, NULL);
+            stop.tv_sec -= start.tv_sec;
+            stop.tv_usec -= start.tv_usec;
+            if (stop.tv_usec < 0) {
+                stop.tv_sec -= 1;
+                stop.tv_usec += 1000000;
+            }
+            reply("MSG_DB_WROTE_DB", db->name, stop.tv_sec, stop.tv_usec);
+            written++;
+        }
+    }
+    return written;
+}
+
+static MODCMD_FUNC(cmd_writeall) {
+    struct timeval start, stop;
+
+    gettimeofday(&start, NULL);
+    saxdb_write_all();
+    gettimeofday(&stop, NULL);
+    stop.tv_sec -= start.tv_sec;
+    stop.tv_usec -= start.tv_usec;
+    if (stop.tv_usec < 0) {
+        stop.tv_sec -= 1;
+        stop.tv_usec += 1000000;
+    }
+    reply("MSG_DB_WROTE_ALL", stop.tv_sec, stop.tv_usec);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_databases) {
+    struct helpfile_table tbl;
+    dict_iterator_t it;
+    unsigned int ii;
+
+    tbl.length = dict_size(saxdbs) + 1;
+    tbl.width = 5;
+    tbl.flags = TABLE_NO_FREE;
+    tbl.contents = calloc(tbl.length, sizeof(tbl.contents[0]));
+    tbl.contents[0] = calloc(tbl.width, sizeof(tbl.contents[0][0]));
+    tbl.contents[0][0] = "Database";
+    tbl.contents[0][1] = "Filename/Section";
+    tbl.contents[0][2] = "Interval";
+    tbl.contents[0][3] = "Last Written";
+    tbl.contents[0][4] = "Last Duration";
+    for (ii=1, it=dict_first(saxdbs); it; it=iter_next(it), ++ii) {
+        struct saxdb *db = iter_data(it);
+        char *buf = malloc(INTERVALLEN*3);
+        tbl.contents[ii] = calloc(tbl.width, sizeof(tbl.contents[ii][0]));
+        tbl.contents[ii][0] = db->name;
+        tbl.contents[ii][1] = db->mondo_section ? db->mondo_section : db->filename;
+        if (db->write_interval) {
+            intervalString(buf, db->write_interval);
+        } else {
+            strcpy(buf, "Never");
+        }
+        tbl.contents[ii][2] = buf;
+        if (db->last_write) {
+            intervalString(buf+INTERVALLEN, now - db->last_write);
+            intervalString(buf+INTERVALLEN*2, db->last_write_duration);
+        } else {
+            strcpy(buf+INTERVALLEN, "Never");
+            strcpy(buf+INTERVALLEN*2, "Never");
+        }
+        tbl.contents[ii][3] = buf+INTERVALLEN;
+        tbl.contents[ii][4] = buf+INTERVALLEN*2;
+    }
+    table_send(cmd->parent->bot, user->nick, 0, 0, tbl);
+    free(tbl.contents[0]);
+    for (ii=1; ii<tbl.length; ++ii) {
+        free((char*)tbl.contents[ii][2]);
+        free(tbl.contents[ii]);
+    }
+    free(tbl.contents);
+    return 0;
+}
+
+static void
+saxdb_cleanup(void) {
+    dict_delete(saxdbs);
+}
+
+static struct helpfile_expansion
+saxdb_expand_help(const char *variable) {
+    struct helpfile_expansion exp;
+    if (!strcasecmp(variable, "dblist")) {
+        dict_iterator_t it;
+        struct string_buffer sbuf;
+        struct saxdb *db;
+
+        exp.type = HF_STRING;
+        string_buffer_init(&sbuf);
+        for (it = dict_first(saxdbs); it; it = iter_next(it)) {
+            db = iter_data(it);
+            if (db->mondo_section) continue;
+            if (sbuf.used) string_buffer_append_string(&sbuf, ", ");
+            string_buffer_append_string(&sbuf, iter_key(it));
+        }
+        exp.value.str = sbuf.list;
+    } else {
+        exp.type = HF_STRING;
+        exp.value.str = NULL;
+    }
+    return exp;
+}
+
+void
+saxdb_init(void) {
+    reg_exit_func(saxdb_cleanup);
+    saxdbs = dict_new();
+    dict_set_free_data(saxdbs, saxdb_free);
+    saxdb_register("mondo", saxdb_mondo_reader, saxdb_mondo_writer);
+    saxdb_module = module_register("saxdb", MAIN_LOG, "saxdb.help", saxdb_expand_help);
+    modcmd_register(saxdb_module, "write", cmd_write, 2, MODCMD_REQUIRE_AUTHED, "level", "800", NULL);
+    modcmd_register(saxdb_module, "writeall", cmd_writeall, 0, MODCMD_REQUIRE_AUTHED, "level", "800", NULL);
+    modcmd_register(saxdb_module, "stats databases", cmd_stats_databases, 0, 0, NULL);
+}
+
+void
+saxdb_finalize(void) {
+    free_database(mondo_db);
+}
+
+static void
+write_database_helper(struct saxdb_context *ctx, struct dict *db) {
+    dict_iterator_t it;
+    struct record_data *rd;
+
+    for (it = dict_first(db); it; it = iter_next(it)) {
+        rd = iter_data(it);
+        switch (rd->type) {
+        case RECDB_INVALID: break;
+        case RECDB_QSTRING: saxdb_write_string(ctx, iter_key(it), rd->d.qstring); break;
+        case RECDB_STRING_LIST: saxdb_write_string_list(ctx, iter_key(it), rd->d.slist); break;
+        case RECDB_OBJECT:
+            saxdb_start_record(ctx, iter_key(it), 1);
+            write_database_helper(ctx, rd->d.object);
+            saxdb_end_record(ctx);
+            break;
+        }
+    }
+}
+
+int
+write_database(FILE *out, struct dict *db) {
+    struct saxdb_context ctx;
+    int res;
+
+    ctx.output = out;
+    ctx.indent = 0;
+    int_list_init(&ctx.complex);
+    if (!(res = setjmp(ctx.jbuf))) {
+        write_database_helper(&ctx, db);
+    } else {
+        log_module(MAIN_LOG, LOG_ERROR, "Exception %d caught while writing to stream", res);
+        int_list_clean(&ctx.complex);
+        return 1;
+    }
+    assert(ctx.complex.used == 0);
+    int_list_clean(&ctx.complex);
+    return 0;
+}
+
+struct saxdb_context *
+saxdb_open_context(FILE *file) {
+    struct saxdb_context *ctx;
+
+    assert(file);
+    ctx = calloc(1, sizeof(*ctx));
+    ctx->output = file;
+    int_list_init(&ctx->complex);
+
+    return ctx;
+}
+
+void
+saxdb_close_context(struct saxdb_context *ctx) {
+    assert(ctx->complex.used == 0);
+    int_list_clean(&ctx->complex);
+    free(ctx);
+}
diff --git a/src/saxdb.h b/src/saxdb.h
new file mode 100644 (file)
index 0000000..38a0fc9
--- /dev/null
@@ -0,0 +1,51 @@
+/* saxdb.h - srvx database manager
+ * Copyright 2002-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#if !defined(DBMGR_H)
+#define DBMGR_H
+
+#include "recdb.h"
+
+struct saxdb;
+struct saxdb_context;
+
+#define SAXDB_READER(NAME) int NAME(struct dict *db)
+typedef SAXDB_READER(saxdb_reader_func_t);
+
+#define SAXDB_WRITER(NAME) int NAME(struct saxdb_context *ctx)
+typedef SAXDB_WRITER(saxdb_writer_func_t);
+
+void saxdb_init(void);
+void saxdb_finalize(void);
+struct saxdb *saxdb_register(const char *name, saxdb_reader_func_t *reader, saxdb_writer_func_t *writer);
+void saxdb_write(const char *db_name);
+void saxdb_write_all(void);
+int write_database(FILE *out, struct dict *db);
+
+/* Callbacks for SAXDB_WRITERs */
+void saxdb_start_record(struct saxdb_context *dest, const char *name, int complex);
+void saxdb_end_record(struct saxdb_context *dest);
+void saxdb_write_string_list(struct saxdb_context *dest, const char *name, struct string_list *list);
+void saxdb_write_string(struct saxdb_context *dest, const char *name, const char *value);
+void saxdb_write_int(struct saxdb_context *dest, const char *name, unsigned long value);
+
+/* For doing db writing by hand */
+struct saxdb_context *saxdb_open_context(FILE *f);
+void saxdb_close_context(struct saxdb_context *ctx);
+
+#endif /* !defined(DBMGR_H) */
diff --git a/src/saxdb.help b/src/saxdb.help
new file mode 100644 (file)
index 0000000..cf1ccec
--- /dev/null
@@ -0,0 +1,7 @@
+"write" ("/msg $S WRITE <dbname>",
+        "Writes out one of the databases.  Supported databases are:",
+        "  ${dblist}",
+        "$uSee also:$u writeall");
+"writeall" ("/msg $S WRITEALL",
+        "Writes out all databases at once.",
+        "$uSee also:$u write");
diff --git a/src/sendmail.c b/src/sendmail.c
new file mode 100644 (file)
index 0000000..fdb403a
--- /dev/null
@@ -0,0 +1,338 @@
+/* sendmail.c - mail sending utilities
+ * Copyright 2002-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "conf.h"
+#include "modcmd.h"
+#include "nickserv.h"
+#include "saxdb.h"
+
+#ifdef HAVE_SYS_WAIT_H
+#include <sys/wait.h>
+#endif
+
+#define KEY_PROHIBITED   "prohibited"
+
+static const struct message_entry msgtab[] = {
+    { "MAILMSG_EMAIL_ALREADY_BANNED", "%s is already banned (%s)." },
+    { "MAILMSG_EMAIL_BANNED", "Email to %s has been forbidden." },
+    { "MAILMSG_EMAIL_NOT_BANNED", "Email to %s was not forbidden." },
+    { "MAILMSG_EMAIL_UNBANNED", "Email to %s is now allowed." },
+    { "MAILMSG_PROHIBITED_EMAIL", "%s: %s" },
+    { "MAILMSG_NO_PROHIBITED_EMAIL", "All email addresses are accepted." },
+    { NULL, NULL }
+};
+
+static dict_t prohibited_addrs, prohibited_masks;
+struct module *sendmail_module;
+
+const char *
+sendmail_prohibited_address(const char *addr)
+{
+    dict_iterator_t it;
+    const char *data;
+
+    if (prohibited_addrs && (data = dict_find(prohibited_addrs, addr, NULL)))
+        return data;
+    if (prohibited_masks)
+        for (it = dict_first(prohibited_masks); it; it = iter_next(it))
+            if (match_ircglob(addr, iter_key(it)))
+                return iter_data(it);
+    return NULL;
+}
+
+/* This function sends the given "paragraph" as flowed text, as
+ * defined in RFC 2646.  It lets us only worry about line wrapping
+ * here, and not in the code that generates mail.
+ */
+static void
+send_flowed_text(FILE *where, const char *para)
+{
+    const char *eol = strchr(para, '\n');
+    unsigned int shift;
+
+    while (*para) {
+        /* Do we need to space-stuff the line? */
+        if ((*para == ' ') || (*para == '>') || !strncmp(para, "From ", 5)) {
+            fputc(' ', where);
+            shift = 1;
+        } else {
+            shift = 0;
+        }
+        /* How much can we put on this line? */
+        if (!eol && (strlen(para) < (80 - shift))) {
+            /* End of paragraph; can put on one line. */
+            fputs(para, where);
+            fputs("\n", where);
+            break;
+        } else if (eol && (eol < para + (80 - shift))) {
+            /* Newline inside paragraph, no need to wrap. */
+            fprintf(where, "%.*s\n", eol - para, para);
+            para = eol + 1;
+        } else {
+            int pos;
+            /* Need to wrap.  Where's the last space in the line? */
+            for (pos=72-shift; pos && (para[pos] != ' '); pos--) ;
+            /* If we didn't find a space, look ahead instead. */
+            if (pos == 0) pos = strcspn(para, " \n");
+            fprintf(where, "%.*s\n", pos+1, para);
+            para += pos + 1;
+        }
+        if (eol && (eol < para)) eol = strchr(para, '\n');
+    }
+}
+
+void
+sendmail(struct userNode *from, struct handle_info *to, const char *subject, const char *body, int first_time)
+{
+    pid_t child;
+    int infds[2], outfds[2];
+    const char *fromaddr, *str;
+
+    /* Grab some config items first. */
+    str = conf_get_data("mail/enable", RECDB_QSTRING);
+    if (!str || !enabled_string(str))
+        return;
+    fromaddr = conf_get_data("mail/from_address", RECDB_QSTRING);
+
+    /* How this works: We fork, and the child tries to send the mail.
+     * It does this by setting up a pipe pair, and forking again (the
+     * grandchild exec()'s the mailer program).  The mid-level child
+     * sends the text to the grandchild's stdin, and then logs the
+     * success or failure.
+     */
+
+    child = fork();
+    if (child < 0) {
+        log_module(MAIN_LOG, LOG_ERROR, "sendmail() to %s couldn't fork(): %s (%d)", to->email_addr, strerror(errno), errno);
+        return;
+    } else if (child > 0) {
+        return;
+    }
+    /* We're in a child now; must _exit() to die properly. */
+    if (pipe(infds) < 0) {
+        log_module(MAIN_LOG, LOG_ERROR, "sendmail() child to %s couldn't pipe(infds): %s (%d)", to->email_addr, strerror(errno), errno);
+        _exit(1);
+    }
+    if (pipe(outfds) < 0) {
+        log_module(MAIN_LOG, LOG_ERROR, "sendmail() child to %s couldn't pipe(outfds): %s (%d)", to->email_addr, strerror(errno), errno);
+        _exit(1);
+    }
+    child = fork();
+    if (child < 0) {
+        log_module(MAIN_LOG, LOG_ERROR, "sendmail() child to %s couldn't fork(): %s (%d)", to->email_addr, strerror(errno), errno);
+        _exit(1);
+    } else if (child > 0) {
+        /* Mid-level child; get ready to send the mail. */
+        FILE *out = fdopen(infds[1], "w");
+        struct string_list *extras;
+        unsigned int nn;
+        int res, rv;
+
+        /* Close the end of pipes we do not use. */
+        close(infds[0]);
+        close(outfds[1]);
+
+        /* Do we have any "extra" headers to send? */
+        extras = conf_get_data("mail/extra_headers", RECDB_STRING_LIST);
+        if (extras) {
+            for (nn=0; nn<extras->used; nn++) {
+                fputs(extras->list[nn], out);
+                fputs("\n", out);
+            }
+        }
+
+        /* Content type?  (format=flowed is a standard for plain text
+         * that lets the receiver reconstruct paragraphs, defined in
+         * RFC 2646.  See comment above send_flowed_text() for more.)
+         */
+        if (!(str = conf_get_data("mail/charset", RECDB_QSTRING))) str = "us-ascii";
+        fprintf(out, "Content-Type: text/plain; charset=%s; format=flowed\n", str);
+
+        /* Send From, To and Subject headers */
+        if (!fromaddr) fromaddr = "admin@poorly.configured.network";
+        fprintf(out, "From: %s <%s>\n", from->nick, fromaddr);
+        fprintf(out, "To: \"%s\" <%s>\n", to->handle, to->email_addr);
+        fprintf(out, "Subject: %s\n", subject);
+
+        /* Send mail body */
+        fputs("\n", out); /* terminate headers */
+        extras = conf_get_data((first_time?"mail/body_prefix_first":"mail/body_prefix"), RECDB_STRING_LIST);
+        if (extras) {
+            for (nn=0; nn<extras->used; nn++) {
+                send_flowed_text(out, extras->list[nn]);
+            }
+            fputs("\n", out);
+        }
+        send_flowed_text(out, body);
+        extras = conf_get_data((first_time?"mail/body_suffix_first":"mail/body_suffix"), RECDB_STRING_LIST);
+        if (extras) {
+            fputs("\n", out);
+            for (nn=0; nn<extras->used; nn++)
+                send_flowed_text(out, extras->list[nn]);
+        }
+
+        /* Close file (sending mail) and check for return code */
+        fflush(out);
+        fclose(out);
+        do {
+            rv = wait4(child, &res, 0, NULL);
+        } while ((rv == -1) && (errno == EINTR));
+        if (rv == child) {
+            /* accept the wait() result */
+        } else {
+            log_module(MAIN_LOG, LOG_ERROR, "sendmail() child to %s: Bad wait() return code %d: %s (%d)", to->email_addr, rv, strerror(errno), errno);
+            _exit(1);
+        }
+        if (res) {
+            log_module(MAIN_LOG, LOG_ERROR, "sendmail() grandchild to %s: Exited with code %d", to->email_addr, res);
+            _exit(1);
+        } else {
+            log_module(MAIN_LOG, LOG_INFO, "sendmail() sent email to %s <%s>: %s", to->handle, to->email_addr, subject);
+        }
+        _exit(0);
+    } else {
+        /* Grandchild; dup2 the fds and exec the mailer. */
+        const char *argv[10], *mpath;
+        unsigned int argc = 0;
+
+        /* Close the end of pipes we do not use. */
+        close(infds[1]);
+        close(outfds[0]);
+
+        dup2(infds[0], STDIN_FILENO);
+        dup2(outfds[1], STDOUT_FILENO);
+        mpath = conf_get_data("mail/mailer", RECDB_QSTRING);
+        if (!mpath) mpath = "/usr/sbin/sendmail";
+        argv[argc++] = mpath;
+        if (fromaddr) {
+            argv[argc++] = "-f";
+            argv[argc++] = fromaddr;
+        }
+        argv[argc++] = to->email_addr;
+        argv[argc++] = NULL;
+        if (execv(mpath, (char**)argv) < 0) {
+            log_module(MAIN_LOG, LOG_ERROR, "sendmail() grandchild to %s couldn't execv(): %s (%d)", to->email_addr, strerror(errno), errno);
+        }
+        _exit(1);
+    }
+}
+
+static int
+sendmail_ban_address(struct userNode *user, struct userNode *bot, const char *addr, const char *reason) {
+    dict_t target;
+    const char *str;
+
+    target = strpbrk(addr, "*?") ? prohibited_masks : prohibited_addrs;
+    if ((str = dict_find(target, addr, NULL))) {
+        if (user)
+            send_message(user, bot, "MAILMSG_EMAIL_ALREADY_BANNED", addr, str);
+        return 0;
+    }
+    dict_insert(target, strdup(addr), strdup(reason));
+    if (user) send_message(user, bot, "MAILMSG_EMAIL_BANNED", addr);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_banemail) {
+    char *reason = unsplit_string(argv+2, argc-2, NULL);
+    return sendmail_ban_address(user, cmd->parent->bot, argv[1], reason);
+}
+
+static MODCMD_FUNC(cmd_unbanemail) {
+    dict_t target;
+    const char *addr;
+
+    addr = argv[1];
+    target = strpbrk(addr, "*?") ? prohibited_masks : prohibited_addrs;
+    if (dict_remove(target, addr))
+        reply("MAILMSG_EMAIL_UNBANNED", addr);
+    else
+        reply("MAILMSG_EMAIL_NOT_BANNED", addr);
+    return 1;
+}
+
+static MODCMD_FUNC(cmd_stats_email) {
+    dict_iterator_t it;
+    int found = 0;
+
+    for (it=dict_first(prohibited_addrs); it; it=iter_next(it)) {
+        reply("MAILMSG_PROHIBITED_EMAIL", iter_key(it), (const char*)iter_data(it));
+        found = 1;
+    }
+    for (it=dict_first(prohibited_masks); it; it=iter_next(it)) {
+        reply("MAILMSG_PROHIBITED_EMAIL", iter_key(it), (const char*)iter_data(it));
+        found = 1;
+    }
+    if (!found)
+        reply("MAILMSG_NO_PROHIBITED_EMAIL");
+    return 0;
+}
+
+static int
+sendmail_saxdb_read(struct dict *db) {
+    struct dict *subdb;
+    struct record_data *rd;
+    dict_iterator_t it;
+
+    if ((subdb = database_get_data(db, KEY_PROHIBITED, RECDB_OBJECT))) {
+        for (it = dict_first(subdb); it; it = iter_next(it)) {
+            rd = iter_data(it);
+            if (rd->type == RECDB_QSTRING)
+                sendmail_ban_address(NULL, NULL, iter_key(it), rd->d.qstring);
+        }
+    }
+    return 0;
+}
+
+static int
+sendmail_saxdb_write(struct saxdb_context *ctx) {
+    dict_iterator_t it;
+
+    saxdb_start_record(ctx, KEY_PROHIBITED, 0);
+    for (it = dict_first(prohibited_masks); it; it = iter_next(it))
+        saxdb_write_string(ctx, iter_key(it), iter_data(it));
+    for (it = dict_first(prohibited_addrs); it; it = iter_next(it))
+        saxdb_write_string(ctx, iter_key(it), iter_data(it));
+    saxdb_end_record(ctx);
+    return 0;
+}
+
+static void
+sendmail_cleanup(void)
+{
+    dict_delete(prohibited_addrs);
+    dict_delete(prohibited_masks);
+}
+
+void
+sendmail_init(void)
+{
+    prohibited_addrs = dict_new();
+    dict_set_free_keys(prohibited_addrs, free);
+    dict_set_free_data(prohibited_addrs, free);
+    prohibited_masks = dict_new();
+    dict_set_free_keys(prohibited_masks, free);
+    dict_set_free_data(prohibited_masks, free);
+    reg_exit_func(sendmail_cleanup);
+    saxdb_register("sendmail", sendmail_saxdb_read, sendmail_saxdb_write);
+    sendmail_module = module_register("sendmail", MAIN_LOG, "sendmail.help", NULL);
+    modcmd_register(sendmail_module, "banemail", cmd_banemail, 3, 0, "level", "601", NULL);
+    modcmd_register(sendmail_module, "stats email", cmd_stats_email, 0, 0, "flags", "+oper", NULL);
+    modcmd_register(sendmail_module, "unbanemail", cmd_unbanemail, 2, 0, "level", "601", NULL);
+    message_register_table(msgtab);
+}
diff --git a/src/sendmail.h b/src/sendmail.h
new file mode 100644 (file)
index 0000000..c22f418
--- /dev/null
@@ -0,0 +1,26 @@
+/* sendmail.h - mail sending utilities
+ * Copyright 2002 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#if !defined(SENDMAIL_H)
+#define SENDMAIL_H
+
+void sendmail_init(void);
+void sendmail(struct userNode *from, struct handle_info *to, const char *subject, const char *body, int first_time);
+const char *sendmail_prohibited_address(const char *addr);
+
+#endif
diff --git a/src/sendmail.help b/src/sendmail.help
new file mode 100644 (file)
index 0000000..6951b5a
--- /dev/null
@@ -0,0 +1,7 @@
+"BANEMAIL" ("/msg $S BANEMAIL <address> <reason>",
+        "Keeps srvx from sending mail to the address.  The address may be a real address, or a glob that uses * and ? wildcards.",
+        "This also prevents anyone from using matching addresses as their account email address.",
+        "$uSee Also:$u unbanemail, stats email");
+"UNBANEMAIL" ("/msg $S UNBANEMAIL <address>",
+        "Removes an email address (or glob) from the banned email address list.",
+        "$uSee Also:$u banemail, stats email");
diff --git a/src/stamp-h.in b/src/stamp-h.in
new file mode 100644 (file)
index 0000000..9788f70
--- /dev/null
@@ -0,0 +1 @@
+timestamp
diff --git a/src/stamp-h1.in b/src/stamp-h1.in
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/timeq.c b/src/timeq.c
new file mode 100644 (file)
index 0000000..3bbbf46
--- /dev/null
@@ -0,0 +1,117 @@
+/* timeq.c - time-based event queue
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "common.h"
+#include "heap.h"
+#include "timeq.h"
+
+heap_t timeq;
+
+struct timeq_entry {
+    timeq_func func;
+    void *data;
+};
+
+static void
+timeq_cleanup(void)
+{
+    timeq_del(0, 0, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_FUNC|TIMEQ_IGNORE_DATA);
+    heap_delete(timeq);
+}
+
+void
+timeq_init(void)
+{
+    timeq = heap_new(int_comparator);
+    reg_exit_func(timeq_cleanup);
+}
+
+time_t
+timeq_next(void)
+{
+    void *time;
+    heap_peek(timeq, &time, 0);
+    return (time_t)time;
+}
+
+void
+timeq_add(time_t when, timeq_func func, void *data)
+{
+    struct timeq_entry *ent;
+    void *w;
+    ent = malloc(sizeof(struct timeq_entry));
+    ent->func = func;
+    ent->data = data;
+    w = (void*)when;
+    heap_insert(timeq, w, ent);
+}
+
+struct timeq_extra {
+    time_t when;
+    timeq_func func;
+    void *data;
+    int mask;
+};
+
+static int
+timeq_del_matching(void *key, void *data, void *extra)
+{
+    struct timeq_entry *a = data;
+    struct timeq_extra *b = extra;
+    if (((b->mask & TIMEQ_IGNORE_WHEN) || ((time_t)key == b->when))
+       && ((b->mask & TIMEQ_IGNORE_FUNC) || (a->func == b->func))
+       && ((b->mask & TIMEQ_IGNORE_DATA) || (a->data == b->data))) {
+        free(data);
+        return 1;
+    } else {
+        return 0;
+    }
+}
+
+void
+timeq_del(time_t when, timeq_func func, void *data, int mask)
+{
+    struct timeq_extra extra;
+    extra.when = when;
+    extra.func = func;
+    extra.data = data;
+    extra.mask = mask;
+    heap_remove_pred(timeq, timeq_del_matching, &extra);
+}
+
+unsigned int
+timeq_size(void)
+{
+    return heap_size(timeq);
+}
+
+void
+timeq_run(void)
+{
+    void *k, *d;
+    struct timeq_entry *ent;
+    while (heap_size(timeq) > 0) {
+       heap_peek(timeq, &k, &d);
+       if ((time_t)k > now)
+            break;
+       ent = d;
+       heap_pop(timeq);
+       ent->func(ent->data);
+        free(ent);
+    }
+}
diff --git a/src/timeq.h b/src/timeq.h
new file mode 100644 (file)
index 0000000..5b75431
--- /dev/null
@@ -0,0 +1,35 @@
+/* timeq.h - time-based event queue
+ * Copyright 2000-2002 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#ifndef TIMEQ_H
+#define TIMEQ_H
+
+typedef void (*timeq_func)(void *data);
+
+#define TIMEQ_IGNORE_WHEN    0x01
+#define TIMEQ_IGNORE_FUNC    0x02
+#define TIMEQ_IGNORE_DATA    0x04
+
+void timeq_init(void);
+void timeq_add(time_t when, timeq_func func, void *data);
+void timeq_del(time_t when, timeq_func func, void *data, int mask);
+time_t timeq_next(void);
+unsigned int timeq_size(void);
+void timeq_run(void);
+
+#endif /* ndef TIMEQ_H */
diff --git a/src/tools.c b/src/tools.c
new file mode 100644 (file)
index 0000000..5026684
--- /dev/null
@@ -0,0 +1,795 @@
+/* tools.c - miscellaneous utility functions
+ * Copyright 2000-2004 srvx Development Team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.  Important limitations are
+ * listed in the COPYING file that accompanies this software.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, email srvx-maintainers@srvx.net.
+ */
+
+#include "log.h"
+#include "nickserv.h"
+#include "recdb.h"
+
+#ifdef HAVE_NETDB_H
+#include <netdb.h>
+#endif
+#ifdef HAVE_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif
+#ifdef HAVE_ARPA_INET_H
+#include <arpa/inet.h>
+#endif
+
+#define NUMNICKLOG 6
+#define NUMNICKBASE (1 << NUMNICKLOG)
+#define NUMNICKMASK (NUMNICKBASE - 1)
+
+/* Yes, P10's encoding here is almost-but-not-quite MIME Base64.  Yay
+ * for gratuitous incompatibilities. */
+static const char convert2y[256] = {
+  'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
+  'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
+  'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
+  'w','x','y','z','0','1','2','3','4','5','6','7','8','9','[',']'
+};
+
+static const unsigned char convert2n[256] = {
+   0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+   0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+   0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  52,53,54,55,56,57,58,59,60,61, 0, 0, 0, 0, 0, 0, 
+   0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
+  15,16,17,18,19,20,21,22,23,24,25,62, 0,63, 0, 0,
+   0,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
+  41,42,43,44,45,46,47,48,49,50,51, 0, 0, 0, 0, 0
+};
+
+unsigned long int
+base64toint(const char* s, int count)
+{
+    unsigned int i = 0;
+    while (*s && count) {
+        i = (i << NUMNICKLOG) + convert2n[(unsigned char)*s++];
+        count--;
+    }
+    return i;
+}
+
+const char* inttobase64(char* buf, unsigned int v, unsigned int count)
+{
+  buf[count] = '\0';
+  while (count > 0) {
+      buf[--count] = convert2y[(unsigned char)(v & NUMNICKMASK)];
+      v >>= NUMNICKLOG;
+  }
+  return buf;
+}
+
+static char irc_tolower[256];
+#undef tolower
+#define tolower(X) irc_tolower[(unsigned char)(X)]
+
+int
+irccasecmp(const char *stra, const char *strb) {
+    while (*stra && (tolower(*stra) == tolower(*strb)))
+        stra++, strb++;
+    return tolower(*stra) - tolower(*strb);
+}
+
+int
+ircncasecmp(const char *stra, const char *strb, unsigned int len) {
+    len--;
+    while (*stra && (tolower(*stra) == tolower(*strb)) && len)
+        stra++, strb++, len--;
+    return tolower(*stra) - tolower(*strb);
+}
+
+const char *
+irccasestr(const char *haystack, const char *needle) {
+    unsigned int hay_len = strlen(haystack), needle_len = strlen(needle), pos;
+    if (hay_len < needle_len)
+        return NULL;
+    for (pos=0; pos<hay_len+1-needle_len; ++pos) {
+        if ((tolower(haystack[pos]) == tolower(*needle))
+            && !ircncasecmp(haystack+pos, needle, needle_len))
+            return haystack+pos;
+    }
+    return NULL;
+}
+
+int
+split_line(char *line, int irc_colon, int argv_size, char *argv[])
+{
+    int argc = 0;
+    int n;
+    while (*line && (argc < argv_size)) {
+       while (*line == ' ') *line++ = 0;
+       if (*line == ':' && irc_colon && argc > 0) {
+           /* the rest is a single parameter */
+           argv[argc++] = line + 1;
+           break;
+       }
+        if (!*line)
+            break;
+       argv[argc++] = line;
+       if (argc >= argv_size)
+            break;
+       while (*line != ' ' && *line) line++;
+    }
+#ifdef NDEBUG
+    n = 0;
+#else
+    for (n=argc; n<argv_size; n++) {
+        argv[n] = (char*)0xFEEDBEEF;
+    }
+#endif
+    return argc;
+}
+
+/* This is ircu's mmatch() function, from match.c. */
+int mmatch(const char *old_mask, const char *new_mask)
+{
+  register const char *m = old_mask;
+  register const char *n = new_mask;
+  const char *ma = m;
+  const char *na = n;
+  int wild = 0;
+  int mq = 0, nq = 0;
+
+  while (1)
+  {
+    if (*m == '*')
+    {
+      while (*m == '*')
+       m++;
+      wild = 1;
+      ma = m;
+      na = n;
+    }
+
+    if (!*m)
+    {
+      if (!*n)
+       return 0;
+      for (m--; (m > old_mask) && (*m == '?'); m--)
+       ;
+      if ((*m == '*') && (m > old_mask) && (m[-1] != '\\'))
+       return 0;
+      if (!wild)
+       return 1;
+      m = ma;
+
+      /* Added to `mmatch' : Because '\?' and '\*' now is one character: */
+      if ((*na == '\\') && ((na[1] == '*') || (na[1] == '?')))
+       ++na;
+
+      n = ++na;
+    }
+    else if (!*n)
+    {
+      while (*m == '*')
+       m++;
+      return (*m != 0);
+    }
+    if ((*m == '\\') && ((m[1] == '*') || (m[1] == '?')))
+    {
+      m++;
+      mq = 1;
+    }
+    else
+      mq = 0;
+
+    /* Added to `mmatch' : Because '\?' and '\*' now is one character: */
+    if ((*n == '\\') && ((n[1] == '*') || (n[1] == '?')))
+    {
+      n++;
+      nq = 1;
+    }
+    else
+      nq = 0;
+
+/*
+ * This `if' has been changed compared to match() to do the following:
+ * Match when:
+ *   old (m)         new (n)         boolean expression
+ *    *               any             (*m == '*' && !mq) ||
+ *    ?               any except '*'  (*m == '?' && !mq && (*n != '*' || nq)) ||
+ * any except * or ?  same as m       (!((*m == '*' || *m == '?') && !mq) &&
+ *                                      toLower(*m) == toLower(*n) &&
+ *                                        !((mq && !nq) || (!mq && nq)))
+ *
+ * Here `any' also includes \* and \? !
+ *
+ * After reworking the boolean expressions, we get:
+ * (Optimized to use boolean shortcircuits, with most frequently occuring
+ *  cases upfront (which took 2 hours!)).
+ */
+    if ((*m == '*' && !mq) ||
+       ((!mq || nq) && tolower(*m) == tolower(*n)) ||
+       (*m == '?' && !mq && (*n != '*' || nq)))
+    {
+      if (*m)
+       m++;
+      if (*n)
+       n++;
+    }
+    else
+    {
+      if (!wild)
+       return 1;
+      m = ma;
+
+      /* Added to `mmatch' : Because '\?' and '\*' now is one character: */
+      if ((*na == '\\') && ((na[1] == '*') || (na[1] == '?')))
+       ++na;
+
+      n = ++na;
+    }
+  }
+}
+
+int
+match_ircglob(const char *text, const char *glob)
+{
+    unsigned int star_p, q_cnt;
+    while (1) {
+       switch (*glob) {
+       case 0:
+           return !*text;
+        case '\\':
+            glob++;
+            /* intentionally not tolower(...) so people can force
+             * capitalization, or we can overload \ in the future */
+            if (*text++ != *glob++) return 0;
+            break;
+       case '*':
+        case '?':
+            star_p = q_cnt = 0;
+            do {
+                if (*glob == '*') star_p = 1;
+                else if (*glob == '?') q_cnt++;
+                else break;
+                glob++;
+            } while (1);
+            while (q_cnt) { if (!*text++) return 0; q_cnt--; }
+            if (star_p) {
+                /* if this is the last glob character, it will match any text */
+                if (!*glob) return 1;
+                /* Thanks to the loop above, we know that the next
+                 * character is a normal character.  So just look for
+                 * the right character.
+                 */
+                for (; *text; text++) {
+                    if ((tolower(*text) == tolower(*glob))
+                        && match_ircglob(text+1, glob+1)) {
+                        return 1;
+                    }
+                }
+                return 0;
+            }
+            /* if !star_p, fall through to normal character case,
+             * first checking to see if ?s carried us to the end */
+            if (!*glob && !*text) return 1;
+       default:
+           if (!*text) return 0;
+           while (*text && *glob && *glob != '*' && *glob != '?' && *glob != '\\') {
+               if (tolower(*text++) != tolower(*glob++)) return 0;
+           }
+       }
+    }
+}
+
+extern const char *hidden_host_suffix;
+
+int
+user_matches_glob(struct userNode *user, const char *orig_glob, int include_nick)
+{
+    char *glob, *marker;
+
+    /* Make a writable copy of the glob */
+    glob = alloca(strlen(orig_glob)+1);
+    strcpy(glob, orig_glob);
+    /* Check the nick, if it's present */
+    if (include_nick) {
+        if (!(marker = strchr(glob, '!'))) {
+            log_module(MAIN_LOG, LOG_ERROR, "user_matches_glob(\"%s\", \"%s\", %d) called, and glob doesn't include a '!'", user->nick, orig_glob, include_nick);
+            return 0;
+        }
+        *marker = 0;
+        if (!match_ircglob(user->nick, glob)) return 0;
+        glob = marker + 1;
+    }
+    /* Check the ident */
+    if (!(marker = strchr(glob, '@'))) {
+        log_module(MAIN_LOG, LOG_ERROR, "user_matches_glob(\"%s\", \"%s\", %d) called, and glob doesn't include an '@'", user->nick, orig_glob, include_nick);
+        return 0;
+    }
+    *marker = 0;
+    if (!match_ircglob(user->ident, glob))
+        return 0;
+    glob = marker + 1;
+    /* Now check the host part */
+    if (isdigit(*glob) && !glob[strspn(glob, "0123456789./*?")]) {
+        /* Looks like an IP-based mask */
+        return match_ircglob(inet_ntoa(user->ip), glob);
+    } else {
+        /* The host part of the mask isn't IP-based */
+        if (hidden_host_suffix && user->handle_info) {
+            char hidden_host[HOSTLEN+1];
+            snprintf(hidden_host, sizeof(hidden_host), "%s.%s", user->handle_info->handle, hidden_host_suffix);
+            if (match_ircglob(hidden_host, glob))
+                return 1;
+        }
+        return match_ircglob(user->hostname, glob);
+    }
+}
+
+int
+is_ircmask(const char *text)
+{
+    while (*text && (isalnum((char)*text) || strchr("-_[]|\\`^{}?*", *text))) text++;
+    if (*text++ != '!') return 0;
+    while (*text && *text != '@' && !isspace((char)*text)) text++;
+    if (*text++ != '@') return 0;
+    while (*text && !isspace((char)*text)) text++;
+    return !*text;
+}
+
+int
+is_gline(const char *text)
+{
+    if (*text == '@') return 0;
+    text += strcspn(text, "@!% \t\r\n");
+    if (*text++ != '@') return 0;
+    if (!*text) return 0;
+    while (*text && (isalnum((char)*text) || strchr(".-?*", *text))) text++;
+    return !*text;
+}
+
+int
+split_ircmask(char *text, char **nick, char **ident, char **host)
+{
+    char *start;
+
+    start = text;
+    while (isalnum((char)*text) || strchr("=[]\\`^{}?*", *text)) text++;
+    if (*text != '!' || ((text - start) > NICKLEN)) return 0;
+    *text = 0;
+    if (nick) *nick = start;
+
+    start = ++text;
+    while (*text && *text != '@' && !isspace((char)*text)) text++;
+    if (*text != '@' || ((text - start) > USERLEN)) return 0;
+    *text = 0;
+    if (ident) *ident = start;
+    
+    start = ++text;
+    while (*text && (isalnum((char)*text) || strchr(".-?*", *text))) text++;
+    if (host) *host = start;
+    return !*text && ((text - start) <= HOSTLEN) && nick && ident && host;
+}
+
+char *
+sanitize_ircmask(char *input)
+{
+    unsigned int length, flag;
+    char *mask, *start, *output;
+
+    /* Sanitize everything in place; input *must* be a valid
+       hostmask. */
+    output = input;
+    flag = 0;
+
+    /* The nick is truncated at the end. */
+    length = 0;
+    mask = input;
+    while(*input++ != '!')
+    {
+       length++;
+    }
+    if(length > NICKLEN)
+    {
+       mask += NICKLEN;
+       *mask++ = '!';
+
+       /* This flag is used to indicate following parts should
+          be shifted. */
+       flag = 1;
+    }
+    else
+    {
+       mask = input;
+    }
+
+    /* The ident and host must be truncated at the beginning and
+       replaced with a '*' to be compatible with ircu. */
+    length = 0;
+    start = input;
+    while(*input++ != '@')
+    {
+       length++;
+    }
+    if(length > USERLEN || flag)
+    {
+       if(length > USERLEN)
+       {
+           start = input - USERLEN;
+           *mask++ = '*';
+       }
+       while(*start != '@')
+       {
+           *mask++ = *start++;
+       }
+       *mask++ = '@';
+
+       flag = 1;
+    }
+    else
+    {
+       mask = input;
+    }
+
+    length = 0;
+    start = input;
+    while(*input++)
+    {
+       length++;
+    }
+    if(length > HOSTLEN || flag)
+    {
+       if(length > HOSTLEN)
+       {
+           start = input - HOSTLEN;
+           *mask++ = '*';
+       }
+       while(*start)
+       {
+           *mask++ = *start++;
+       }
+       *mask = '\0';
+    }
+
+    return output;
+}
+
+static long
+TypeLength(char type)
+{
+    switch (type) {
+    case 'y': return 365*24*60*60;
+    case 'M': return 31*24*60*60;
+    case 'w': return 7*24*60*60;
+    case 'd': return 24*60*60;
+    case 'h': return 60*60;
+    case 'm': return 60;
+    case 's': return 1;
+    default: return 0;
+    }
+}
+
+unsigned long
+ParseInterval(const char *interval)
+{
+    unsigned long seconds = 0;
+    int partial = 0;
+    char c;
+
+    /* process the string, resetting the count if we find a unit character */
+    while ((c = *interval++)) {
+       if (isdigit((int)c)) {
+           partial = partial*10 + c - '0';
+       } else {
+           seconds += TypeLength(c) * partial;
+           partial = 0;
+       }
+    }
+    /* assume the last chunk is seconds (the normal case) */
+    return seconds + partial;
+}
+
+static long
+GetSizeMultiplier(char type)
+{
+    switch (type) {
+    case 'G': case 'g': return 1024*1024*1024;
+    case 'M': case 'm': return 1024*1024;
+    case 'K': case 'k': return 1024;
+    case 'B': case 'b': return 1;
+    default: return 0;
+    }
+}
+
+unsigned long
+ParseVolume(const char *volume)
+{
+    unsigned long accum = 0, partial = 0;
+    char c;
+    while ((c = *volume++)) {
+        if (isdigit((int)c)) {
+            partial = partial*10 + c - '0';
+        } else {
+            accum += GetSizeMultiplier(c) * partial;
+            partial = 0;
+        }
+    }
+    return accum + partial;
+}
+
+int
+parse_ipmask(const char *str, struct in_addr *addr, unsigned long *mask)
+{
+    int accum, pos;
+    unsigned long t_a, t_m;
+
+    t_a = t_m = pos = 0;
+    if (addr) addr->s_addr = htonl(t_a);
+    if (mask) *mask = t_m;
+    while (*str) {
+        if (!isdigit(*str)) return 0;
+        accum = 0;
+        do {
+            accum = (accum * 10) + *str++ - '0';
+        } while (isdigit(*str));
+        if (accum > 255) return 0;
+        t_a = (t_a << 8) | accum;
+        t_m = (t_m << 8) | 255;
+        pos += 8;
+        if (*str == '.') {
+            str++;
+            while (*str == '*') {
+                str++;
+                if (*str == '.') {
+                    t_a <<= 8;
+                    t_m <<= 8;
+                    pos += 8;
+                    str++;
+                } else if (*str == 0) {
+                    t_a <<= 32 - pos;
+                    t_m <<= 32 - pos;
+                    pos = 32;
+                    goto out;
+                } else {
+                    return 0;
+                }
+            }
+        } else if (*str == '/') {
+            int start = pos;
+            accum = 0;
+            do {
+                accum = (accum * 10) + *str++ - '0';
+            } while (isdigit(*str));
+            while (pos < start+accum && pos < 32) {
+                t_a = (t_a << 1) | 0;
+                t_m = (t_m << 1) | 1;
+                pos++;
+            }
+            if (pos != start+accum) return 0;
+        } else if (*str == 0) {
+            break;
+        } else {
+            return 0;
+        }
+    }
+out:
+    if (pos != 32) return 0;
+    if (addr) addr->s_addr = htonl(t_a);
+    if (mask) *mask = t_m;
+    return 1;
+}
+
+char *
+unsplit_string(char *set[], unsigned int max, char *dest)
+{
+    static char unsplit_buffer[MAXLEN*2];
+    unsigned int ii, jj, pos;
+
+    if (!dest) dest = unsplit_buffer;
+    for (ii=pos=0; ii<max; ii++) {
+        for (jj=0; set[ii][jj]; jj++) {
+            dest[pos++] = set[ii][jj];
+        }
+        dest[pos++] = ' ';
+    }
+    dest[--pos] = 0;
+    return dest;
+}
+
+char *
+intervalString2(char *output, time_t interval, int brief)
+{
+    static const struct {
+        const char *name;
+        long length;
+    } unit[] = {
+        { "year", 365 * 24 * 60 * 60 },
+        { "week",   7 * 24 * 60 * 60 },
+        { "day",        24 * 60 * 60 },
+        { "hour",            60 * 60 },
+        { "minute",               60 },
+        { "second",                1 }
+    };
+    unsigned int type, words, pos, count;
+
+    if(!interval)
+    {
+       strcpy(output, brief ? "0s" : "0 seconds");
+       return output;
+    }
+
+    for (type = 0, words = pos = 0;
+         interval && (words < 2) && (type < ArrayLength(unit));
+         type++) {
+       if (interval < unit[type].length)
+            continue;
+        count = interval / unit[type].length;
+        interval = interval % unit[type].length;
+
+        if (brief)
+            pos += sprintf(output + pos, "%d%c", count, unit[type].name[0]);
+        else if (words == 1)
+            pos += sprintf(output + pos, " and %d %s", count, unit[type].name);
+        else
+            pos += sprintf(output + pos, "%d %s", count, unit[type].name);
+        if (count != 1)
+            output[pos++] = 's';
+        words++;
+    }
+
+    output[pos] = 0;
+    return output;
+}
+
+int
+getipbyname(const char *name, unsigned long *ip)
+{
+    struct hostent *he = gethostbyname(name);
+    if (he) {
+       if (he->h_addrtype != AF_INET)
+            return 0;
+       memcpy(ip, he->h_addr_list[0], sizeof(*ip));
+       return 1;
+    } else {
+       return 0;
+    }
+}
+
+DEFINE_LIST(string_buffer, char)
+
+void
+string_buffer_append_substring(struct string_buffer *buf, const char *tail, unsigned int len)
+{
+    while (buf->used + len >= buf->size) {
+        if (!buf->size)
+            buf->size = 16;
+        else
+            buf->size <<= 1;
+        buf->list = realloc(buf->list, buf->size*sizeof(buf->list[0]));
+    }
+    memcpy(buf->list + buf->used, tail, len+1);
+    buf->used += len;
+}
+
+void
+string_buffer_append_string(struct string_buffer *buf, const char *tail)
+{
+    string_buffer_append_substring(buf, tail, strlen(tail));
+}
+
+void
+string_buffer_append_vprintf(struct string_buffer *buf, const char *fmt, va_list args)
+{
+    va_list working;
+    unsigned int len;
+    int ret;
+
+    VA_COPY(working, args);
+    len = strlen(fmt);
+    if (!buf->list || ((buf->used + buf->size) < len)) {
+        buf->size = buf->used + len;
+        buf->list = realloc(buf->list, buf->size);
+    }
+    ret = vsnprintf(buf->list + buf->used, buf->size - buf->used, fmt, working);
+    if (ret <= 0) {
+        /* pre-C99 behavior; double buffer size until it is big enough */
+        va_end(working);
+        VA_COPY(working, args);
+        while ((ret = vsnprintf(buf->list + buf->used, buf->size, fmt, working)) == -1) {
+            buf->size += len;
+            buf->list = realloc(buf->list, buf->size);
+            va_end(working);
+            VA_COPY(working, args);
+        }
+        buf->used += ret;
+    } else if (buf->used + ret < buf->size) {
+        /* no need to increase allocation size */
+        buf->used += ret;
+    } else {
+        /* now we know exactly how much space we need */
+        if (buf->size <= buf->used + ret) {
+            buf->size = buf->used + ret + 1;
+            buf->list = realloc(buf->list, buf->size);
+        }
+        va_end(working);
+        VA_COPY(working, args);
+        buf->used += vsnprintf(buf->list + buf->used, buf->size, fmt, working);
+    }
+    va_end(working);
+    va_end(args);
+}
+
+void string_buffer_append_printf(struct string_buffer *buf, const char *fmt, ...)
+{
+    va_list args;
+    va_start(args, fmt);
+    string_buffer_append_vprintf(buf, fmt, args);
+}
+
+void
+string_buffer_replace(struct string_buffer *buf, unsigned int from, unsigned int len, const char *repl)
+{
+    unsigned int repl_len = strlen(repl);
+    if (from > buf->used) return;
+    if (len + from > buf->used) len = buf->used - from;
+    buf->used = buf->used + repl_len - len;
+    if (buf->size <= buf->used) {
+        while (buf->used >= buf->size) {
+            buf->size <<= 1;
+        }
+        buf->list = realloc(buf->list, buf->size*sizeof(buf->list[0]));
+    }
+    memmove(buf->list+from+repl_len, buf->list+from+len, strlen(buf->list+from+len));
+    strcpy(buf->list+from, repl);
+}
+
+struct string_list str_tab;
+
+const char *
+strtab(unsigned int ii) {
+    if (ii > 65536) return NULL;
+    if (ii > str_tab.size) {
+        unsigned int old_size = str_tab.size;
+        while (ii >= str_tab.size) str_tab.size <<= 1;
+        str_tab.list = realloc(str_tab.list, str_tab.size*sizeof(str_tab.list[0]));
+        memset(str_tab.list+old_size, 0, (str_tab.size-old_size)*sizeof(str_tab.list[0]));
+    }
+    if (!str_tab.list[ii]) {
+        str_tab.list[ii] = malloc(12);
+        sprintf(str_tab.list[ii], "%u", ii);
+    }
+    return str_tab.list[ii];
+}
+
+void
+tools_init(void)
+{
+    unsigned int upr, lwr;
+    for (lwr=0; lwr<256; ++lwr) tolower(lwr) = lwr;
+    for (upr='A', lwr='a'; lwr <= 'z'; ++upr, ++lwr) tolower(upr) = lwr;
+#ifdef WITH_PROTOCOL_P10
+    for (upr='[', lwr='{'; lwr <= '~'; ++upr, ++lwr) tolower(upr) = lwr;
+    for (upr=0xc0, lwr=0xe0; lwr <= 0xf6; ++upr, ++lwr) tolower(upr) = lwr;
+    for (upr=0xd8, lwr=0xf8; lwr <= 0xfe; ++upr, ++lwr) tolower(upr) = lwr;
+#endif
+    str_tab.size = 1001;
+    str_tab.list = calloc(str_tab.size, sizeof(str_tab.list[0]));
+}
+
+void
+tools_cleanup(void)
+{
+    unsigned int ii;
+    for (ii=0; ii<str_tab.size; ++ii) {
+        if (str_tab.list[ii]) free(str_tab.list[ii]);
+    }
+    free(str_tab.list);
+}
diff --git a/srvx.conf.example b/srvx.conf.example
new file mode 100644 (file)
index 0000000..6c0e841
--- /dev/null
@@ -0,0 +1,381 @@
+// services configuration file (example)
+/* It allows two kinds of comments.  Whitespaces between tokens are
+ * ignored.  All strings (even if they're just numbers) MUST be
+ * enclosed in double quotes.  There must be a semicolon after every
+ * key/value pair.
+ */
+
+// The "uplinks" section describes what servers we can possibly link
+// to.  Each subsection describes one server.
+"uplinks" {
+    "private-network" {
+        // IP address and port the server listens on
+        "address"        "10.0.0.3";
+        "port"           "6660";
+        // What password should we send when we connect?
+        "password"       "passwordtoconnect";
+        // What password should we require our peer to send?
+        // (If it is blank, we do not require a specific password.)
+        "their_password" "passwordtorequire";
+        "enabled"        "1";
+        // How many times should we try to connect before giving up?
+        "max_tries"      "3";
+        // What IP should we bind to?
+        // If you do not specify bind_address, the default is used.
+        // "bind_address"   "192.168.0.10"; // use this ip to link
+    };
+
+    /* unused-uplink is just an example to show you how you can
+     * define more than one uplink (and how you can disable one or
+     * more of them.) */
+    "unused-uplink" {
+        "address"        "10.0.0.4";
+        "port"           "6660";
+        "password"       "passwordtoconnect";
+        "their_password" "passwordtorequire";
+        // If "enabled" is 0, we will not try to use this uplink.
+        "enabled"        "0";
+        "max_tries"      "3";
+    };
+};
+
+// The "services" section configures the services that make up srvx.
+"services" {
+    "nickserv" {
+        "nick" "NickServ";
+        // If you want to by have *@* as the default hostmask, set
+        // default_hostmask.  This is discouraged for security reasons.
+        // "default_hostmask" "1";
+        // do we warn users when someone new auths to their account?
+        "warn_clone_auth" "1";
+        // what is the default maxlogins value?
+        "default_maxlogins" "2";
+        // what is the absolute maxlogins value?
+        "hard_maxlogins" "10";
+        // This names a file that contains easily guessed passwords.
+        // It always contains "password", "<password>" and the user's
+        // account name.
+        "dict_file" "/usr/share/dict/words";
+        // Minimum number of various types of characters permitted in
+        // a password.
+        "password_min_length" "4";
+        "password_min_digits" "1";
+        "password_min_upper" "0";
+        "password_min_lower" "0";
+        // What should valid account and nicks look like?
+        // If valid_nick_regex is omitted, valid_account_regex is used
+        // for both nicks and accounts.
+        // These look funny because "[][-]" is the only way to write the
+        // character class containing the characters ']', '[' and '-'.
+        "valid_account_regex" "^[][_a-z^`'{}|-][][_a-z0-9^`'{}|-]*$";
+        "valid_nick_regex" "^[-_a-z][-_a-z0-9]*$";
+
+        // Should nick ownership be disabled?
+        "disable_nicks" "0";
+        // One account may only own this many nicks.
+        "nicks_per_account" "4";
+        // Send a warning when someone uses a registered nick?
+        "warn_nick_owned" "0";
+        // What to do when someone uses the NickServ "reclaim" command?
+        // This can be one of "none", "warn", "svsnick", or "kill", but
+        // stock ircu does not support svsnick -- you need Bahamut or a
+        // patch for ircu.  no, don't ask srvx developers for the patch.
+        "reclaim_action" "none";
+        // What (else) to do when someone uses a registered nick?
+        // This can be anything "reclaim_action" can be, but it makes
+        // more sense to use the "warn_nick_owned" instead of "warn".
+        "auto_reclaim_action" "none";
+        // How long to wait before doing the auto_reclaim_action?
+        // This is ignored if "auto_reclaim_action" is "none".
+        "auto_reclaim_delay" "0";
+
+        // access control for who can change account flags
+        "flag_levels" {
+            "g" "800";
+            "lc_h" "800"; // specifically lower case h
+            "uc_H" "800"; // .. and upper case H
+            "S" "999";
+            "b" "1";
+        };
+        // and for who can change epithets for staff
+        "set_epithet_level" "800";
+        // what opserv access level do you need to set somebody else's level?
+        "modoper_level" "850";
+
+        // how often should accounts be expired?
+        "account_expire_freq" "1d";
+        // how long until an account with access to any channel(s) expires?
+        "account_expire_delay" "35d";
+        // how long until an account with no access to any channels expires?
+        "nochan_account_expire_delay" "14d";
+        /* "require_qualified" has been removed. It is now
+         * integrated into the modcmd command authorization
+         * and dispatch mechanism.  "/msg OpServ help modcmd"
+         * for details.
+         */
+        // If somebody keeps guessing passwords incorrectly, do we gag them?
+        "autogag_enabled" "1";
+        "autogag_duration" "30m";
+        "auth_policer" {
+            "size" "5";
+            "drain-rate" "0.05";
+        };
+        // How to integrate with email cookies?
+        "email_enabled" "0"; // if set, /mail/enable MUST be set too
+        "email_required" "0"; // ignored unless email_enabled is non-zero
+        "cookie_timeout" "1d"; // how long before we expire cookies?
+        "accounts_per_email" "1"; // you may want to increase this; or not
+        "email_search_level" "600"; // minimum OpServ level to search based on email address
+        "email_visible_level" "800"; // minimum OpServ level to see somebody's email address
+
+        "set_title_level" "900"; // minimum OpServ level to set a title on an account
+        "set_fakehost_level" "1000"; // minimum OpServ level to set a freeform fakehost on an account
+        "titlehost_suffix" "example.net"; // suffix to use with automatically generated fakehosts
+    };
+
+    "opserv" {
+        "nick" "OpServ";
+        // what channel should opserv send debug output to?
+        "debug_channel" "#opserv";
+        "debug_channel_modes" "+tinms";
+        // where to send general alerts (e.g. flood alerts)?
+        "alert_channel" "#ircops";
+        "alert_channel_modes" "+tns";
+        // who to tell about staff auths?
+        "staff_auth_channel" "#opserv";
+        "staff_auth_channel_modes" "+tinms";
+        // how many clones to allow from an untrusted host?
+        "untrusted_max" "4";
+        // how long of a g-line should be issued if the max hosts is exceeded?
+        "clone_gline_duration" "1h";
+        // how long to g-line for ?block (or, by default, for ?trace gline)?
+        "block_gline_duration" "1h";
+        // how long to keep an illegal channel locked down (seconds)?
+        "purge_lock_delay" "60";
+        // channel join flood policer params?
+        "join_policer" {
+            "size" "20";
+            "drain-rate" "1";
+        };
+        // automatically moderate join flooded channels?
+        "join_flood_moderate" "1";
+        // Don't moderate and warn channels unless there are more than
+        // join_flood_moderate_threshold users in the channel. the
+        // value 0 will disable the threshold.
+        "join_flood_moderate_threshold" "50";
+        // new user flood policer params
+        "new_user_policer" {
+            "size" "200";
+            "drain-rate" "3";
+        };
+        // character to make OpServ pay attention to you
+        "trigger" "?";
+    };
+
+    "chanserv" {
+        // You may disable a service by removing its "nick" config item.
+        // That means: SERVICES WILL ONLY WORK IF YOU DEFINE THEIR NICK.
+        // (This is changed relative srvx-1.0.x, which would use default
+        // unless you specified ".disabled".)
+        "nick" "ChanServ";
+        // how long should a person be unseen before resending infoline?
+        "info_delay" "120";
+        // maximum greeting length
+        "max_greetlen" "120";
+        // maximum users in a channel userlist
+        "max_chan_users" "512";
+        // maximum bans on a channel banlist
+        "max_chan_bans" "512";
+        // If DynLimit is on and there are N users in the channel, ChanServ will
+        // try to keep the limit at N+<adjust_threshold>.
+        "adjust_threshold" "15";
+        // .. but ChanServ will only increment or decrement the limit this often.
+        "adjust_delay" "30";
+        // How often to look for channels that have expired?
+        "chan_expire_freq" "3d";
+        // How long is a channel unvisited (by masters or above) before it can be expired?
+        "chan_expire_delay" "30d";
+        // character to make ChanServ pay attention to you
+        "trigger" "!";
+        // what !set options should we show when user calls "!set" with no arguments?
+        "set_shows" ("DefaultTopic", "TopicMask", "Greeting", "UserGreeting", "Modes", "PubCmd", "StrictOp", "AutoOp", "EnfModes", "EnfTopic", "Protect", "Toys", "Setters", "TopicRefresh", "CtcpUsers", "CtcpReaction", "Mod", "Game",
+                "Voice", "UserInfo", "DynLimit", "TopicSnarf", "NoDelete");
+
+        // A list of !8ball responses
+        "8ball" ("Not a chance.",
+                "In your dreams.",
+                "Absolutely!",
+                "Could be, could be.");
+        // channel(s) that support helpers must be in to be helping
+        // if this is a list, any one by itself will do
+        "support_channel" ("#support", "#registration");
+        // maximum number of channels owned by one account before FORCE is required
+        "max_owned" "5";
+        // how long between automatic topic refreshes with TopicRefresh 0
+        "refresh_period" "3h";
+        // what should !access say for various staff?
+        "irc_operator_epithet" "a megalomaniacal power hungry tyrant";
+        "network_helper_epithet" "a wannabe tyrant";
+        "support_helper_epithet" "a wannabe tyrant";
+        // what should a newly registered channel get as its modes?
+        "default_modes" "+nt";
+        // minimum opserv access to set, clear or override nodelete setting?
+        "nodelete_level" "1";
+    };
+
+    "global" {
+        "nick" "Global";
+        // should users get community announcements by default or not?
+        "announcements_default" "on";
+    };
+};
+
+// The modules section gives configuration information to optional modules.
+"modules" {
+    "helpserv" {
+        // The description/fullname field
+        "description" "Help Queue Manager";
+        // HelpServ bots log all of their requests to this file, with
+        // details on when they were opened, closed, their contents,
+        // helper, etc. The file is written in saxdb format for easy
+        // parsing by external programs. Please note that you cannot
+        // use ?set to change this value while srvx is running.
+        "reqlogfile" "helpservreq.log";
+        // How long should a helpserv be inactive (no requests assigned)
+        // before it can be unregistered by the expire command?
+        "expiration" "60d";
+    };
+    "sockcheck" {
+        "max_sockets" "64";  // allow 64 concurrent clients to be checked
+        "max_read" "1024"; // don't read more than 1024 bytes from any client
+        "gline_duration" "1h"; // issue G-lines lasting one hour
+        "max_cache_age" "60"; // only cache results for 60 seconds
+        // "address" "192.168.0.10"; // do proxy tests from this address
+    };
+    "snoop" {
+        // Where to send snoop messages?
+        "channel" "#wherever";
+        // Which bot?
+        "bot" "OpServ";
+        // Show new users and joins from net joins?  (off by default)
+        "show_bursts" "0";
+    };
+    "memoserv" {
+        "bot" "NickServ";
+        "message_expiry" "30d"; // age when messages are deleted; set
+                                // to 0 to disable message expiration
+    };
+};
+
+"policers" {
+    "commands-luser" {
+        "size" "5";
+        "drain-rate" "0.5";
+    };
+};
+
+"rlimits" {
+    "data" "50M";
+    "stack" "6M";
+    "vmem" "100M";
+};
+
+"server" {
+    "hostname" "localhost.domain";
+    "description" "Network Services";
+    "network" "GenericNET";
+    "hidden_host" "users.Generic.NET"; // set this if you enabled ircd/Undernet's +x mode
+    /* hidden_host should match the F:HIDDEN_HOST: line in your ircu's ircd.conf;
+     * srvx does not set the host suffix for users, but must know it when making
+     * things like bans, where it should not show the user's real hostname. */
+    "numeric" "10"; // hint: If you get collisions on link, CHANGE THIS.
+    "max_users" "256"; // You can save a little memory by setting this to a lower value.
+    "force_n2k" "1"; // Use extended (5-digit) numnick for self, even if 3 are possible.
+    "ping_freq" "60";
+    "ping_timeout" "90";
+    "max_cycles" "30"; // max uplink cycles before giving up
+    // Admin information is traditionally: location, location, email
+    "admin" ("IRC Network", "Gotham City, GO", "Mr Commissioner <james.gordon@police.gov>");
+};
+
+// controlling how services (mostly NickServ) send mail
+"mail" {
+    // These options are the defaults.
+    "enable" "0";
+    "mailer" "/usr/sbin/sendmail";
+    "from_address" "admin@poorly.configured.server";
+    // These are not :>
+    "extra_headers" ("X-Ereet-Services: srvx r reet");
+    "body_prefix_first" ("Welcome to our network.  This prefix is used whenever srvx thinks this may be the first time we send email to your address.");
+    "body_prefix" ("This goes before the mail text.", "Each string here is treated as a separate \"paragraph\" for line wrapping.");
+    "body_suffix_first" ("We care a lot about spam.  If you did not request this email, bitch and moan loudly at our opers, and tell our ISP to gank our connection.");
+    "body_suffix" ("PLEASE DO NOT BE ALARMED.  CALMLY BOARD THE AIRCRAFT, STRAP YOUR ARMS ACROSS YOUR BODY, AND JUMP THE HELL OUT OF THE PLANE.", "Yaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaatta!");
+};
+
+// New section in srvx-1.2 to control database locations, etc.
+// If you leave this section out, each database will be in its own file,
+// and they will be written out every half hour.
+"dbs" {
+    // This just illustrates how you can jam every database into one huge ("mondo") file.
+    "ChanServ" { "mondo_section" "ChanServ"; };
+    "gline" { "mondo_section" "gline"; };
+    "Global" { "mondo_section" "Global"; };
+    "HelpServ" { "mondo_section" "HelpServ"; };
+    "modcmd" { "mondo_section" "modcmd"; };
+    "NickServ" { "mondo_section" "NickServ"; };
+    "OpServ" { "mondo_section" "OpServ"; };
+    "sendmail" { "mondo_section" "sendmail"; };
+
+    // These are the options if you want a database to be in its own file.
+    "mondo" {
+        // Where to put it?
+        "filename" "srvx.db";
+        // How often should it be saved?
+        // (You can disable automatic saves by setting this to 0.)
+        "frequency" "30m";
+    };
+};
+
+// New section in srvx-1.2 to control log destinations.
+// If you leave this section out, each service (technically, each log
+// facility) will be in its own file, just like before.
+"logs" {
+    // Two kinds of items exist in this section.
+
+    // One is a facility configuration subsection.  These have the
+    // name of a log facility (one of "ChanServ", "Global",
+    // "HelpServ", "NickServ", "OpServ", "ProxyCheck", or "srvx") and
+    // the value is a subsection. The "srvx" log facility is a
+    // catch-all/fall-back facility.
+    "srvx" {
+        // The "max_age" option says how long to keep log audit entries.
+        "max_age" "10m";
+        // The "max_count" option says how many log audit entries to keep.
+        "max_count" "1024";
+        // Audit (command tracking) entries are discarded if they exceed
+        // either limit: for example, if entry 500 is 10 minutes old, it
+        // will be discarded next time any audit command is logged.
+    };
+
+    // The other kind of item is a target list.  The name of each is a
+    // description of facility-and-severity combinations, and the value
+    // is a string (or list of strings) that describe where matching
+    // events should be logged.  As a special case, the facility * will
+    // specify how to log events regardless of their true facility, and
+    // the severity * will match all severities for a facility.
+    // Log targets use a psuedo-URI syntax:  one of "file:filename",
+    // "std:[out|err|n]" where n is a valid file descriptor, or
+    // "irc:#channel" (nicknames or server masks can be used instead
+    // of channel names, but should be used with care).
+    // The severity is one of "replay", "debug", "command", "info",
+    // "override", "staff", "warning", "error", or "fatal".
+    // WARNING: If any severity except "replay" for a facility is left
+    // unspecified, it will use the default target (for example,
+    // "file:chanserv.log").  For "replay" severity, you must ALWAYS
+    // list a target to log it -- this is because it is very rarely
+    // useful.
+    "*.*" ("std:out", "file:everything.log"); // does NOT suppress any defaults
+    "*.override,staff" "irc:#big-brother"; // report all uses of staff commands
+    "ChanServ.*" "file:chanserv.log"; // duplicates the default behavior
+    "ProxyCheck.*" (); // stop it from logging anything
+};
diff --git a/stamp-h2.in b/stamp-h2.in
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/coverage-2.cmd b/tests/coverage-2.cmd
new file mode 100644 (file)
index 0000000..ba31e5e
--- /dev/null
@@ -0,0 +1,721 @@
+define srv1 irc.clan-dk.org:7701
+define srv1name irc.clan-dk.org
+define srv2 irc.clan-dk.org:7711
+define srv2name irc2.clan-dk.org
+define srvx srvx.clan-dk.org
+define domain troilus.org
+define chanserv AlphaIRC
+define global AlphaIRC
+define memoserv AlphaIRC
+define nickserv AlphaIRC
+define opserv AlphaIRC
+define helpserv CoverageServ
+define helpserv2 C0v3r4g3S3rv
+define opernick test_oper
+define operpass i_r_teh_0p3r
+define testchan #testchan
+
+# Connect, join testing channel, oper up, log in
+connect cl1 test1 test1 %srv1% :Test Bot 1
+:cl1 join %testchan%1
+:cl1 raw :OPER %opernick% %operpass%
+:cl1 privmsg %nickserv% :ACCOUNTINFO
+:cl1 privmsg %nickserv%@%srvx% :AUTH
+:cl1 privmsg %nickserv%@%srvx% :AUTH bogus bogus
+:cl1 privmsg %nickserv%@%srvx% :AUTH testest
+:cl1 privmsg %nickserv% :OSET test1 EPITHET some damn test bot
+:cl1 privmsg %nickserv% :ACCOUNTINFO
+
+# Test common infrastructure things
+:cl1 nick test1_new
+:cl1 nick test1
+:cl1 privmsg %opserv% :REHASH
+:cl1 privmsg %opserv% :REOPEN
+:cl1 privmsg %opserv% :QUERY
+:cl1 privmsg %opserv% :LOG LIMIT 30
+:cl1 privmsg %opserv% :RECONNECT
+:cl1 privmsg %opserv% :HELP WRITE
+:cl1 privmsg %opserv% :WRITE MONDO
+:cl1 privmsg %opserv% :WRITEALL
+:cl1 privmsg %opserv% :STATS DATABASES
+
+# Test global's functionality
+:cl1 privmsg %global% :NOTICE users Hello world!
+:cl1 privmsg %global% :MESSAGE TARGET users DURATION 1h TEXT Hello world (short duration)!
+connect cl2 test2 test2 %srv1% :Test Bot 2
+connect cl3 test3 test3 %srv1% :Test Bot 3
+:cl2 join %testchan%1
+:cl2 privmsg %nickserv%@%srvx% :REGISTER test2 testest
+:cl2 privmsg %global% :LIST
+:cl3 join %testchan%1
+:cl3 privmsg %global% :MESSAGES
+:cl3 privmsg %global% :VERSION
+:cl1 wait cl2,cl3
+:cl1 privmsg %global% :REMOVE 1
+:cl1 privmsg %global% :MESSAGE SOURCELESS pizza TARGET all TARGET helpers TARGET opers TARGET staff TARGET channels DURATION 5s TEXT Hollow world (very short duration).
+:cl1 privmsg %global% :MESSAGE TARGET all
+:cl1 privmsg %global% :NOTICE ANNOUNCEMENT test of announcement code
+:cl1 privmsg %global% :NOTICE CHANNELS test of channel spamming code (sorry! :)
+:cl1 privmsg %global% :NOTICE BOGUS
+:cl1 privmsg %global% :NOTICE DIFFERENTLY BOGUS
+:cl1 privmsg %global% :LIST
+:cl1 privmsg %global% :REMOVE 30
+:cl1 privmsg %global% :MESSAGES
+
+# Test ChanServ functions
+:cl1 privmsg %chanserv% :HELP
+:cl1 privmsg %chanserv% :HELP commands
+:cl1 privmsg %chanserv% :HELP note types
+:cl1 privmsg %chanserv% :VERSION ARCH
+:cl1 privmsg %chanserv% :NETINFO
+:cl1 privmsg %chanserv% :STAFF
+:cl1 privmsg %chanserv% :GOD ON
+:cl1 privmsg %chanserv% :REGISTER %testchan%1
+:cl1 privmsg %chanserv% :REGISTER %testchan%2 test2
+:cl1 privmsg %chanserv% :GOD OFF
+:cl1 privmsg %chanserv% :ADDUSER %testchan%1 OP test2
+:cl1 privmsg %chanserv% :GOD ON
+:cl1 privmsg %testchan%1 :\ 1PING\ 1
+:cl1 privmsg %chanserv% :CREATENOTE url setter all 400
+:cl1 privmsg %chanserv% :%testchan%1 NOTE url http://www.srvx.net/index.php
+:cl1 privmsg %chanserv% :CREATENOTE url privileged 1 privileged 20
+:cl1 privmsg %chanserv% :CREATENOTE url channel owner channel_users 20
+:cl1 privmsg %chanserv% :CREATENOTE url bogus all 20
+:cl1 privmsg %chanserv% :%testchan%1 NOTE
+:cl1 privmsg %chanserv% :REMOVENOTE url
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%1 NOTE
+:cl1 privmsg %chanserv% :REMOVENOTE bogus
+:cl1 privmsg %chanserv% :%testchan%1 DELNOTE bogus
+:cl1 privmsg %chanserv% :%testchan%1 DELNOTE url
+:cl1 privmsg %chanserv% :%testchan%1 NOTE url http://www.srvx.net/
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :REMOVENOTE url FORCE
+:cl1 privmsg %chanserv% :%testchan%1 ADDUSER OP test2
+:cl1 privmsg %chanserv% :%testchan%1 OP test2
+:cl1 privmsg %chanserv% :%testchan%1 OP test3
+:cl2 wait cl1
+:cl2 mode %testchan%1 -clo test3
+:cl1 privmsg %chanserv% :%testchan%1 SET MODES +sntlrcCDk 500 bah
+:cl1 privmsg %chanserv% :%testchan%1 SET MODES -lk
+:cl1 privmsg %chanserv% :%testchan%1 SET ENFMODES 4
+:cl1 privmsg %chanserv% :%testchan%1 SET PROTECT 0
+:cl2 wait cl1
+:cl2 mode %testchan%1 +l 600
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :%testchan%1 SET CTCPUSERS 6
+:cl3 wait cl1
+:cl3 privmsg %testchan%1 :\ 1TIME\ 1
+:cl1 privmsg %chanserv% :EXPIRE
+:cl2 privmsg %chanserv% :%testchan%1 DELETEME a5bfa227
+:cl1 privmsg %chanserv% :NOREGISTER *test2 USUX
+:cl1 privmsg %chanserv% :NOREGISTER %testchan%3 USUX2
+:cl1 privmsg %chanserv% :NOREGISTER #*tch* USUX3
+:cl1 privmsg %chanserv% :NOREGISTER %testchan%3
+:cl1 privmsg %chanserv% :NOREGISTER *test2
+:cl1 privmsg %chanserv% :NOREGISTER *test194
+:cl1 privmsg %chanserv% :NOREGISTER
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %chanserv% :ALLOWREGISTER
+:cl1 privmsg %chanserv% :ALLOWREGISTER *test2
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %chanserv% :ALLOWREGISTER %testchan%3
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %chanserv% :ALLOWREGISTER #*tch*
+:cl1 join %testchan%3
+:cl1 privmsg %opserv% :ADDBAD %testchan%3
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %opserv% :CHANINFO %testchan%3
+:cl1 privmsg %chanserv% :%testchan%1 MOVE %testchan%3
+:cl1 join %testchan%3
+:cl1 privmsg %opserv% :DELBAD %testchan%3
+:cl1 privmsg %opserv% :ADDBAD %testchan%4
+:cl1 privmsg %chanserv% :REGISTER %testchan%4 test2
+:cl1 privmsg %chanserv% :%testchan%1 MOVE %testchan%4
+:cl1 privmsg %opserv% :DELBAD %testchan%4
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %chanserv% :ALLOWREGISTER #pizza
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%3 OPCHAN
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :%testchan%3 CSUSPEND 1m H8!
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%3 UNREGISTER 1234a2ec
+:cl2 privmsg %chanserv% :%testchan%3 OPCHAN
+:cl2 privmsg %chanserv% :%testchan%1 UNREGISTER
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :%testchan%3 CUNSUSPEND
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%3 UNREGISTER
+:cl2 privmsg %chanserv% :%testchan%3 OPCHAN
+:cl2 privmsg %chanserv% :%testchan%3 UNREGISTER 1234a2ec
+:cl1 join %testchan%4
+:cl1 privmsg %chanserv% :%testchan%4 UNREGISTER
+:cl1 privmsg %chanserv% :%testchan%2 MOVE %testchan%4
+:cl1 privmsg %chanserv% :%testchan%4 MERGE %testchan%1
+:cl1 privmsg %chanserv% :%testchan%1 OPCHAN
+:cl1 privmsg %chanserv% :%testchan%1 CLVL test2 bogus
+:cl1 privmsg %chanserv% :%testchan%1 CLVL test2 COOWNER
+:cl1 privmsg %chanserv% :%testchan%1 DELUSER COOWNER test2
+:cl1 privmsg %chanserv% :%testchan%1 MDELOP *
+:cl1 privmsg %chanserv% :%testchan%1 TRIM BANS 1w
+:cl1 privmsg %chanserv% :%testchan%1 TRIM USERS 1w
+:cl1 privmsg %chanserv% :%testchan%1 DOWN
+:cl1 privmsg %chanserv% :%testchan%1 UP
+:cl1 privmsg %chanserv% :UPALL
+:cl1 privmsg %chanserv% :DOWNALL
+:cl1 privmsg %chanserv% :%testchan%1 OP test1
+:cl1 privmsg %chanserv% :%testchan%1 OP test2
+:cl1 privmsg %chanserv% :%testchan%1 DEOP test2
+:cl1 privmsg %chanserv% :%testchan%1 VOICE test2
+:cl1 privmsg %chanserv% :%testchan%1 DEVOICE test2
+:cl1 privmsg %chanserv% :%testchan%1 ADDTIMEDBAN test2 30s WEH8U
+:cl1 privmsg %chanserv% :%testchan%1 BANS
+:cl1 privmsg %chanserv% :%testchan%1 UNBAN test3
+:cl1 privmsg %chanserv% :%testchan%1 DELBAN test2
+:cl1 mode %testchan%1 +bbb abcdef!ghijkl@123456789012345678901234567890mnopqr.stuvwx.yz ghijkl!mnopqr@123456789012345678901234567890stuvwx.yzabcd.ef mnopqr!stuvwx@123456789012345678901234567890yzabcd.efghij.kl
+:cl1 mode %testchan%1 +bbb stuvwx!yzabcd@123456789012345678901234567890efghij.klmnop.qr yzabcd!efghij@123456789012345678901234567890klmnop.qrstuv.wx efghij!klmnop@123456789012345678901234567890qrstuv.wxyzab.cd
+:cl1 mode %testchan%1 +bbb klmnop!qrstuv@123456789012345678901234567890wxyzab.cdefgh.ij qrstuv!wxyzab@123456789012345678901234567890cdefgh.ijklmn.op wxyzab!cdefgh@123456789012345678901234567890ijklmn.opqrst.uv
+:cl1 privmsg %chanserv% :%testchan%1 ADDTIMEDBAN a!b@c.om 15s
+:cl1 privmsg %chanserv% :%testchan%1 UNBANALL
+:cl1 privmsg %chanserv% :%testchan%1 OPEN
+:cl1 privmsg %chanserv% :%testchan%1 ACCESS test2
+:cl1 privmsg %chanserv% :%testchan%1 ACCESS test1
+:cl1 privmsg %chanserv% :%testchan%1 USERS
+:cl1 privmsg %chanserv% :%testchan%1 CSUSPEND 1w WEH8URCHAN
+:cl1 privmsg %chanserv% :%testchan%1 INFO
+:cl1 privmsg %chanserv% :%testchan%1 CUNSUSPEND
+:cl1 privmsg %chanserv% :%testchan%1 PEEK
+:cl1 privmsg %chanserv% :%testchan%1 SETINFO Wraa!
+:cl1 privmsg %chanserv% :%testchan%1 ADDUSER MASTER test2
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%1 SETINFO Arrr!
+:cl1 privmsg %chanserv% :%testchan%1 WIPEINFO test2
+:cl1 privmsg %chanserv% :%testchan%1 SEEN test2
+:cl2 privmsg %chanserv% :%testchan%1 NAMES
+:cl1 privmsg %chanserv% :%testchan%1 EVENTS
+:cl1 privmsg %chanserv% :%testchan%1 SAY Hi
+:cl1 privmsg %chanserv% :%testchan%1 EMOTE burps.
+:cl1 privmsg %chanserv% :CSEARCH PRINT LIMIT 20
+:cl1 privmsg %chanserv% :UNVISITED
+:cl1 privmsg %chanserv% :%testchan%1 SET DEFAULTTOPIC foo bar baz
+:cl1 privmsg %chanserv% :%testchan%1 SET TOPICMASK foo * baz
+:cl1 privmsg %chanserv% :%testchan%1 SET ENFTOPIC 5
+:cl1 privmsg %chanserv% :%testchan%1 SET GREETING Hello non-user!
+:cl1 privmsg %chanserv% :%testchan%1 SET USERGREETING Hello user!
+:cl1 privmsg %chanserv% :%testchan%1 SET PUBCMD 6
+:cl1 privmsg %chanserv% :%testchan%1 SET STRICTOP 5
+:cl1 privmsg %chanserv% :%testchan%1 SET AUTOOP 4
+:cl1 privmsg %chanserv% :%testchan%1 SET PROTECT 0
+:cl1 privmsg %chanserv% :%testchan%1 SET TOYS 0
+:cl1 privmsg %chanserv% :%testchan%1 SET SETTERS 2
+:cl1 privmsg %chanserv% :%testchan%1 SET TOPICREFRESH 1
+:cl1 privmsg %chanserv% :%testchan%1 SET VOICE OFF
+:cl1 privmsg %chanserv% :%testchan%1 SET USERINFO ON
+:cl1 privmsg %chanserv% :%testchan%1 SET DYNLIMIT ON
+:cl1 privmsg %chanserv% :%testchan%1 SET TOPICSNARF OFF
+:cl1 privmsg %chanserv% :%testchan%1 SET PEONINVITE OFF
+:cl1 privmsg %chanserv% :%testchan%1 SET NODELETE ON
+:cl1 privmsg %chanserv% :%testchan%1 SET DYNLIMIT OFF
+:cl1 privmsg %chanserv% :%testchan%1 SET MODES +nt
+:cl1 raw :MODE %testchan%1 +bb abc!def@ghi.com foo!bar@baz.com
+:cl1 raw :MODE %testchan%1 -plkb 500 bah foo!bar@baz.com
+:cl1 raw :MODE %testchan%1 +plkntDrcC 500 bah
+:cl1 raw :CLEARMODE %testchan%1
+:cl1 raw :OPMODE %testchan%1 +oo %chanserv% test1
+:cl1 raw :GLINE +foo@example.com * 3600 :We don't like Examplians.
+:cl1 raw :GLINE -foo@example.com * 3600 :We like you again
+:cl1 privmsg %chanserv% :%testchan%1 UNREGISTER
+:cl1 privmsg %chanserv% :%testchan%1 TOPIC blah blah blah
+:cl1 privmsg %chanserv% :%testchan%1 DEOP %chanserv%
+:cl1 raw :KICK %testchan%1 test2
+:cl1 raw :TOPIC %testchan%1 :Topic set by test1
+:cl1 privmsg %testchan%1 :goodbye
+
+# Test raw protocol functionality
+:cl1 raw :STATS u %srvx%
+:cl1 raw :STATS c %srvx%
+:cl1 raw :VERSION %srvx%
+:cl1 raw :ADMIN %srvx%
+:cl1 raw :WHOIS %nickserv% %nickserv%
+:cl1 join 0
+:cl1 raw :AWAY :doing stuff
+:cl1 raw :AWAY
+:cl1 raw :MODE test1 +iwsdh
+:cl1 raw :KILL test3 :die, foo
+:cl1 raw :MODE test1 -oiwsdh
+
+# Test gline functions
+:cl1 raw :OPER %opernick% %operpass%
+:cl1 privmsg %opserv% :gline a@b.com 1h Test gline 1
+:cl1 privmsg %opserv% :gline b@c.com 1m Test gline 2
+:cl1 privmsg %opserv% :gline b@c.com 1h Test gline 2 (updated)
+:cl1 privmsg %opserv% :gline a@a.com 10 Very short gline
+:cl1 privmsg %opserv% :refreshg %srv1name%
+:cl1 privmsg %opserv% :refreshg
+:cl1 privmsg %opserv% :stats glines
+:cl1 privmsg %opserv% :gtrace print mask *@* limit 5 issuer test1 reason *
+:cl1 privmsg %opserv% :gtrace count mask *@* limit 5 issuer test1 reason *
+:cl1 privmsg %opserv% :gtrace ungline mask *@b.com
+:cl1 privmsg %opserv% :gtrace break mask *@b.com
+:cl1 privmsg %opserv% :trace print ip 66.0.0.0/8 mask *!*@* limit 5
+:cl1 privmsg %opserv% :trace print ip 66.*
+:cl1 mode %testchan%1 +b abc!def@ghi.com
+:cl1 privmsg %opserv% :%testchan%1 BAN def
+:cl1 privmsg %opserv% :%testchan%1 BAN *!*@def.ghi.com
+
+# Test modcmd functions
+:cl1 privmsg %chanserv% :%testchan%1
+:cl1 privmsg %opserv% :TIMECMD BIND %opserv% gumbo *modcmd.bind %opserv% $1- $$
+:cl1 privmsg %opserv% :HELP gumbo
+:cl1 privmsg %opserv% :gumbo gumbo gumbo
+:cl1 privmsg %opserv% :MODCMD gumbo FLAGS gumbo
+:cl1 privmsg %opserv% :MODCMD gumbo FLAGS +gumbo
+:cl1 privmsg %opserv% :MODCMD gumbo FLAGS +disabled,-oper CHANNEL_LEVEL none
+:cl1 privmsg %opserv% :MODCMD gumbo OPER_LEVEL 1001
+:cl1 privmsg %opserv% :MODCMD gumbo ACCOUNT_FLAGS +g WEIGHT 0
+:cl1 privmsg %opserv% :MODCMD gumbo bogus options
+:cl1 privmsg %opserv% :UNBIND %opserv% gumbo
+:cl1 privmsg %opserv% :TIMECMD BIND %opserv% gumbo %opserv%.bind %opserv% $1-
+:cl1 privmsg %opserv% :UNBIND %opserv% gumbo
+:cl1 privmsg %opserv% :STATS
+:cl1 privmsg %opserv% :STATS MODULES
+:cl1 privmsg %opserv% :STATS MODULES MODCMD
+:cl1 privmsg %opserv% :STATS SERVICES
+:cl1 privmsg %opserv% :STATS SERVICES %opserv%
+:cl1 privmsg %opserv% :READHELP OpServ
+:cl1 privmsg %opserv% :SHOWCOMMANDS
+:cl1 privmsg %opserv% :HELPFILES %opserv%
+:cl1 privmsg %chanserv% :COMMAND REGISTER
+
+# Test HelpServ functions
+connect cl3 test3 test3 %srv1% :Test Bot 3
+:cl1 privmsg %opserv% :HELPSERV REGISTER %helpserv% %testchan%1 test1
+:cl1 privmsg %helpserv% :huh?
+:cl1 privmsg %helpserv% :ADDHELPER test2
+:cl1 privmsg %helpserv% :CLVL test2 pizzaboy
+:cl1 privmsg %helpserv% :DELUSER test2
+:cl1 privmsg %helpserv% :DELUSER testy
+:cl1 privmsg %helpserv% :SET PAGETARGET %testchan%1
+:cl1 privmsg %helpserv% :SET PAGETYPE NOTICE
+:cl1 privmsg %helpserv% :SET ALERTPAGETARGET %testchan%1
+:cl1 privmsg %helpserv% :SET ALERTPAGETYPE PRIVMSG
+:cl1 privmsg %helpserv% :SET STATUSPAGETARGET %testchan%1
+:cl1 privmsg %helpserv% :SET STATUSPAGETYPE ONOTICE
+:cl1 privmsg %helpserv% :SET GREETING Hello Earthling!  Please talk to me!
+:cl1 privmsg %helpserv% :SET REQOPENED Your request has been accepted!
+:cl1 privmsg %helpserv% :SET REQASSIGNED Your request has been assigned to a helper!
+:cl1 privmsg %helpserv% :SET REQCLOSED Goodbye and leave us alone next time!
+:cl1 privmsg %helpserv% :SET IDLEDELAY 5m
+:cl1 privmsg %helpserv% :SET WHINEDELAY 3m
+:cl1 privmsg %helpserv% :SET WHINEINTERVAL 3m
+:cl1 privmsg %helpserv% :SET EMPTYINTERVAL 3m
+:cl1 privmsg %helpserv% :SET STALEDELAY 5m
+:cl1 privmsg %helpserv% :SET REQPERSIST PART
+:cl1 privmsg %helpserv% :SET HELPERPERSIST CLOSE
+:cl1 privmsg %helpserv% :SET NOTIFICATION ACCOUNTCHANGES
+:cl1 privmsg %helpserv% :SET REQMAXLEN 5
+:cl1 privmsg %helpserv% :SET IDWRAP 10
+:cl1 privmsg %helpserv% :SET REQONJOIN ON
+:cl1 privmsg %helpserv% :SET AUTOVOICE ON
+:cl1 privmsg %helpserv% :SET AUTODEVOICE ON
+:cl1 privmsg %helpserv% :SET
+:cl1 privmsg %helpserv% :LIST ALL
+:cl3 wait cl1
+:cl3 join %testchan%1
+:cl3 privmsg %helpserv% :eye kant auth 2 my acount test2 plz 2 help!
+:cl1 wait cl3
+:cl1 privmsg %helpserv% :LIST
+:cl1 privmsg %helpserv% :LIST ASSIGNED
+:cl1 privmsg %helpserv% :STATS
+:cl1 privmsg %helpserv% :STATS test1
+:cl1 privmsg %helpserv% :NEXT
+:cl1 privmsg %helpserv% :NEXT
+:cl1 privmsg %helpserv% :PICKUP test3
+:cl1 privmsg %helpserv% :LIST ASSIGNED
+:cl1 privmsg %helpserv% :LIST UNASSIGNED
+:cl1 privmsg %helpserv% :LIST ALL
+:cl1 privmsg %helpserv% :LIST PIZZA
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test5
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test2
+:cl1 privmsg %nickserv% :ALLOWAUTH test3
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test2
+:cl3 wait cl1
+:cl3 nick test4
+:cl3 privmsg %nickserv%@%srvx% :AUTH test2 tested
+:cl3 nick test3
+:cl1 wait cl3
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test2
+:cl1 privmsg %helpserv% :REASSIGN test3 test1
+:cl3 wait cl1
+:cl3 privmsg %nickserv%@%srvx% :AUTH test2 testest
+:cl3 privmsg %helpserv% :THX IT WORX NOW!!
+:cl1 wait cl3
+:cl1 privmsg %helpserv% :LIST ME
+:cl1 privmsg %helpserv% :ADDNOTE george this guy is a tool
+:cl1 privmsg %helpserv% :ADDNOTE test2 this should be the first note that works
+:cl1 privmsg %helpserv% :ADDNOTE *test2 this guy is a tool
+:cl1 privmsg %helpserv% :CLOSE 2
+:cl1 privmsg %helpserv% :SHOW 1
+:cl1 privmsg %helpserv% :CLOSE test3
+:cl1 privmsg %opserv% :RECONNECT
+:cl1 sleep 20
+:cl1 privmsg %helpserv% :HELP
+:cl1 privmsg %helpserv% :HELP COMMANDS
+:cl1 privmsg %helpserv% :HELP BOTS
+:cl1 privmsg %helpserv% :BOTS
+:cl1 privmsg %nickserv% :SET BOGUS
+:cl1 privmsg %nickserv% :SET STYLE DEF
+:cl1 privmsg %helpserv% :HELPERS
+:cl1 privmsg %nickserv% :SET STYLE ZOOT
+:cl1 privmsg %helpserv% :HELPERS
+:cl1 privmsg %helpserv% :VERSION CVS
+:cl1 privmsg %helpserv% :PAGE and i-----i'm calling all you angels
+:cl1 privmsg %helpserv% :STATSREPORT
+:cl1 part %testchan%1
+:cl1 privmsg %opserv% :HELPSERV 
+:cl1 privmsg %opserv% :HELPSERV BOGUS
+:cl1 privmsg %opserv% :HELPSERV PICKUP
+:cl1 privmsg %opserv% :HELPSERV READHELP
+:cl1 privmsg %opserv% :HELPSERV BOTS
+:cl1 privmsg %opserv% :HELPSERV STATS %helpserv%
+:cl1 privmsg %opserv% :HELPSERV STATS %helpserv% test1
+:cl1 privmsg %opserv% :HELPSERV MOVE %helpserv% %helpserv2%
+:cl1 privmsg %opserv% :HELPSERV UNREGISTER %helpserv2%
+
+# Test NickServ functions
+:cl1 privmsg %nickserv% :STATUS
+:cl1 privmsg %nickserv% :VERSION
+:cl1 privmsg %nickserv% :HELP COMMANDS
+:cl1 privmsg %nickserv% :ADDMASK
+:cl1 privmsg %nickserv% :ADDMASK *!**foo@**.bar.com
+:cl1 privmsg %nickserv% :ADDMASK **foo@**.bar.com
+:cl1 privmsg %nickserv% :OADDMASK test1 *!**foo@**.bar.com
+:cl1 privmsg %nickserv% :ODELMASK test1 *!**foo@**.bar.com
+:cl1 privmsg %nickserv% :DELMASK **foo@**.bar.com
+:cl1 privmsg %nickserv% :DELMASK *@*.%domain%
+:cl1 privmsg %nickserv% :SEARCH PRINT HOSTMASK
+:cl1 privmsg %nickserv% :SEARCH PRINT HOSTMASK EXACT *foo@*.bar.com LIMIT 5 REGISTERED >=1m
+# cannot test with email since it breaks profiling.. argh
+:cl3 privmsg %nickserv%@%srvx% :REGISTER test3 bleh
+:cl1 wait cl3
+:cl1 privmsg %nickserv% :OUNREGISTER *bleh
+:cl1 privmsg %nickserv%@%srvx% :OREGISTER test4 bleh *@* test3
+:cl1 privmsg %nickserv%@%srvx% :OREGISTER test4 bleh test3@bar
+:cl1 privmsg %nickserv% :ACCOUNTINFO test3
+:cl1 privmsg %nickserv% :ACCOUNTINFO test3bcd
+:cl1 privmsg %nickserv% :USERINFO test3
+:cl1 privmsg %nickserv% :NICKINFO test3
+:cl1 privmsg %nickserv% :OSET test3
+:cl1 privmsg %nickserv% :OSET jobaba
+:cl1 privmsg %nickserv% :OSET test3 BOGUS
+:cl1 privmsg %nickserv% :OSET test3 FLAGS +f
+:cl1 privmsg %nickserv% :RENAME test4 test3
+:cl3 wait cl1
+:cl3 privmsg %nickserv%@%srvx% :REGISTER test3 bleh
+:cl3 privmsg %nickserv%@%srvx% :AUTH bleh
+:cl1 wait cl3
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test2
+:cl3 wait cl1
+:cl3 nick test4
+:cl3 privmsg %nickserv% :REGNICK
+:cl3 nick test3
+:cl3 privmsg %nickserv%@%srvx% :REGISTER test3 bleh
+:cl3 privmsg %nickserv%@%srvx% :AUTH bleh
+:cl3 privmsg %nickserv%@%srvx% :PASS bleh blargh
+:cl3 privmsg %nickserv%@%srvx% :ADDMASK *@foo.%domain%
+:cl3 privmsg %nickserv%@%srvx% :DELMASK *@foo.%domain%
+:cl3 privmsg %nickserv%@%srvx% :SET
+:cl3 privmsg %nickserv%@%srvx% :SET MAXLOGINS 1
+:cl3 privmsg %nickserv%@%srvx% :RECLAIM test3
+:cl3 privmsg %nickserv%@%srvx% :UNREGNICK test3
+:cl3 privmsg %nickserv%@%srvx% :UNREGISTER bleach
+:cl1 wait cl3
+:cl3 quit
+:cl1 sleep 5
+:cl1 privmsg %nickserv% :RENAME *test4 test3
+:cl1 privmsg %nickserv% :OSET *test3 INFO hi hi hi!
+:cl1 privmsg %nickserv% :OSET *test3 WIDTH 1
+:cl1 privmsg %nickserv% :OSET *test3 WIDTH 80
+:cl1 privmsg %nickserv% :OSET *test3 WIDTH 1000
+:cl1 privmsg %nickserv% :OSET *test3 TABLEWIDTH 1
+:cl1 privmsg %nickserv% :OSET *test3 TABLEWIDTH 80
+:cl1 privmsg %nickserv% :OSET *test3 TABLEWIDTH 1000
+:cl1 privmsg %nickserv% :OSET *test3 COLOR OFF
+:cl1 privmsg %nickserv% :OSET *test3 COLOR ON
+:cl1 privmsg %nickserv% :OSET *test3 COLOR TV
+:cl1 privmsg %nickserv% :OSET *test3 PRIVMSG ON
+:cl1 privmsg %nickserv% :OSET *test3 PRIVMSG OFF
+:cl1 privmsg %nickserv% :OSET *test3 PRIVMSG IGNORED
+:cl1 privmsg %nickserv% :OSET *test3 ANNOUNCEMENTS ON
+:cl1 privmsg %nickserv% :OSET *test3 ANNOUNCEMENTS OFF
+:cl1 privmsg %nickserv% :OSET *test3 ANNOUNCEMENTS ?
+:cl1 privmsg %nickserv% :OSET *test3 ANNOUNCEMENTS ARE NOT SPAM
+:cl1 privmsg %nickserv% :OSET *test3 PASSWORD whocares?
+:cl1 privmsg %nickserv% :ACCOUNTINFO *test3
+:cl1 privmsg %nickserv% :OSET *test3 INFO *
+:cl1 privmsg %nickserv% :OREGISTER test4 bleh *@*
+:cl1 privmsg %nickserv% :OREGISTER test4@bogus bleh *@*
+:cl1 privmsg %nickserv% :OREGNICK *test3 test3a
+:cl1 privmsg %nickserv% :OREGNICK *test3 test3b
+:cl1 privmsg %nickserv% :OREGNICK *test3 test3c
+:cl1 privmsg %nickserv% :OUNREGNICK test3c
+:cl1 privmsg %nickserv% :OUNREGNICK test3b
+:cl1 privmsg %nickserv% :OUNREGNICK test3a
+:cl1 privmsg %chanserv% :REGISTER %testchan%2 *test2
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 *test3
+:cl1 privmsg %chanserv% :%testchan%2 ADDUSER COOWNER *test3
+:cl1 privmsg %chanserv% :%testchan%3 ADDUSER COOWNER *test2
+:cl1 privmsg %chanserv% :%testchan%1 ADDUSER COOWNER *test3
+:cl1 privmsg %chanserv% :%testchan%1 ADDUSER COOWNER *test2
+:cl1 privmsg %nickserv% :MERGE *test3 *test2
+:cl1 privmsg %nickserv% :SET STYLE DEF
+:cl1 privmsg %chanserv% :%testchan%1 USERS
+:cl1 privmsg %chanserv% :%testchan%2 USERS
+:cl1 privmsg %chanserv% :%testchan%3 USERS
+:cl1 privmsg %nickserv% :ACCOUNTINFO *test2
+:cl1 privmsg %nickserv% :OSET *test2 MAXLOGINS 100
+:cl1 privmsg %nickserv% :OSET *test2 MAXLOGINS 1
+:cl1 privmsg %nickserv% :OSET *test2 LEVEL 999
+:cl1 privmsg %nickserv% :OSET *test2 LEVEL 998
+connect cl3 test3 test3 %srv1% :Test Bot 3
+:cl1 sleep 6
+:cl3 wait cl1
+:cl3 privmsg %nickserv%@%srvx% :AUTH test2 testest
+:cl3 privmsg %nickserv% :VACATION
+:cl2 wait cl3
+:cl2 privmsg %nickserv% :GHOST test3
+:cl3 sleep 3
+:cl3 quit
+
+# Test OpServ functions
+:cl1 privmsg %opserv% :ACCESS
+:cl1 privmsg %opserv% :ACCESS *
+:cl1 privmsg %opserv% :CHANINFO %testchan%1
+:cl1 privmsg %opserv% :WHOIS test1
+:cl1 privmsg %opserv% :INVITEME
+:cl1 privmsg %opserv% :JOIN %testchan%1
+:cl1 privmsg %opserv% :PART %testchan%1
+:cl1 privmsg %opserv% :STATS BAD
+:cl1 privmsg %opserv% :STATS GLINES
+:cl1 privmsg %opserv% :STATS LINKS
+:cl1 privmsg %opserv% :STATS MAX
+:cl1 privmsg %opserv% :STATS NETWORK
+:cl1 privmsg %opserv% :STATS NETWORK2
+:cl1 privmsg %opserv% :STATS RESERVED
+:cl1 privmsg %opserv% :STATS TRUSTED
+:cl1 privmsg %opserv% :STATS UPLINK
+:cl1 privmsg %opserv% :STATS UPTIME
+:cl1 privmsg %opserv% :STATS ALERTS
+:cl1 privmsg %opserv% :STATS GAGS
+:cl1 privmsg %opserv% :STATS TIMEQ
+:cl1 privmsg %opserv% :STATS WARN
+:cl1 privmsg %opserv% :VERSION
+:cl1 privmsg %opserv% :HELP COMMANDS
+:cl1 privmsg %opserv% :HELP USER
+:cl1 privmsg %opserv% :TRACE DOMAINS DEPTH 2
+:cl1 privmsg %opserv% :TRACE COUNT LIMIT 3
+:cl1 privmsg %opserv% :TRACE HULA-HOOP LIMIT 3
+:cl1 privmsg %opserv% :CSEARCH PRINT NAME * TOPIC * USERS <3 TIMESTAMP >0 LIMIT 5
+:cl1 privmsg %opserv% :CSEARCH COUNT NAME * TOPIC * USERS <3 TIMESTAMP >0 LIMIT 5
+:cl1 privmsg %opserv% :WARN %testchan%4 quiche eaters live here
+:cl1 privmsg %opserv% :STATS WARN
+:cl1 join %testchan%4
+:cl1 privmsg %opserv% :UNWARN %testchan%4
+:cl1 mode %testchan%4 +bbbsnt a!b@c.com b!c@a.org c!a.b.net
+:cl1 privmsg %opserv% :CLEARBANS %testchan%4
+:cl1 privmsg %opserv% :CLEARMODES %testchan%4
+:cl1 privmsg %opserv% :DEOP %testchan%4 test1
+:cl1 privmsg %opserv% :OP %testchan%4 test1
+:cl1 privmsg %opserv% :DEOPALL %testchan%4
+:cl1 privmsg %opserv% :VOICEALL %testchan%4
+:cl1 privmsg %opserv% :OPALL %testchan%4
+:cl1 privmsg %opserv% :JUPE crap.tacular.net 4095 Craptacular Jupe Server
+:cl1 privmsg %opserv% :UNJUPE crap.tacular.net
+:cl1 privmsg %opserv% :JUMP clan-dk
+:cl1 privmsg %opserv% :GLINE pizza 1y Pizza is not allowed on this network
+:cl1 privmsg %opserv% :GLINE *@* 1w GO AWAY I HATE THE WORLD
+:cl1 privmsg %opserv% :GLINE pizza@thehut.com 0 Fat-laden freak
+:cl1 privmsg %opserv% :GLINE foo@bar.com 1m Testing G-line removal
+:cl1 privmsg %opserv% :UNGLINE foo@bar.com 1m Testing G-line removal
+:cl1 privmsg %opserv% :UNGLINE foo@bar.com 1m Testing G-line removal
+:cl1 privmsg %opserv% :REFRESHG pizza.thehut.com
+:cl1 privmsg %opserv% :GSYNC %srv1name%.illegal
+:cl1 privmsg %opserv% :GSYNC
+:cl1 privmsg %opserv% :WHOIS test1
+:cl1 privmsg %opserv% :JOIN pizza.thehut.com
+:cl1 privmsg %opserv% :JOIN %testchan%4
+:cl1 privmsg %opserv% :JOIN %testchan%4
+:cl1 privmsg %opserv% :KICK %testchan%4 test1
+:cl1 join %testchan%4
+:cl1 privmsg %opserv% :KICKALL %testchan%4
+:cl1 join %testchan%4
+:cl1 privmsg %opserv% :KICKBAN %testchan%4 test1
+:cl1 privmsg %opserv% :PART %testchan%4 hahah u r banned
+:cl1 join %testchan%4
+:cl1 privmsg %opserv% :MODE %testchan%4 +snti
+:cl1 privmsg %opserv% :NICKBAN %testchan%4 test1
+:cl1 privmsg %opserv% :UNBAN %testchan%4 *!*@*.%domain%
+:cl1 privmsg %opserv% :KICKBANALL %testchan%4
+:cl1 part %testchan%4
+:cl1 privmsg %opserv% :COLLIDE test3 foo bar.com nick jupe
+:cl1 privmsg %opserv% :UNRESERVE test3
+:cl1 privmsg %opserv% :RESERVE test3 foo bar.com nick jupe 2
+:cl1 privmsg %opserv% :UNRESERVE test3
+:cl1 privmsg %opserv% :ADDBAD %testchan%4abc
+:cl1 privmsg %opserv% :ADDBAD %testchan%4
+:cl1 privmsg %opserv% :ADDBAD %testchan%4abc EXCEPT
+:cl1 privmsg %opserv% :ADDBAD %testchan%4abc EXCEPT %testchan%4ab
+:cl1 privmsg %opserv% :ADDEXEMPT %testchan%4ab
+:cl1 privmsg %opserv% :DELEXEMPT %testchan%4ab
+:cl1 privmsg %opserv% :ADDTRUST 1.2.3.4 0 1w We like incrementing numbers
+:cl1 privmsg %opserv% :ADDTRUST foo@1.2.3.4 0 1w We like incrementing numbers
+:cl1 privmsg %opserv% :ADDTRUST 1.2.3.4 0 1w We like incrementing numbers
+:cl1 privmsg %opserv% :DELTRUST 1.2.3.4
+:cl1 privmsg %opserv% :CLONE ADD test3 joe.bar.com nick jupe 3
+:cl1 privmsg %opserv% :CLONE ADD test3 joe@bar.com nick jupe 3
+:cl1 privmsg %opserv% :CLONE REMOVE gobbledygook
+:cl1 privmsg %opserv% :CLONE REMOVE %chanserv%
+:cl1 privmsg %opserv% :CLONE bogus test3
+:cl1 privmsg %opserv% :CLONE JOIN test3 %testchan%1
+:cl1 privmsg %opserv% :CLONE OP test3 %testchan%1
+:cl1 privmsg %opserv% :CLONE SAY test3 %testchan%1
+:cl1 privmsg %opserv% :CLONE SAY test3 %testchan%1 HAHA H4X
+:cl1 privmsg %opserv% :CLONE JOIN test3 %testchan%1abc
+:cl1 privmsg %opserv% :CLONE PART test3 %testchan%1
+:cl1 privmsg %opserv% :CLONE REMOVE test3
+:cl1 privmsg %opserv% :GAG test3!*@*.%domain% 1w Clones sux
+connect cl3 test3 test3 %srv2% :Test Bot 3
+:cl1 wait cl3
+:cl1 privmsg %opserv% :ADDALERT test3 kill NICK test3
+:cl1 privmsg %opserv% :DELALERT test3 kill NICK test3
+:cl3 privmsg %nickserv% :HELP
+:cl3 nick test4
+:cl3 privmsg %nickserv% :HELP
+:cl3 nick test3
+:cl3 privmsg %nickserv% :HELP
+:cl1 privmsg %opserv% :UNGAG test3!*@*.%domain%
+:cl1 privmsg %opserv% :SET server/max_users 128
+:cl1 privmsg %opserv% :SETTIME *
+
+# Test MemoServ functions
+:cl1 privmsg %memoserv% :SEND gobble,dy HELLO?
+:cl1 privmsg %memoserv% :SEND test2 HELLO?
+:cl1 privmsg %memoserv% :SET NOTIFY ON
+:cl1 privmsg %memoserv% :SET AUTHNOTIFY ON
+:cl2 wait cl1
+:cl2 privmsg %memoserv% :SET NOTIFY OFF
+:cl2 privmsg %memoserv% :SET AUTHNOTIFY OFF
+:cl2 privmsg %memoserv% :LIST
+:cl2 privmsg %memoserv% :SEND test1 HELLO!
+:cl2 privmsg %memoserv% :DELETE 0
+:cl1 wait cl2
+:cl1 privmsg %memoserv% :SET PRIVATE ON
+:cl2 wait cl1
+:cl2 privmsg %memoserv% :SEND test1 DO YOU STILL LIKE ME?
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :%testchan%1 DELUSER test2
+:cl1 privmsg %nickserv% :RENAME test2 testy
+:cl2 wait cl1
+:cl2 privmsg %memoserv% :SEND test1 DO YOU STILL LIKE ME?
+:cl1 privmsg %memoserv% :LIST
+:cl1 privmsg %memoserv% :READ 1
+:cl1 privmsg %memoserv% :READ 10
+:cl1 privmsg %memoserv% :DELETE 10
+:cl1 privmsg %memoserv% :DELETE ALL
+:cl1 privmsg %memoserv% :DELETE ALL CONFIRM
+:cl1 privmsg %memoserv% :EXPIRE
+:cl1 privmsg %memoserv% :EXPIRY
+:cl1 privmsg %memoserv% :VERSION
+:cl1 privmsg %memoserv% :STATUS
+
+# Test ServerSpy functions
+:cl1 privmsg %opserv% :DISCONNECT
+:cl1 privmsg %opserv% :DISCONNECT
+:cl1 privmsg %opserv% :STATS SERVERSPY
+:cl1 privmsg %opserv% :CONNECT
+:cl1 privmsg %opserv% :CONNECT
+:cl1 privmsg %opserv% :DELMOD hl bogus
+:cl1 privmsg %opserv% :DELMOD hl cstrike
+:cl1 privmsg %opserv% :DELMOD bogus cstrike
+:cl1 privmsg %opserv% :DELGAME hl
+:cl1 privmsg %opserv% :DELGAME hl
+:cl1 privmsg %opserv% :ADDGAME hl Half Life
+:cl1 privmsg %opserv% :ADDGAME hl Half Life
+:cl1 privmsg %opserv% :ADDMOD hl cstrike Counter-Strike
+:cl1 privmsg %opserv% :ADDMOD hl cstrike Counter-Strike
+:cl1 privmsg %opserv% :ADDMOD bogus cstrike Counter-Strike
+:cl1 privmsg %chanserv% :HELP SERVERSPY
+:cl1 privmsg %chanserv% :SERVERSPY GAME hl
+:cl1 privmsg %chanserv% :SERVERSPY NAME Jose
+:cl1 privmsg %chanserv% :SERVERSPY NAME Jose GAME bogus
+:cl1 privmsg %chanserv% :SERVERSPY NAME Jose GAME hl MOD bogus
+:cl1 privmsg %chanserv% :SERVERSPY NAME Jose GAME hl MOD cstrike
+:cl1 privmsg %chanserv% :SERVERSPY NAME *p* GAME hl MOD cstrike
+:cl1 privmsg %chanserv% :SERVERSPY SERVER *?p* GAME hl MOD cstrike
+:cl1 privmsg %chanserv% :%testchan%1 SET GAME
+:cl1 privmsg %chanserv% :%testchan%1 SET GAME bogus
+:cl1 privmsg %chanserv% :%testchan%1 SET GAME hl
+:cl1 privmsg %chanserv% :%testchan%1 SET GAME
+:cl1 privmsg %chanserv% :%testchan%1 SET MOD
+:cl1 privmsg %chanserv% :%testchan%1 SET MOD bogus
+:cl1 privmsg %chanserv% :%testchan%1 SET MOD cstrike
+:cl1 privmsg %chanserv% :%testchan%1 SET MOD
+:cl1 privmsg %chanserv% :%testchan%1 SET CLANTAG [D]
+:cl1 privmsg %chanserv% :%testchan%1 SET CLANTAG [D*
+:cl1 privmsg %chanserv% :%testchan%1 SET CLANTAG
+:cl1 privmsg %chanserv% :%testchan%1 SET SERVERTAG [D]
+:cl1 privmsg %chanserv% :%testchan%1 SET SERVERTAG [D*
+:cl1 privmsg %chanserv% :%testchan%1 SET SERVERTAG
+:cl1 privmsg %chanserv% :%testchan%1 SERVERSPY NAME *p*
+:cl1 privmsg %chanserv% :%testchan%1 LOCATECLAN
+:cl1 privmsg %chanserv% :%testchan%1 LOCATESERVER
+:cl1 privmsg %opserv% :STATS SERVERSPY
+
+# Test proxy checker code
+:cl1 privmsg %opserv% :HOSTSCAN 62.255.216.72
+:cl1 sleep 10
+:cl1 privmsg %opserv% :CLEARHOST 62.255.216.72
+
+# Clean up test channel
+:cl1 privmsg %chanserv% :%testchan%1 SET NODELETE OFF
+:cl1 privmsg %chanserv% :%testchan%1 UNREGISTER
+
+# exit all clients
+:cl2 wait cl1
+:cl2 privmsg %nickserv%@%srvx% :UNREGISTER MY SHIZNIT
+:cl2 privmsg %nickserv%@%srvx% :UNREGISTER testest
+:cl1 wait cl2
+:cl1 quit
+:cl2 quit
+:cl3 quit
+
+# THINGS NOT HIT YET:
+# announcing user modes +w, +s, +d, +g, +h, +x
+# sending bursts with:
+#  user list wrapping to a new line
+#  voiced users on srvx's side
+#  ban list wrapping to a new line (on first ban or on later bans)
+#  sending ERROR
+#  KILL from a real user
+#  sending SVSNICK
+#  sending PART with no reason (not just an empty reason)
+#  sending raw text
+#  calling change_nicklen()
+#  receiving numerics 331, 432 from uplink
+#  receiving AC from uplink
+#  receiving FA from uplink
+#  .. or any other fake host support
+#  receiving voiced users in burst
+#  receiving a burst where remote channel is younger
+#  receiving a KILL from uplink
+#  receiving a SQUIT from uplink
+#  receiving a NOTICE from uplink
+#  receiving a GLINE from uplink
+#  receiving a MODE <nick> change for: +s, +h, +f
+#  receiving a MODE <#channel> change for: +p, -k, -b
+#  receiving a ERROR from uplink
+#  clearing modes for a channel with modes: +t, +n, +D, +r, +c, +C, +b
+#  removing a ban from a channel where an earlier ban doesn't match
+#  mod_chanmode() with MC_NOTIFY flag
+#  various hostmask generation options
diff --git a/tests/coverage.cmd b/tests/coverage.cmd
new file mode 100644 (file)
index 0000000..4a39a84
--- /dev/null
@@ -0,0 +1,687 @@
+# Declarations at first
+define srv1 irc.clan-dk.org:7701
+define srv1name irc.clan-dk.org
+define srv2 irc.clan-dk.org:7711
+define srv2name irc2.clan-dk.org
+define srvx srvx.clan-dk.org
+define domain troilus.org
+# These aren't so likely to change
+define chanserv ChanServ
+define global Global
+define memoserv MemoServ
+define nickserv AuthServ
+define opserv OpServ
+define helpserv CoverageServ
+define helpserv2 C0v3r4g3S3rv
+define opernick test_oper
+define operpass i_r_teh_0p3r
+define testchan #testchan
+
+# Connect, join testing channel, oper up, log in
+connect cl1 test1 test1 %srv1% :Test Bot 1
+:cl1 join %testchan%1
+:cl1 raw :OPER %opernick% %operpass%
+:cl1 privmsg %nickserv% :ACCOUNTINFO
+:cl1 privmsg %nickserv%@%srvx% :AUTH
+:cl1 privmsg %nickserv%@%srvx% :AUTH bogus bogus
+:cl1 privmsg %nickserv%@%srvx% :AUTH testest
+:cl1 privmsg %nickserv% :OSET test1 EPITHET some damn test bot
+:cl1 privmsg %nickserv% :ACCOUNTINFO
+
+# Test common infrastructure things
+:cl1 nick test1_new
+:cl1 nick test1
+:cl1 privmsg %opserv% :REHASH
+:cl1 privmsg %opserv% :REOPEN
+:cl1 privmsg %opserv% :QUERY
+:cl1 privmsg %opserv% :LOG LIMIT 30
+:cl1 privmsg %opserv% :RECONNECT
+:cl1 privmsg %opserv% :HELP WRITE
+:cl1 privmsg %opserv% :WRITE MONDO
+:cl1 privmsg %opserv% :WRITEALL
+:cl1 privmsg %opserv% :STATS DATABASES
+
+# Test global's functionality
+:cl1 privmsg %global% :NOTICE users Hello world!
+:cl1 privmsg %global% :MESSAGE TARGET users DURATION 1h TEXT Hello world (short duration)!
+connect cl2 test2 test2 %srv1% :Test Bot 2
+connect cl3 test3 test3 %srv1% :Test Bot 3
+:cl2 join %testchan%1
+:cl2 privmsg %nickserv%@%srvx% :REGISTER test2 testest
+:cl2 privmsg %global% :LIST
+:cl3 join %testchan%1
+:cl3 privmsg %global% :MESSAGES
+:cl3 privmsg %global% :VERSION
+:cl1 wait cl2,cl3
+:cl1 privmsg %global% :REMOVE 1
+:cl1 privmsg %global% :MESSAGE SOURCELESS pizza TARGET all TARGET helpers TARGET opers TARGET staff TARGET channels DURATION 5s TEXT Hollow world (very short duration).
+:cl1 privmsg %global% :MESSAGE TARGET all
+:cl1 privmsg %global% :NOTICE ANNOUNCEMENT test of announcement code
+:cl1 privmsg %global% :NOTICE CHANNELS test of channel spamming code (sorry! :)
+:cl1 privmsg %global% :NOTICE BOGUS
+:cl1 privmsg %global% :NOTICE DIFFERENTLY BOGUS
+:cl1 privmsg %global% :LIST
+:cl1 privmsg %global% :REMOVE 30
+:cl1 privmsg %global% :MESSAGES
+
+# Test ChanServ functions
+:cl1 privmsg %chanserv% :HELP
+:cl1 privmsg %chanserv% :HELP commands
+:cl1 privmsg %chanserv% :HELP note types
+:cl1 privmsg %chanserv% :VERSION CVS
+:cl1 privmsg %chanserv% :NETINFO
+:cl1 privmsg %chanserv% :STAFF
+:cl1 privmsg %chanserv% :GOD ON
+:cl1 privmsg %chanserv% :REGISTER %testchan%1
+:cl1 privmsg %chanserv% :REGISTER %testchan%2 test2
+:cl1 privmsg %chanserv% :GOD OFF
+:cl1 privmsg %chanserv% :ADDUSER %testchan%1 OP test2
+:cl1 privmsg %chanserv% :GOD ON
+:cl1 privmsg %testchan%1 :\ 1PING\ 1
+:cl1 privmsg %chanserv% :CREATENOTE url setter all 400
+:cl1 privmsg %chanserv% :%testchan%1 NOTE url http://www.srvx.net/index.php
+:cl1 privmsg %chanserv% :CREATENOTE url privileged 1 privileged 20
+:cl1 privmsg %chanserv% :CREATENOTE url channel owner channel_users 20
+:cl1 privmsg %chanserv% :CREATENOTE url bogus all 20
+:cl1 privmsg %chanserv% :%testchan%1 NOTE
+:cl1 privmsg %chanserv% :REMOVENOTE url
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%1 NOTE
+:cl1 privmsg %chanserv% :REMOVENOTE bogus
+:cl1 privmsg %chanserv% :%testchan%1 DELNOTE bogus
+:cl1 privmsg %chanserv% :%testchan%1 DELNOTE url
+:cl1 privmsg %chanserv% :%testchan%1 NOTE url http://www.srvx.net/
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :REMOVENOTE url FORCE
+:cl1 privmsg %chanserv% :%testchan%1 ADDUSER OP test2
+:cl1 privmsg %chanserv% :%testchan%1 OP test2
+:cl1 privmsg %chanserv% :%testchan%1 OP test3
+:cl2 wait cl1
+:cl2 mode %testchan%1 -clo test3
+:cl1 privmsg %chanserv% :%testchan%1 SET MODES +sntlcCDk 500 bah
+:cl1 privmsg %chanserv% :%testchan%1 SET MODES -lk
+:cl1 privmsg %chanserv% :%testchan%1 SET ENFMODES 4
+:cl1 privmsg %chanserv% :%testchan%1 SET PROTECT 0
+:cl2 wait cl1
+:cl2 mode %testchan%1 +l 600
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :%testchan%1 SET CTCPUSERS 6
+:cl3 wait cl1
+:cl3 privmsg %testchan%1 :\ 1TIME\ 1
+:cl1 privmsg %chanserv% :EXPIRE
+:cl2 privmsg %chanserv% :%testchan%1 DELETEME a5bfa227
+:cl1 privmsg %chanserv% :NOREGISTER *test2 USUX
+:cl1 privmsg %chanserv% :NOREGISTER %testchan%3 USUX2
+:cl1 privmsg %chanserv% :NOREGISTER #*tch* USUX3
+:cl1 privmsg %chanserv% :NOREGISTER %testchan%3
+:cl1 privmsg %chanserv% :NOREGISTER *test2
+:cl1 privmsg %chanserv% :NOREGISTER *test194
+:cl1 privmsg %chanserv% :NOREGISTER
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %chanserv% :ALLOWREGISTER
+:cl1 privmsg %chanserv% :ALLOWREGISTER *test2
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %chanserv% :ALLOWREGISTER %testchan%3
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %chanserv% :ALLOWREGISTER #*tch*
+:cl1 join %testchan%3
+:cl1 privmsg %opserv% :ADDBAD %testchan%3
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %opserv% :CHANINFO %testchan%3
+:cl1 privmsg %chanserv% :%testchan%1 MOVE %testchan%3
+:cl1 join %testchan%3
+:cl1 privmsg %opserv% :DELBAD %testchan%3
+:cl1 privmsg %opserv% :ADDBAD %testchan%4
+:cl1 privmsg %chanserv% :REGISTER %testchan%4 test2
+:cl1 privmsg %chanserv% :%testchan%1 MOVE %testchan%4
+:cl1 privmsg %opserv% :DELBAD %testchan%4
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 test2
+:cl1 privmsg %chanserv% :ALLOWREGISTER #pizza
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%3 OPCHAN
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :%testchan%3 CSUSPEND 1m H8!
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%3 UNREGISTER 1234a2ec
+:cl2 privmsg %chanserv% :%testchan%3 OPCHAN
+:cl2 privmsg %chanserv% :%testchan%1 UNREGISTER
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :%testchan%3 CUNSUSPEND
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%3 UNREGISTER
+:cl2 privmsg %chanserv% :%testchan%3 OPCHAN
+:cl2 privmsg %chanserv% :%testchan%3 UNREGISTER 1234a2ec
+:cl1 join %testchan%4
+:cl1 privmsg %chanserv% :%testchan%4 UNREGISTER
+:cl1 privmsg %chanserv% :%testchan%2 MOVE %testchan%4
+:cl1 privmsg %chanserv% :%testchan%4 MERGE %testchan%1
+:cl1 privmsg %chanserv% :%testchan%1 OPCHAN
+:cl1 privmsg %chanserv% :%testchan%1 CLVL test2 bogus
+:cl1 privmsg %chanserv% :%testchan%1 CLVL test2 COOWNER
+:cl1 privmsg %chanserv% :%testchan%1 DELUSER COOWNER test2
+:cl1 privmsg %chanserv% :%testchan%1 MDELOP *
+:cl1 privmsg %chanserv% :%testchan%1 TRIM BANS 1w
+:cl1 privmsg %chanserv% :%testchan%1 TRIM USERS 1w
+:cl1 privmsg %chanserv% :%testchan%1 DOWN
+:cl1 privmsg %chanserv% :%testchan%1 UP
+:cl1 privmsg %chanserv% :UPALL
+:cl1 privmsg %chanserv% :DOWNALL
+:cl1 privmsg %chanserv% :%testchan%1 OP test1
+:cl1 privmsg %chanserv% :%testchan%1 OP test2
+:cl1 privmsg %chanserv% :%testchan%1 DEOP test2
+:cl1 privmsg %chanserv% :%testchan%1 VOICE test2
+:cl1 privmsg %chanserv% :%testchan%1 DEVOICE test2
+:cl1 privmsg %chanserv% :%testchan%1 ADDTIMEDBAN test2 30s WEH8U
+:cl1 privmsg %chanserv% :%testchan%1 BANS
+:cl1 privmsg %chanserv% :%testchan%1 UNBAN test3
+:cl1 privmsg %chanserv% :%testchan%1 DELBAN test2
+:cl1 mode %testchan%1 +bbb abcdef!ghijkl@123456789012345678901234567890mnopqr.stuvwx.yz ghijkl!mnopqr@123456789012345678901234567890stuvwx.yzabcd.ef mnopqr!stuvwx@123456789012345678901234567890yzabcd.efghij.kl
+:cl1 mode %testchan%1 +bbb stuvwx!yzabcd@123456789012345678901234567890efghij.klmnop.qr yzabcd!efghij@123456789012345678901234567890klmnop.qrstuv.wx efghij!klmnop@123456789012345678901234567890qrstuv.wxyzab.cd
+:cl1 mode %testchan%1 +bbb klmnop!qrstuv@123456789012345678901234567890wxyzab.cdefgh.ij qrstuv!wxyzab@123456789012345678901234567890cdefgh.ijklmn.op wxyzab!cdefgh@123456789012345678901234567890ijklmn.opqrst.uv
+:cl1 privmsg %chanserv% :%testchan%1 ADDTIMEDBAN a!b@c.om 15s
+:cl1 privmsg %chanserv% :%testchan%1 UNBANALL
+:cl1 privmsg %chanserv% :%testchan%1 OPEN
+:cl1 privmsg %chanserv% :%testchan%1 ACCESS test2
+:cl1 privmsg %chanserv% :%testchan%1 ACCESS test1
+:cl1 privmsg %chanserv% :%testchan%1 USERS
+:cl1 privmsg %chanserv% :%testchan%1 CSUSPEND 1w WEH8URCHAN
+:cl1 privmsg %chanserv% :%testchan%1 INFO
+:cl1 privmsg %chanserv% :%testchan%1 CUNSUSPEND
+:cl1 privmsg %chanserv% :%testchan%1 PEEK
+:cl1 privmsg %chanserv% :%testchan%1 SETINFO Wraa!
+:cl1 privmsg %chanserv% :%testchan%1 ADDUSER MASTER test2
+:cl2 wait cl1
+:cl2 privmsg %chanserv% :%testchan%1 SETINFO Arrr!
+:cl1 privmsg %chanserv% :%testchan%1 WIPEINFO test2
+:cl1 privmsg %chanserv% :%testchan%1 SEEN test2
+:cl2 privmsg %chanserv% :%testchan%1 NAMES
+:cl1 privmsg %chanserv% :%testchan%1 EVENTS
+:cl1 privmsg %chanserv% :%testchan%1 SAY Hi
+:cl1 privmsg %chanserv% :%testchan%1 EMOTE burps.
+:cl1 privmsg %chanserv% :CSEARCH PRINT LIMIT 20
+:cl1 privmsg %chanserv% :UNVISITED
+:cl1 privmsg %chanserv% :%testchan%1 SET DEFAULTTOPIC foo bar baz
+:cl1 privmsg %chanserv% :%testchan%1 SET TOPICMASK foo * baz
+:cl1 privmsg %chanserv% :%testchan%1 SET ENFTOPIC 5
+:cl1 privmsg %chanserv% :%testchan%1 SET GREETING Hello non-user!
+:cl1 privmsg %chanserv% :%testchan%1 SET USERGREETING Hello user!
+:cl1 privmsg %chanserv% :%testchan%1 SET PUBCMD 6
+:cl1 privmsg %chanserv% :%testchan%1 SET STRICTOP 5
+:cl1 privmsg %chanserv% :%testchan%1 SET AUTOOP 4
+:cl1 privmsg %chanserv% :%testchan%1 SET PROTECT 0
+:cl1 privmsg %chanserv% :%testchan%1 SET TOYS 0
+:cl1 privmsg %chanserv% :%testchan%1 SET SETTERS 2
+:cl1 privmsg %chanserv% :%testchan%1 SET TOPICREFRESH 1
+:cl1 privmsg %chanserv% :%testchan%1 SET VOICE OFF
+:cl1 privmsg %chanserv% :%testchan%1 SET USERINFO ON
+:cl1 privmsg %chanserv% :%testchan%1 SET DYNLIMIT ON
+:cl1 privmsg %chanserv% :%testchan%1 SET TOPICSNARF OFF
+:cl1 privmsg %chanserv% :%testchan%1 SET PEONINVITE OFF
+:cl1 privmsg %chanserv% :%testchan%1 SET NODELETE ON
+:cl1 privmsg %chanserv% :%testchan%1 SET DYNLIMIT OFF
+:cl1 raw :CLEARMODE %testchan%1
+:cl1 raw :OPMODE %testchan%1 +oo %chanserv% test1
+:cl1 privmsg %chanserv% :%testchan%1 UNREGISTER
+:cl1 privmsg %chanserv% :%testchan%1 TOPIC blah blah blah
+:cl1 privmsg %chanserv% :%testchan%1 DEOP %chanserv%
+:cl1 raw :KICK %testchan%1 test2
+:cl1 raw :TOPIC %testchan%1 :Topic set by test1
+:cl1 privmsg %testchan%1 :goodbye
+
+# Test raw protocol functionality
+:cl1 raw :STATS u %srvx%
+:cl1 raw :STATS c %srvx%
+:cl1 raw :VERSION %srvx%
+:cl1 raw :ADMIN %srvx%
+:cl1 raw :WHOIS %nickserv% %nickserv%
+:cl1 join 0
+:cl1 raw :AWAY :doing stuff
+:cl1 raw :AWAY
+:cl1 raw :MODE test1 +iwsdh
+:cl1 raw :KILL test3 :die, foo
+:cl1 raw :MODE test1 -oiwsdh
+
+# Test gline functions
+:cl1 raw :OPER %opernick% %operpass%
+:cl1 privmsg %opserv% :gline a@b.com 1h Test gline 1
+:cl1 privmsg %opserv% :gline b@c.com 1m Test gline 2
+:cl1 privmsg %opserv% :gline b@c.com 1h Test gline 2 (updated)
+:cl1 privmsg %opserv% :gline a@a.com 10 Very short gline
+:cl1 privmsg %opserv% :refreshg %srv1name%
+:cl1 privmsg %opserv% :refreshg
+:cl1 privmsg %opserv% :stats glines
+:cl1 privmsg %opserv% :gtrace print mask *@* limit 5 issuer test1 reason *
+:cl1 privmsg %opserv% :gtrace count mask *@* limit 5 issuer test1 reason *
+:cl1 privmsg %opserv% :gtrace ungline mask *@b.com
+:cl1 privmsg %opserv% :gtrace break mask *@b.com
+:cl1 privmsg %opserv% :trace print ip 66.0.0.0/8 mask *!*@* limit 5
+:cl1 privmsg %opserv% :trace print ip 66.*
+:cl1 mode %testchan%1 +b abc!def@ghi.com
+:cl1 privmsg %opserv% :%testchan%1 BAN def
+:cl1 privmsg %opserv% :%testchan%1 BAN *!*@def.ghi.com
+
+# Test modcmd functions
+:cl1 privmsg %chanserv% :%testchan%1
+:cl1 privmsg %opserv% :TIMECMD BIND %opserv% gumbo *modcmd.bind %opserv% $1- $$
+:cl1 privmsg %opserv% :HELP gumbo
+:cl1 privmsg %opserv% :gumbo gumbo gumbo
+:cl1 privmsg %opserv% :MODCMD gumbo FLAGS gumbo
+:cl1 privmsg %opserv% :MODCMD gumbo FLAGS +gumbo
+:cl1 privmsg %opserv% :MODCMD gumbo FLAGS +disabled,-oper CHANNEL_LEVEL none
+:cl1 privmsg %opserv% :MODCMD gumbo OPER_LEVEL 1001
+:cl1 privmsg %opserv% :MODCMD gumbo ACCOUNT_FLAGS +g WEIGHT 0
+:cl1 privmsg %opserv% :MODCMD gumbo bogus options
+:cl1 privmsg %opserv% :UNBIND %opserv% gumbo
+:cl1 privmsg %opserv% :TIMECMD BIND %opserv% gumbo %opserv%.bind %opserv% $1-
+:cl1 privmsg %opserv% :UNBIND %opserv% gumbo
+:cl1 privmsg %opserv% :STATS
+:cl1 privmsg %opserv% :STATS MODULES
+:cl1 privmsg %opserv% :STATS MODULES MODCMD
+:cl1 privmsg %opserv% :STATS SERVICES
+:cl1 privmsg %opserv% :STATS SERVICES %opserv%
+:cl1 privmsg %opserv% :READHELP OpServ
+:cl1 privmsg %opserv% :SHOWCOMMANDS
+:cl1 privmsg %opserv% :HELPFILES %opserv%
+:cl1 privmsg %chanserv% :COMMAND REGISTER
+
+# Test HelpServ functions
+connect cl3 test3 test3 %srv1% :Test Bot 3
+:cl1 privmsg %opserv% :HELPSERV REGISTER %helpserv% %testchan%1 test1
+:cl1 privmsg %helpserv% :huh?
+:cl1 privmsg %helpserv% :ADDHELPER test2
+:cl1 privmsg %helpserv% :CLVL test2 pizzaboy
+:cl1 privmsg %helpserv% :DELUSER test2
+:cl1 privmsg %helpserv% :DELUSER testy
+:cl1 privmsg %helpserv% :SET PAGETARGET %testchan%1
+:cl1 privmsg %helpserv% :SET PAGETYPE NOTICE
+:cl1 privmsg %helpserv% :SET ALERTPAGETARGET %testchan%1
+:cl1 privmsg %helpserv% :SET ALERTPAGETYPE PRIVMSG
+:cl1 privmsg %helpserv% :SET STATUSPAGETARGET %testchan%1
+:cl1 privmsg %helpserv% :SET STATUSPAGETYPE ONOTICE
+:cl1 privmsg %helpserv% :SET GREETING Hello Earthling!  Please talk to me!
+:cl1 privmsg %helpserv% :SET REQOPENED Your request has been accepted!
+:cl1 privmsg %helpserv% :SET REQASSIGNED Your request has been assigned to a helper!
+:cl1 privmsg %helpserv% :SET REQCLOSED Goodbye and leave us alone next time!
+:cl1 privmsg %helpserv% :SET IDLEDELAY 5m
+:cl1 privmsg %helpserv% :SET WHINEDELAY 3m
+:cl1 privmsg %helpserv% :SET WHINEINTERVAL 3m
+:cl1 privmsg %helpserv% :SET EMPTYINTERVAL 3m
+:cl1 privmsg %helpserv% :SET STALEDELAY 5m
+:cl1 privmsg %helpserv% :SET REQPERSIST PART
+:cl1 privmsg %helpserv% :SET HELPERPERSIST CLOSE
+:cl1 privmsg %helpserv% :SET NOTIFICATION ACCOUNTCHANGES
+:cl1 privmsg %helpserv% :SET REQMAXLEN 5
+:cl1 privmsg %helpserv% :SET IDWRAP 10
+:cl1 privmsg %helpserv% :SET REQONJOIN ON
+:cl1 privmsg %helpserv% :SET AUTOVOICE ON
+:cl1 privmsg %helpserv% :SET AUTODEVOICE ON
+:cl1 privmsg %helpserv% :SET
+:cl1 privmsg %helpserv% :LIST ALL
+:cl3 wait cl1
+:cl3 join %testchan%1
+:cl3 privmsg %helpserv% :eye kant auth 2 my acount test2 plz 2 help!
+:cl1 wait cl3
+:cl1 privmsg %helpserv% :LIST
+:cl1 privmsg %helpserv% :LIST ASSIGNED
+:cl1 privmsg %helpserv% :STATS
+:cl1 privmsg %helpserv% :STATS test1
+:cl1 privmsg %helpserv% :NEXT
+:cl1 privmsg %helpserv% :NEXT
+:cl1 privmsg %helpserv% :PICKUP test3
+:cl1 privmsg %helpserv% :LIST ASSIGNED
+:cl1 privmsg %helpserv% :LIST UNASSIGNED
+:cl1 privmsg %helpserv% :LIST ALL
+:cl1 privmsg %helpserv% :LIST PIZZA
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test5
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test2
+:cl1 privmsg %nickserv% :ALLOWAUTH test3
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test2
+:cl3 wait cl1
+:cl3 nick test4
+:cl3 privmsg %nickserv%@%srvx% :AUTH test2 tested
+:cl3 nick test3
+:cl1 wait cl3
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test2
+:cl1 privmsg %helpserv% :REASSIGN test3 test1
+:cl3 wait cl1
+:cl3 privmsg %nickserv%@%srvx% :AUTH test2 testest
+:cl3 privmsg %helpserv% :THX IT WORX NOW!!
+:cl1 wait cl3
+:cl1 privmsg %helpserv% :LIST ME
+:cl1 privmsg %helpserv% :ADDNOTE george this guy is a tool
+:cl1 privmsg %helpserv% :ADDNOTE test2 this should be the first note that works
+:cl1 privmsg %helpserv% :ADDNOTE *test2 this guy is a tool
+:cl1 privmsg %helpserv% :CLOSE 2
+:cl1 privmsg %helpserv% :SHOW 1
+:cl1 privmsg %helpserv% :CLOSE test3
+:cl1 privmsg %opserv% :RECONNECT
+:cl1 sleep 20
+:cl1 privmsg %helpserv% :HELP
+:cl1 privmsg %helpserv% :HELP COMMANDS
+:cl1 privmsg %helpserv% :HELP BOTS
+:cl1 privmsg %helpserv% :BOTS
+:cl1 privmsg %nickserv% :SET BOGUS
+:cl1 privmsg %nickserv% :SET STYLE DEF
+:cl1 privmsg %helpserv% :HELPERS
+:cl1 privmsg %nickserv% :SET STYLE ZOOT
+:cl1 privmsg %helpserv% :HELPERS
+:cl1 privmsg %helpserv% :VERSION CVS
+:cl1 privmsg %helpserv% :PAGE and i-----i'm calling all you angels
+:cl1 privmsg %helpserv% :STATSREPORT
+:cl1 part %testchan%1
+:cl1 privmsg %opserv% :HELPSERV 
+:cl1 privmsg %opserv% :HELPSERV BOGUS
+:cl1 privmsg %opserv% :HELPSERV PICKUP
+:cl1 privmsg %opserv% :HELPSERV READHELP
+:cl1 privmsg %opserv% :HELPSERV BOTS
+:cl1 privmsg %opserv% :HELPSERV STATS %helpserv%
+:cl1 privmsg %opserv% :HELPSERV STATS %helpserv% test1
+:cl1 privmsg %opserv% :HELPSERV MOVE %helpserv% %helpserv2%
+:cl1 privmsg %opserv% :HELPSERV UNREGISTER %helpserv2%
+
+# Test NickServ functions
+:cl1 privmsg %nickserv% :STATUS
+:cl1 privmsg %nickserv% :VERSION
+:cl1 privmsg %nickserv% :HELP COMMANDS
+:cl1 privmsg %nickserv% :ADDMASK
+:cl1 privmsg %nickserv% :ADDMASK *!**foo@**.bar.com
+:cl1 privmsg %nickserv% :ADDMASK **foo@**.bar.com
+:cl1 privmsg %nickserv% :OADDMASK test1 *!**foo@**.bar.com
+:cl1 privmsg %nickserv% :ODELMASK test1 *!**foo@**.bar.com
+:cl1 privmsg %nickserv% :DELMASK **foo@**.bar.com
+:cl1 privmsg %nickserv% :DELMASK *@*.%domain%
+:cl1 privmsg %nickserv% :SEARCH PRINT HOSTMASK
+:cl1 privmsg %nickserv% :SEARCH PRINT HOSTMASK EXACT *foo@*.bar.com LIMIT 5 REGISTERED >=1m
+# cannot test with email since it breaks profiling.. argh
+:cl3 privmsg %nickserv%@%srvx% :REGISTER test3 bleh
+:cl1 wait cl3
+:cl1 privmsg %nickserv% :OUNREGISTER *bleh
+:cl1 privmsg %nickserv%@%srvx% :OREGISTER test4 bleh *@* test3
+:cl1 privmsg %nickserv%@%srvx% :OREGISTER test4 bleh test3@bar
+:cl1 privmsg %nickserv% :ACCOUNTINFO test3
+:cl1 privmsg %nickserv% :ACCOUNTINFO test3bcd
+:cl1 privmsg %nickserv% :USERINFO test3
+:cl1 privmsg %nickserv% :NICKINFO test3
+:cl1 privmsg %nickserv% :OSET test3
+:cl1 privmsg %nickserv% :OSET jobaba
+:cl1 privmsg %nickserv% :OSET test3 BOGUS
+:cl1 privmsg %nickserv% :OSET test3 FLAGS +f
+:cl1 privmsg %nickserv% :RENAME test4 test3
+:cl3 wait cl1
+:cl3 privmsg %nickserv%@%srvx% :REGISTER test3 bleh
+:cl3 privmsg %nickserv%@%srvx% :AUTH bleh
+:cl1 wait cl3
+:cl1 privmsg %nickserv% :ALLOWAUTH test3 test2
+:cl3 wait cl1
+:cl3 nick test4
+:cl3 privmsg %nickserv% :REGNICK
+:cl3 nick test3
+:cl3 privmsg %nickserv%@%srvx% :REGISTER test3 bleh
+:cl3 privmsg %nickserv%@%srvx% :AUTH bleh
+:cl3 privmsg %nickserv%@%srvx% :PASS bleh blargh
+:cl3 privmsg %nickserv%@%srvx% :ADDMASK *@foo.%domain%
+:cl3 privmsg %nickserv%@%srvx% :DELMASK *@foo.%domain%
+:cl3 privmsg %nickserv%@%srvx% :SET
+:cl3 privmsg %nickserv%@%srvx% :SET MAXLOGINS 1
+:cl3 privmsg %nickserv%@%srvx% :RECLAIM test3
+:cl3 privmsg %nickserv%@%srvx% :UNREGNICK test3
+:cl3 privmsg %nickserv%@%srvx% :UNREGISTER bleach
+:cl1 wait cl3
+:cl3 quit
+:cl1 sleep 5
+:cl1 privmsg %nickserv% :RENAME *test4 test3
+:cl1 privmsg %nickserv% :OSET *test3 INFO hi hi hi!
+:cl1 privmsg %nickserv% :OSET *test3 WIDTH 1
+:cl1 privmsg %nickserv% :OSET *test3 WIDTH 80
+:cl1 privmsg %nickserv% :OSET *test3 WIDTH 1000
+:cl1 privmsg %nickserv% :OSET *test3 TABLEWIDTH 1
+:cl1 privmsg %nickserv% :OSET *test3 TABLEWIDTH 80
+:cl1 privmsg %nickserv% :OSET *test3 TABLEWIDTH 1000
+:cl1 privmsg %nickserv% :OSET *test3 COLOR OFF
+:cl1 privmsg %nickserv% :OSET *test3 COLOR ON
+:cl1 privmsg %nickserv% :OSET *test3 COLOR TV
+:cl1 privmsg %nickserv% :OSET *test3 PRIVMSG ON
+:cl1 privmsg %nickserv% :OSET *test3 PRIVMSG OFF
+:cl1 privmsg %nickserv% :OSET *test3 PRIVMSG IGNORED
+:cl1 privmsg %nickserv% :OSET *test3 ANNOUNCEMENTS ON
+:cl1 privmsg %nickserv% :OSET *test3 ANNOUNCEMENTS OFF
+:cl1 privmsg %nickserv% :OSET *test3 ANNOUNCEMENTS ?
+:cl1 privmsg %nickserv% :OSET *test3 ANNOUNCEMENTS ARE NOT SPAM
+:cl1 privmsg %nickserv% :OSET *test3 PASSWORD whocares?
+:cl1 privmsg %nickserv% :ACCOUNTINFO *test3
+:cl1 privmsg %nickserv% :OSET *test3 INFO *
+:cl1 privmsg %nickserv% :OREGISTER test4 bleh *@*
+:cl1 privmsg %nickserv% :OREGISTER test4@bogus bleh *@*
+:cl1 privmsg %nickserv% :OREGNICK *test3 test3a
+:cl1 privmsg %nickserv% :OREGNICK *test3 test3b
+:cl1 privmsg %nickserv% :OREGNICK *test3 test3c
+:cl1 privmsg %nickserv% :OUNREGNICK test3c
+:cl1 privmsg %nickserv% :OUNREGNICK test3b
+:cl1 privmsg %nickserv% :OUNREGNICK test3a
+:cl1 privmsg %chanserv% :REGISTER %testchan%2 *test2
+:cl1 privmsg %chanserv% :REGISTER %testchan%3 *test3
+:cl1 privmsg %chanserv% :%testchan%2 ADDUSER COOWNER *test3
+:cl1 privmsg %chanserv% :%testchan%3 ADDUSER COOWNER *test2
+:cl1 privmsg %chanserv% :%testchan%1 ADDUSER COOWNER *test3
+:cl1 privmsg %chanserv% :%testchan%1 ADDUSER COOWNER *test2
+:cl1 privmsg %nickserv% :MERGE *test3 *test2
+:cl1 privmsg %nickserv% :SET STYLE DEF
+:cl1 privmsg %chanserv% :%testchan%1 USERS
+:cl1 privmsg %chanserv% :%testchan%2 USERS
+:cl1 privmsg %chanserv% :%testchan%3 USERS
+:cl1 privmsg %nickserv% :ACCOUNTINFO *test2
+:cl1 privmsg %nickserv% :OSET *test2 MAXLOGINS 100
+:cl1 privmsg %nickserv% :OSET *test2 MAXLOGINS 1
+:cl1 privmsg %nickserv% :OSET *test2 LEVEL 999
+:cl1 privmsg %nickserv% :OSET *test2 LEVEL 998
+connect cl3 test3 test3 %srv1% :Test Bot 3
+:cl1 sleep 6
+:cl3 wait cl1
+:cl3 privmsg %nickserv%@%srvx% :AUTH test2 testest
+:cl3 privmsg %nickserv% :VACATION
+:cl2 wait cl3
+:cl2 privmsg %nickserv% :GHOST test3
+:cl3 sleep 3
+:cl3 quit
+
+# Test OpServ functions
+:cl1 privmsg %opserv% :ACCESS
+:cl1 privmsg %opserv% :ACCESS *
+:cl1 privmsg %opserv% :CHANINFO %testchan%1
+:cl1 privmsg %opserv% :WHOIS test1
+:cl1 privmsg %opserv% :INVITEME
+:cl1 privmsg %opserv% :JOIN %testchan%1
+:cl1 privmsg %opserv% :PART %testchan%1
+:cl1 privmsg %opserv% :STATS BAD
+:cl1 privmsg %opserv% :STATS GLINES
+:cl1 privmsg %opserv% :STATS LINKS
+:cl1 privmsg %opserv% :STATS MAX
+:cl1 privmsg %opserv% :STATS NETWORK
+:cl1 privmsg %opserv% :STATS NETWORK2
+:cl1 privmsg %opserv% :STATS RESERVED
+:cl1 privmsg %opserv% :STATS TRUSTED
+:cl1 privmsg %opserv% :STATS UPLINK
+:cl1 privmsg %opserv% :STATS UPTIME
+:cl1 privmsg %opserv% :STATS ALERTS
+:cl1 privmsg %opserv% :STATS GAGS
+:cl1 privmsg %opserv% :STATS TIMEQ
+:cl1 privmsg %opserv% :STATS WARN
+:cl1 privmsg %opserv% :VERSION
+:cl1 privmsg %opserv% :HELP COMMANDS
+:cl1 privmsg %opserv% :HELP USER
+:cl1 privmsg %opserv% :TRACE DOMAINS DEPTH 2
+:cl1 privmsg %opserv% :TRACE COUNT LIMIT 3
+:cl1 privmsg %opserv% :TRACE HULA-HOOP LIMIT 3
+:cl1 privmsg %opserv% :CSEARCH PRINT NAME * TOPIC * USERS <3 TIMESTAMP >0 LIMIT 5
+:cl1 privmsg %opserv% :CSEARCH COUNT NAME * TOPIC * USERS <3 TIMESTAMP >0 LIMIT 5
+:cl1 privmsg %opserv% :WARN %testchan%4 quiche eaters live here
+:cl1 privmsg %opserv% :STATS WARN
+:cl1 join %testchan%4
+:cl1 privmsg %opserv% :UNWARN %testchan%4
+:cl1 mode %testchan%4 +bbbsnt a!b@c.com b!c@a.org c!a.b.net
+:cl1 privmsg %opserv% :CLEARBANS %testchan%4
+:cl1 privmsg %opserv% :CLEARMODES %testchan%4
+:cl1 privmsg %opserv% :DEOP %testchan%4 test1
+:cl1 privmsg %opserv% :OP %testchan%4 test1
+:cl1 privmsg %opserv% :DEOPALL %testchan%4
+:cl1 privmsg %opserv% :VOICEALL %testchan%4
+:cl1 privmsg %opserv% :OPALL %testchan%4
+:cl1 privmsg %opserv% :JUPE crap.tacular.net 4095 Craptacular Jupe Server
+:cl1 privmsg %opserv% :UNJUPE crap.tacular.net
+:cl1 privmsg %opserv% :JUMP clan-dk
+:cl1 privmsg %opserv% :GLINE pizza 1y Pizza is not allowed on this network
+:cl1 privmsg %opserv% :GLINE *@* 1w GO AWAY I HATE THE WORLD
+:cl1 privmsg %opserv% :GLINE pizza@thehut.com 0 Fat-laden freak
+:cl1 privmsg %opserv% :GLINE foo@bar.com 1m Testing G-line removal
+:cl1 privmsg %opserv% :UNGLINE foo@bar.com 1m Testing G-line removal
+:cl1 privmsg %opserv% :UNGLINE foo@bar.com 1m Testing G-line removal
+:cl1 privmsg %opserv% :REFRESHG pizza.thehut.com
+:cl1 privmsg %opserv% :GSYNC %srv1name%.illegal
+:cl1 privmsg %opserv% :GSYNC
+:cl1 privmsg %opserv% :WHOIS test1
+:cl1 privmsg %opserv% :JOIN pizza.thehut.com
+:cl1 privmsg %opserv% :JOIN %testchan%4
+:cl1 privmsg %opserv% :JOIN %testchan%4
+:cl1 privmsg %opserv% :KICK %testchan%4 test1
+:cl1 join %testchan%4
+:cl1 privmsg %opserv% :KICKALL %testchan%4
+:cl1 join %testchan%4
+:cl1 privmsg %opserv% :KICKBAN %testchan%4 test1
+:cl1 privmsg %opserv% :PART %testchan%4 hahah u r banned
+:cl1 join %testchan%4
+:cl1 privmsg %opserv% :MODE %testchan%4 +snti
+:cl1 privmsg %opserv% :NICKBAN %testchan%4 test1
+:cl1 privmsg %opserv% :UNBAN %testchan%4 *!*@*.%domain%
+:cl1 privmsg %opserv% :KICKBANALL %testchan%4
+:cl1 part %testchan%4
+:cl1 privmsg %opserv% :COLLIDE test3 foo bar.com nick jupe
+:cl1 privmsg %opserv% :UNRESERVE test3
+:cl1 privmsg %opserv% :RESERVE test3 foo bar.com nick jupe 2
+:cl1 privmsg %opserv% :UNRESERVE test3
+:cl1 privmsg %opserv% :ADDBAD %testchan%4abc
+:cl1 privmsg %opserv% :ADDBAD %testchan%4
+:cl1 privmsg %opserv% :ADDBAD %testchan%4abc EXCEPT
+:cl1 privmsg %opserv% :ADDBAD %testchan%4abc EXCEPT %testchan%4ab
+:cl1 privmsg %opserv% :ADDEXEMPT %testchan%4ab
+:cl1 privmsg %opserv% :DELEXEMPT %testchan%4ab
+:cl1 privmsg %opserv% :ADDTRUST 1.2.3.4 0 1w We like incrementing numbers
+:cl1 privmsg %opserv% :ADDTRUST foo@1.2.3.4 0 1w We like incrementing numbers
+:cl1 privmsg %opserv% :ADDTRUST 1.2.3.4 0 1w We like incrementing numbers
+:cl1 privmsg %opserv% :DELTRUST 1.2.3.4
+:cl1 privmsg %opserv% :CLONE ADD test3 joe.bar.com nick jupe 3
+:cl1 privmsg %opserv% :CLONE ADD test3 joe@bar.com nick jupe 3
+:cl1 privmsg %opserv% :CLONE REMOVE gobbledygook
+:cl1 privmsg %opserv% :CLONE REMOVE %chanserv%
+:cl1 privmsg %opserv% :CLONE bogus test3
+:cl1 privmsg %opserv% :CLONE JOIN test3 %testchan%1
+:cl1 privmsg %opserv% :CLONE OP test3 %testchan%1
+:cl1 privmsg %opserv% :CLONE SAY test3 %testchan%1
+:cl1 privmsg %opserv% :CLONE SAY test3 %testchan%1 HAHA H4X
+:cl1 privmsg %opserv% :CLONE JOIN test3 %testchan%1abc
+:cl1 privmsg %opserv% :CLONE PART test3 %testchan%1
+:cl1 privmsg %opserv% :CLONE REMOVE test3
+:cl1 privmsg %opserv% :GAG test3!*@*.%domain% 1w Clones sux
+connect cl3 test3 test3 %srv2% :Test Bot 3
+:cl1 wait cl3
+:cl1 privmsg %opserv% :ADDALERT test3 kill NICK test3
+:cl1 privmsg %opserv% :DELALERT test3 kill NICK test3
+:cl3 privmsg %nickserv% :HELP
+:cl3 nick test4
+:cl3 privmsg %nickserv% :HELP
+:cl3 nick test3
+:cl3 privmsg %nickserv% :HELP
+:cl1 privmsg %opserv% :UNGAG test3!*@*.%domain%
+:cl1 privmsg %opserv% :SET server/max_users 128
+:cl1 privmsg %opserv% :SETTIME *
+
+# Test MemoServ functions
+:cl1 privmsg %memoserv% :SEND gobble,dy HELLO?
+:cl1 privmsg %memoserv% :SEND test2 HELLO?
+:cl1 privmsg %memoserv% :SET NOTIFY ON
+:cl1 privmsg %memoserv% :SET AUTHNOTIFY ON
+:cl2 wait cl1
+:cl2 privmsg %memoserv% :SET NOTIFY OFF
+:cl2 privmsg %memoserv% :SET AUTHNOTIFY OFF
+:cl2 privmsg %memoserv% :LIST
+:cl2 privmsg %memoserv% :SEND test1 HELLO!
+:cl2 privmsg %memoserv% :DELETE 0
+:cl1 wait cl2
+:cl1 privmsg %memoserv% :SET PRIVATE ON
+:cl2 wait cl1
+:cl2 privmsg %memoserv% :SEND test1 DO YOU STILL LIKE ME?
+:cl1 wait cl2
+:cl1 privmsg %chanserv% :%testchan%1 DELUSER test2
+:cl1 privmsg %nickserv% :RENAME test2 testy
+:cl2 wait cl1
+:cl2 privmsg %memoserv% :SEND test1 DO YOU STILL LIKE ME?
+:cl1 privmsg %memoserv% :LIST
+:cl1 privmsg %memoserv% :READ 1
+:cl1 privmsg %memoserv% :READ 10
+:cl1 privmsg %memoserv% :DELETE 10
+:cl1 privmsg %memoserv% :DELETE ALL
+:cl1 privmsg %memoserv% :DELETE ALL CONFIRM
+:cl1 privmsg %memoserv% :EXPIRE
+:cl1 privmsg %memoserv% :EXPIRY
+:cl1 privmsg %memoserv% :VERSION
+:cl1 privmsg %memoserv% :STATUS
+
+# Test ServerSpy functions
+:cl1 privmsg %opserv% :DISCONNECT
+:cl1 privmsg %opserv% :DISCONNECT
+:cl1 privmsg %opserv% :STATS SERVERSPY
+:cl1 privmsg %opserv% :CONNECT
+:cl1 privmsg %opserv% :CONNECT
+:cl1 privmsg %opserv% :DELMOD hl bogus
+:cl1 privmsg %opserv% :DELMOD hl cstrike
+:cl1 privmsg %opserv% :DELMOD bogus cstrike
+:cl1 privmsg %opserv% :DELGAME hl
+:cl1 privmsg %opserv% :DELGAME hl
+:cl1 privmsg %opserv% :ADDGAME hl Half Life
+:cl1 privmsg %opserv% :ADDGAME hl Half Life
+:cl1 privmsg %opserv% :ADDMOD hl cstrike Counter-Strike
+:cl1 privmsg %opserv% :ADDMOD hl cstrike Counter-Strike
+:cl1 privmsg %opserv% :ADDMOD bogus cstrike Counter-Strike
+:cl1 privmsg %chanserv% :HELP SERVERSPY
+:cl1 privmsg %chanserv% :SERVERSPY GAME hl
+:cl1 privmsg %chanserv% :SERVERSPY NAME Jose
+:cl1 privmsg %chanserv% :SERVERSPY NAME Jose GAME bogus
+:cl1 privmsg %chanserv% :SERVERSPY NAME Jose GAME hl MOD bogus
+:cl1 privmsg %chanserv% :SERVERSPY NAME Jose GAME hl MOD cstrike
+:cl1 privmsg %chanserv% :SERVERSPY NAME *p* GAME hl MOD cstrike
+:cl1 privmsg %chanserv% :SERVERSPY SERVER *?p* GAME hl MOD cstrike
+:cl1 privmsg %chanserv% :%testchan%1 SET GAME
+:cl1 privmsg %chanserv% :%testchan%1 SET GAME bogus
+:cl1 privmsg %chanserv% :%testchan%1 SET GAME hl
+:cl1 privmsg %chanserv% :%testchan%1 SET GAME
+:cl1 privmsg %chanserv% :%testchan%1 SET MOD
+:cl1 privmsg %chanserv% :%testchan%1 SET MOD bogus
+:cl1 privmsg %chanserv% :%testchan%1 SET MOD cstrike
+:cl1 privmsg %chanserv% :%testchan%1 SET MOD
+:cl1 privmsg %chanserv% :%testchan%1 SET CLANTAG [D]
+:cl1 privmsg %chanserv% :%testchan%1 SET CLANTAG [D*
+:cl1 privmsg %chanserv% :%testchan%1 SET CLANTAG
+:cl1 privmsg %chanserv% :%testchan%1 SET SERVERTAG [D]
+:cl1 privmsg %chanserv% :%testchan%1 SET SERVERTAG [D*
+:cl1 privmsg %chanserv% :%testchan%1 SET SERVERTAG
+:cl1 privmsg %chanserv% :%testchan%1 SERVERSPY NAME *p*
+:cl1 privmsg %chanserv% :%testchan%1 LOCATECLAN
+:cl1 privmsg %chanserv% :%testchan%1 LOCATESERVER
+:cl1 privmsg %opserv% :STATS SERVERSPY
+
+# Test proxy checker code
+:cl1 privmsg %opserv% :HOSTSCAN 62.255.216.72
+:cl1 sleep 10
+:cl1 privmsg %opserv% :CLEARHOST 62.255.216.72
+
+# Clean up test channel
+:cl1 privmsg %chanserv% :%testchan%1 SET NODELETE OFF
+:cl1 privmsg %chanserv% :%testchan%1 UNREGISTER
+
+# exit all clients
+:cl2 wait cl1
+:cl2 privmsg %nickserv%@%srvx% :UNREGISTER MY SHIZNIT
+:cl2 privmsg %nickserv%@%srvx% :UNREGISTER testest
+:cl1 wait cl2
+:cl1 quit
+:cl2 quit
+:cl3 quit
diff --git a/tests/coverage.txt b/tests/coverage.txt
new file mode 100644 (file)
index 0000000..8911acd
--- /dev/null
@@ -0,0 +1,28 @@
+chanserv.c - 68%
+conf.c - effectively 100%
+dict-splay.c - effectively 100%
+gline.c - effectively 100%
+global.c - effectively 100%
+hash.c - 78% (not ReintroduceUser(), wipeout_channel(), AddChannelBan() setting a wider ban, extremely large channel_apply_bans())
+heap.c - 100%
+helpfile.c - except for backup, post-expansion newline handling
+helpserv.c - 68%
+ioset.c - except for certain ioset_find_line_length(), reopen handling
+log.c - 80%
+main.c - enough (all the code called more than once)
+md5.c - effectively 100%
+mod-memoserv.c - 85%
+mod-serverspy.c - 88%
+modcmd.c - 66%
+modules.c - enough (no dependency code, no failure paths)
+nickserv.c - 67%
+opserv.c - 73%
+policer.c - effectively 100%
+proto-p10.c - 75%
+proto-common.c - except for hostmask generation
+recdb.c - except for char-by-char reading
+saxdb.c - except for the standalone database code
+sendmail.c - 16%
+sockcheck.c - 53%
+timeq.c - 100%
+tools.c - 81% (not some glob matching, sanitize_ircmask() with too-long parts)
diff --git a/tests/ircd.conf b/tests/ircd.conf
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/ircd.motd b/tests/ircd.motd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/nickserv.cmd b/tests/nickserv.cmd
new file mode 100644 (file)
index 0000000..f4924a9
--- /dev/null
@@ -0,0 +1,80 @@
+define srv irc.clan-dk.org:7701
+define nickserv-nick NickServ-Ent
+define nickserv %nickserv-nick%@srvx.clan-dk.org
+
+# Log on, join testing channel
+connect cl1 D00dm4n d00dm4n %srv% :Some Dude Man
+:cl1 join #test
+
+# Read a few help topics
+:cl1 privmsg %nickserv-nick% :help
+:cl1 expect %nickserv-nick% notice :\ 2?%nickserv-nick% Help\ 2?
+:cl1 privmsg %nickserv-nick% :help account
+:cl1 expect %nickserv-nick% notice :Account management commands are:
+:cl1 privmsg %nickserv-nick% :help register
+:cl1 expect %nickserv-nick% notice :See Also:
+
+# Try to register (stumbling at first)
+:cl1 privmsg %nickserv-nick% :register
+:cl1 expect %nickserv-nick% notice :"/msg %nickserv% register"
+:cl1 privmsg %nickserv% :register
+:cl1 expect %nickserv-nick% notice :requires more parameters.
+:cl1 privmsg %nickserv% :register D00dm4n sekrit
+:cl1 expect %nickserv-nick% notice :Account.*registered
+:cl1 privmsg %nickserv% :register D00dm4n-2 sekrit
+:cl1 expect %nickserv-nick% notice :You're already authenticated.*rename your
+
+# Connect another client and try to register there
+connect cl2 D00dm4n-2 d00dm4n %srv% :Some Dude Man
+:cl2 join #test
+:cl2 privmsg %nickserv% :register D00dm4n sekrit
+:cl2 expect %nickserv-nick% notice :Account.*already registered
+:cl2 privmsg %nickserv% :register D00dm4n-2 sekrit
+:cl2 expect %nickserv-nick% notice :Account.*been registered
+:cl2 quit Cycling client
+
+# .. now try to auth to an existing account
+:cl1 privmsg %nickserv% :auth D00dm4n sekrit
+:cl1 expect %nickserv-nick% notice :You are already authed.*reconnect
+connect cl3 D00dm4n-2 d00dm4n %srv% :Some Dude Man
+:cl3 privmsg %nickserv% :auth
+:cl3 expect %nickserv-nick% notice :requires more parameters
+:cl3 privmsg %nickserv% :auth D00dm4n-2 not-sekrit
+:cl3 expect %nickserv-nick% notice :Incorrect password
+:cl3 privmsg %nickserv% :auth D00dm4n-2 sekrit
+:cl3 expect %nickserv-nick% notice :I recognize you.
+
+# change some handle settings
+:cl1 privmsg %nickserv% :pass not-sekrit s00p3r-sekrit
+:cl1 expect %nickserv-nick% :Incorrect password
+:cl1 privmsg %nickserv% :pass sekrit s00p3r-sekrit
+:cl1 expect %nickserv-nick% :Password changed
+:cl1 privmsg %nickserv-nick% :set
+:cl1 expect %nickserv-nick% :account settings
+:cl1 privmsg %nickserv-nick% :set bad-option
+:cl1 expect %nickserv-nick% :invalid account setting
+:cl1 privmsg %nickserv-nick% :set info
+:cl1 expect %nickserv-nick% :\ 2?info:
+:cl1 privmsg %nickserv-nick% :set info Test infoline with unique pattern
+:cl1 expect %nickserv-nick% :info:.*Test infoline with unique pattern
+
+# check account info
+:cl1 privmsg %nickserv-nick% :handleinfo
+:cl1 expect %nickserv-nick% :Current nickname
+:cl1 privmsg %nickserv-nick% :handleinfo *d00dm4n
+:cl1 expect %nickserv-nick% :Current nickname
+:cl1 privmsg %nickserv-nick% :handleinfo *d00dm4n-2
+:cl1 expect %nickserv-nick% :Infoline
+:cl1 privmsg %nickserv-nick% :userinfo d00dm4n-2
+:cl1 expect %nickserv-nick% :is authenticated to account Entrope.
+
+# miscellaneous other commands
+:cl1 privmsg %nickserv-nick% :vacation
+:cl1 expect %nickserv-nick% :You are now on vacation
+:cl1 privmsg %nickserv-nick% :status
+:cl1 expect %nickserv-nick% :registered globally
+
+# Unregister our account(s) so we can repeat the script later
+sync cl1,cl3
+:cl1 privmsg %nickserv% :unregister s00p3r-sekrit
+:cl3 privmsg %nickserv% :unregister sekrit
diff --git a/tests/p10.cmd b/tests/p10.cmd
new file mode 100644 (file)
index 0000000..0e60bfe
--- /dev/null
@@ -0,0 +1,28 @@
+define srv1 irc.clan-dk.org:7701
+define srv1-name irc0.clan-dk.org
+define srv2 irc.clan-dk.org:7711
+define srv2-name irc1.clan-dk.org
+define nickserv NickServ-Ent
+
+# Connect one client
+define cl1-nick D00dm4n
+connect cl1 %cl1-nick% d00dm4n %srv1% :Some Dude Man
+:cl1 raw :STATS u
+:cl1 expect %srv1-name% 242 %cl1-nick% :Server Up
+:cl1 expect %srv1-name% 219 u :End of /STATS report
+:cl1 raw :WHOIS %nickserv% %nickserv%
+:cl1 expect %srv1-name% 311 %cl1-nick% %nickserv%
+:cl1 expect %srv1-name% 312 %cl1-nick% %nickserv%
+:cl1 expect %srv1-name% 313 %cl1-nick% %nickserv%
+:cl1 expect %srv1-name% 318 %cl1-nick% %nickserv% :End of /WHOIS list.
+:cl1 raw :WHOIS %nickserv% not-nickserv
+:cl1 expect %srv1-name% 401 %cl1-nick% not-nickserv :No such nick
+:cl1 raw :PING 1
+# TODO: expect
+:cl1 raw :PING %cl1-nick% :%srv1-name%
+:cl1 expect :PONG %srv1-name% %cl1-nick%
+
+define channel #random-channel
+:cl1 join %channel%
+:cl1 mode %channel% +ntspimDlkb 12345 foobar foo!bar@baz
+:cl1 mode %channel% +bbb bar!baz@bat baz!bat@blah bat!blah@foo
diff --git a/tests/srvx.conf b/tests/srvx.conf
new file mode 100644 (file)
index 0000000..a5d392b
--- /dev/null
@@ -0,0 +1,140 @@
+// services configuration file (test network)
+
+"uplinks" {
+        "localhost" {
+                "address"               "localhost";
+                "port"                  "8764";
+                "password"              "irctest";
+                "their_password"        "irctest";
+                "enabled"               "1";
+        };
+};
+
+"services" {
+       "nickserv" {
+               "nick"                  "NickServ";
+                "db_backup_freq"        "1h";
+               "nicks_per_account"     "4";
+               "disable_nicks"         "0";
+                "warn_nick_owned"       "1";
+               "password_min_length"   "4";
+               "password_min_digits"   "1";
+               "password_min_upper"    "0";
+               "password_min_lower"    "0";
+                "valid_account_regex" "^[][_a-z^`'{}|-][][_a-z0-9^`'{}|-]*$";
+                "valid_nick_regex" "^[-_a-z][-_a-z0-9]*$";
+
+                "flag_levels" {
+                        "g" "800";
+                        "lc_h" "800"; // specifically lower case h
+                        "uc_H" "800"; // .. and upper case H
+                        "S" "999";
+                };
+                "set_epithet_level" "800";
+
+               "account_expire_freq" "1d";
+               "account_expire_delay" "35d";
+               "nochan_account_expire_delay" "14d";
+                "require_qualified" "1";
+                "autogag_enabled" "1";
+                "autogag_duration" "30m";
+                "auth_policer" {
+                        "size" "5";
+                        "drain-rate" "0.05";
+                };
+
+                // How to integrate with email cookies?
+                "email_enabled" "0"; // if set, /mail/enable MUST be set too
+                "email_required" "0";
+                "cookie_timeout" "1d";
+                "accounts_per_email" "1"; // you may want to increase this; or not
+                "email_visible_level" "800"; // minimum OpServ level to see somebody's email address
+       };
+
+       "opserv" {
+               "nick" "OpServ";
+               "db_backup_freq" "2700";
+               "debug_channel" "#opserv";
+               "alert_channel" "#ircops";
+                "staff_auth_channel" "#opserv";
+               "untrusted_max" "4";
+                "clone_gline_duration" "1h";
+                "block_gline_duration" "1h";
+               "purge_lock_delay" "60";
+               "join_policer" {
+                       "size" "20";
+                       "drain-rate" "1";
+               };
+               "new_user_policer" {
+                       "size" "200";
+                       "drain-rate" "3";
+               };
+                "trigger" "?";
+       };
+
+       "chanserv" {
+               "nick" "ChanServ";
+               "debug_channel" "#chanserv";
+               "db_backup_freq" "1800";
+               "info_delay" "120";
+               "max_greetlen" "120";
+               "adjust_threshold" "15";
+               "joinflood_threshold" "5";
+               "adjust_delay" "30";
+               "chan_expire_freq" "3d";
+               "chan_expire_delay" "30d";
+               "max_chan_users" "512";
+               "max_chan_bans" "512";
+                "trigger" "!";
+                "lame_tricks" "1";
+                "8ball" ("Not a chance.",
+                         "In your dreams.",
+                         "Absolutely!",
+                         "Could be, could be.");
+                "support_channel" "#support";
+                "max_owned" "5";
+                "refresh_period" "3h";
+       };
+
+       "global" {
+               "nick" "Global";
+               "db_backup_freq" "1000";
+       };
+
+       "helpserv" {
+               "enable" "1";
+               "description" "Help Queue Manager";
+               "db_backup_freq" "900";
+       };
+};
+
+"sockcheck" {
+       "enabled"        "0";
+       "max_sockets"    "64";
+       "max_read"       "1024";
+        "gline_duration" "1h";
+       "max_cache_age"  "60";
+};
+
+"policers" {
+       "commands-luser" {
+               "size" "3";
+               "drain-rate" "0.33";
+       };
+};
+
+"rlimits" {
+        "data" "50M";
+        "stack" "6M";
+        "vmem" "100M";
+};
+
+"server" {
+       "hostname" "srvx.test.net";
+       "description" "Test Network Services";
+        "network" "Testnet";
+       "numeric" "10";
+       "ping_freq" "60";
+       "ping_timeout" "90";
+       "max_cycles" "30";
+};
diff --git a/tests/test-driver.pl b/tests/test-driver.pl
new file mode 100755 (executable)
index 0000000..6696235
--- /dev/null
@@ -0,0 +1,483 @@
+#! /usr/bin/perl -wT
+
+# If you edit this file, please check carefully that the garbage
+# collection isn't broken.  POE is sometimes too clever for our good
+# in finding references to sessions, and keeps running even after we
+# want to stop.
+
+require 5.006;
+
+use warnings;
+use strict;
+use vars;
+use constant DELAY => 2;
+use constant EXPECT_TIMEOUT => 15;
+use constant RECONNECT_TIMEOUT => 5;
+use constant THROTTLED_TIMEOUT => 90;
+
+use FileHandle;
+use POE;
+use POE::Component::IRC;
+
+# this defines commands that take "zero time" to execute
+# (specifically, those which do not send commands from the issuing
+# client to the server)
+our $zero_time = {
+                  expect => 1,
+                  sleep => 1,
+                  wait => 1,
+                 };
+
+# Create the main session and start POE.
+# All the empty anonymous subs are just to make POE:Session::ASSERT_STATES happy.
+POE::Session->create(inline_states =>
+                     {
+                      # POE kernel interaction
+                      _start => \&drv_start,
+                      _child => sub {},
+                      _stop => sub {
+                        my $heap = $_[HEAP];
+                        print "\nThat's all, folks!";
+                        print "(exiting at line $heap->{lineno}: $heap->{line})"
+                          if $heap->{line};
+                        print "\n";
+                      },
+                      _default => \&drv_default,
+                      # generic utilities or miscellaneous functions
+                      heartbeat => \&drv_heartbeat,
+                      timeout_expect => \&drv_timeout_expect,
+                      reconnect => \&drv_reconnect,
+                      enable_client => sub { $_[ARG0]->{ready} = 1; },
+                      disable_client => sub { $_[ARG0]->{ready} = 0; },
+                      die => sub { $_[KERNEL]->signal($_[SESSION], 'TERM'); },
+                      # client-based command issuers
+                      cmd_expect => \&cmd_expect,
+                      cmd_join => \&cmd_generic,
+                      cmd_mode => \&cmd_generic,
+                      cmd_nick => \&cmd_generic,
+                      cmd_notice => \&cmd_message,
+                      cmd_part => \&cmd_generic,
+                      cmd_privmsg => \&cmd_message,
+                      cmd_quit => \&cmd_generic,
+                      cmd_raw => \&cmd_raw,
+                      cmd_sleep => \&cmd_sleep,
+                      cmd_wait => \&cmd_wait,
+                      # handlers for messages from IRC
+                      irc_001 => \&irc_connected, # Welcome to ...
+                      irc_snotice => sub {}, # notice from a server (anonymous/our uplink)
+                      irc_notice => \&irc_notice, # NOTICE to self or channel
+                      irc_msg => \&irc_msg, # PRIVMSG to self
+                      irc_public => \&irc_public, # PRIVMSG to channel
+                      irc_connected => sub {},
+                      irc_ctcp_action => sub {},
+                      irc_ctcp_ping => sub {},
+                      irc_ctcp_time => sub {},
+                      irc_ctcpreply_ping => sub {},
+                      irc_ctcpreply_time => sub {},
+                      irc_invite => sub {},
+                      irc_join => sub {},
+                      irc_kick => sub {},
+                      irc_kill => sub {},
+                      irc_mode => sub {},
+                      irc_nick => sub {},
+                      irc_part => sub {},
+                      irc_ping => sub {},
+                      irc_quit => sub {},
+                      irc_topic => sub {},
+                      irc_error => \&irc_error,
+                      irc_disconnected => \&irc_disconnected,
+                     },
+                     args => [@ARGV]);
+
+$| = 1;
+$poe_kernel->run();
+exit;
+
+# Core/bookkeeping test driver functions
+
+sub drv_start {
+  my ($kernel, $session, $heap) = @_[KERNEL, SESSION, HEAP];
+
+  # initialize heap
+  $heap->{clients} = {}; # session details, indexed by (short) session name
+  $heap->{sessions} = {}; # session details, indexed by session ref
+  $heap->{servers} = {}; # server addresses, indexed by short names
+  $heap->{macros} = {}; # macros
+
+  # Parse arguments
+  foreach my $arg (@_[ARG0..$#_]) {
+    if ($arg =~ /^-D$/) {
+      $heap->{irc_debug} = 1;
+    } elsif ($arg =~ /^-V$/) {
+      $heap->{verbose} = 1;
+    } else {
+      die "Extra command-line argument $arg\n" if $heap->{script};
+      $heap->{script} = new FileHandle($arg, 'r')
+        or die "Unable to open $arg for reading: $!\n";
+    }
+  }
+  die "No test name specified\n" unless $heap->{script};
+
+  # hook in to POE
+  $kernel->alias_set('control');
+  $kernel->yield('heartbeat');
+}
+
+sub drv_heartbeat {
+  my ($kernel, $session, $heap) = @_[KERNEL, SESSION, HEAP];
+  my $script = $heap->{script};
+  my $used = {};
+  my $delay = DELAY;
+
+  while (1) {
+    my ($line, $lineno);
+    if ($heap->{line}) {
+      $line = delete $heap->{line};
+    } elsif (defined($line = <$script>)) {
+      $heap->{lineno} = $.;
+      print ".";
+    } else {
+      # close all connections
+      foreach my $client (values %{$heap->{clients}}) {
+        $kernel->call($client->{irc}, 'quit', "I fell off the end of my script");
+        $client->{quitting} = 1;
+      }
+      # unalias the control session
+      $kernel->alias_remove('control');
+      # die in a few seconds
+      $kernel->delay_set('die', 5);
+      return;
+    }
+
+    chomp $line;
+    # ignore comments and blank lines
+    next if $line =~ /^\#/ or $line !~ /\S/;
+
+    # expand any macros in the line
+    $line =~ s/(?<=[^\\])%(\S+?)%/$heap->{macros}->{$1}
+      or die "Use of undefined macro $1 at $heap->{lineno}\n"/eg;
+    # remove any \-escapes
+    $line =~ s/\\(.)/$1/g;
+    # figure out the type of line
+    if ($line =~ /^define (\S+) (.+)$/i) {
+      # define a new macro
+      $heap->{macros}->{$1} = $2;
+    } elsif ($line =~ /^undef (\S+)$/i) {
+      # remove the macro
+      delete $heap->{macros}->{$1};
+    } elsif ($line =~ /^connect (\S+) (\S+) (\S+) (\S+) :(.+)$/i) {
+      # connect a new session (named $1) to server $4
+      my ($name, $nick, $ident, $server, $userinfo, $port) = ($1, $2, $3, $4, $5, 6667);
+      $server = $heap->{servers}->{$server} || $server;
+      if ($server =~ /(.+):(\d+)/) {
+        $server = $1;
+        $port = $2;
+      }
+      die "Client with nick $nick already exists (line $heap->{lineno})" if $heap->{clients}->{$nick};
+      my $alias = "client_$name";
+      POE::Component::IRC->new($alias)
+          or die "Unable to create new user $nick (line $heap->{lineno}): $!";
+      my $client = { name => $name,
+                     nick => $nick,
+                     ready => 0,
+                     expect => [],
+                     expect_alarms => [],
+                     irc => $kernel->alias_resolve($alias),
+                     params => { Nick     => $nick,
+                                 Server   => $server,
+                                 Port     => $port,
+                                 Username => $ident,
+                                 Ircname  => $userinfo,
+                                 Debug    => $heap->{irc_debug},
+                               }
+                   };
+      $heap->{clients}->{$client->{name}} = $client;
+      $heap->{sessions}->{$client->{irc}} = $client;
+      $kernel->call($client->{irc}, 'register', 'all');
+      $kernel->call($client->{irc}, 'connect', $client->{params});
+      $used->{$name} = 1;
+    } elsif ($line =~ /^sync (.+)$/i) {
+      # do multi-way synchronization between every session named in $1
+      my @synced = split(/,|\s/, $1);
+      # first, check that they exist and are ready
+      foreach my $clnt (@synced) {
+        die "Unknown session name $clnt (line $heap->{lineno})" unless $heap->{clients}->{$clnt};
+        goto REDO unless $heap->{clients}->{$clnt}->{ready};
+      }
+      # next we actually send the synchronization signals
+      foreach my $clnt (@synced) {
+        my $client = $heap->{clients}->{$clnt};
+        $client->{sync_wait} = [map { $_ eq $clnt ? () : $heap->{clients}->{$_}->{nick} } @synced];
+        $kernel->call($client->{irc}, 'notice', $client->{sync_wait}, 'SYNC');
+        $kernel->call($session, 'disable_client', $client);
+      }
+    } elsif ($line =~ /^:(\S+) (\S+)(.*)$/i) {
+      # generic command handler
+      my ($names, $cmd, $args) = ($1, lc($2), $3);
+      my (@avail, @unavail);
+      # figure out whether each listed client is available or not
+      foreach my $c (split ',', $names) {
+        my $client = $heap->{clients}->{$c};
+        if (not $client) {
+          print "ERROR: Unknown session name $c (line $heap->{lineno}; ignoring)\n";
+        } elsif (($used->{$c} and not $zero_time->{$cmd}) or not $client->{ready}) {
+          push @unavail, $c;
+        } else {
+          push @avail, $c;
+        }
+      }
+      # redo command with unavailable clients
+      if (@unavail) {
+        # This will break if the command can cause a redo for
+        # available clients.. this should be fixed sometime
+        $line = ':'.join(',', @unavail).' '.$cmd.$args;
+        $heap->{redo} = 1;
+      }
+      # do command with available clients
+      if (@avail) {
+        # split up the argument part of the line
+        $args =~ /^((?:(?: [^:])|[^ ])+)?(?: :(.+))?$/;
+        $args = [($1 ? split(' ', $1) : ()), ($2 ? $2 : ())];
+        # find the client and figure out if we need to wait
+        foreach my $c (@avail) {
+          my $client = $heap->{clients}->{$c};
+          die "Client $c used twice as source (line $heap->{lineno})" if $used->{c} and not $zero_time->{$cmd};
+          $kernel->call($session, 'cmd_'.$cmd, $client, $args);
+          $used->{$c} = 1 unless $zero_time->{$cmd};
+        }
+      }
+    } else {
+      die "Unrecognized input line $heap->{lineno}: $line";
+    }
+    if ($heap->{redo}) {
+    REDO:
+      delete $heap->{redo};
+      $heap->{line} = $line;
+      last;
+    }
+  }
+  # issue new heartbeat with appropriate delay
+  $kernel->delay_set('heartbeat', $delay);
+}
+
+sub drv_timeout_expect {
+  my ($kernel, $session, $client) = @_[KERNEL, SESSION, ARG0];
+  print "ERROR: Dropping timed-out expectation by $client->{name}: ".join(',', @{$client->{expect}->[0]})."\n";
+  $client->{expect_alarms}->[0] = undef;
+  unexpect($kernel, $session, $client);
+}
+
+sub drv_reconnect {
+  my ($kernel, $session, $client) = @_[KERNEL, SESSION, ARG0];
+  $kernel->call($client->{irc}, 'connect', $client->{params});
+}
+
+sub drv_default {
+  my ($kernel, $heap, $sender, $session, $state, $args) = @_[KERNEL, HEAP, SENDER, SESSION, ARG0, ARG1];
+  if ($state =~ /^irc_(\d\d\d)$/) {
+    my $client = $heap->{sessions}->{$sender};
+    if (@{$client->{expect}}
+        and $args->[0] eq $client->{expect}->[0]->[0]
+        and $client->{expect}->[0]->[1] eq "$1") {
+      my $expect = $client->{expect}->[0];
+      my $mismatch;
+      for (my $x=2; ($x<=$#$expect) and ($x<=$#$args) and not $mismatch; $x++) {
+        $mismatch = 1 unless $args->[$x] =~ /$expect->[$x]/i;
+      }
+      unexpect($kernel, $session, $client) unless $mismatch;
+    }
+    return undef;
+  }
+  print "ERROR: Unexpected event $state to test driver (from ".$sender->ID.")\n";
+  return undef;
+}
+
+# client-based command issuers
+
+sub cmd_message {
+  my ($kernel, $heap, $event, $client, $args) = @_[KERNEL, HEAP, STATE, ARG0, ARG1];
+  die "Missing arguments" unless $#$args >= 1;
+  # translate each target as appropriate (e.g. *sessionname)
+  my @targets = split(/,/, $args->[0]);
+  foreach my $target (@targets) {
+    if ($target =~ /^\*(.+)$/) {
+      my $other = $heap->{clients}->{$1} or die "Unknown session name $1 (line $heap->{lineno})\n";
+      $target = $other->{nick};
+    }
+  }
+  $kernel->call($client->{irc}, substr($event, 4), \@targets, $args->[1]);
+}
+
+sub cmd_generic {
+  my ($kernel, $heap, $event, $client, $args) = @_[KERNEL, HEAP, STATE, ARG0, ARG1];
+  $event =~ s/^cmd_//;
+  $kernel->call($client->{irc}, $event, @$args);
+}
+
+sub cmd_raw {
+  my ($kernel, $heap, $client, $args) = @_[KERNEL, HEAP, ARG0, ARG1];
+  die "Missing argument" unless $#$args >= 0;
+  $kernel->call($client->{irc}, 'sl', $args->[0]);
+}
+
+sub cmd_sleep {
+  my ($kernel, $session, $heap, $client, $args) = @_[KERNEL, SESSION, HEAP, ARG0, ARG1];
+  die "Missing argument" unless $#$args >= 0;
+  $kernel->call($session, 'disable_client', $client);
+  $kernel->delay_set('enable_client', $args->[0], $client);
+}
+
+sub cmd_wait {
+  my ($kernel, $session, $heap, $client, $args) = @_[KERNEL, SESSION, HEAP, ARG0, ARG1];
+  die "Missing argument" unless $#$args >= 0;
+  # if argument was comma-delimited, split it up (space-delimited is split by generic parser)
+  $args = [split(/,/, $args->[0])] if $args->[0] =~ /,/;
+  # make sure we only wait if all the other clients are ready
+  foreach my $other (@$args) {
+    if (not $heap->{clients}->{$other}->{ready}) {
+      $heap->{redo} = 1;
+      return;
+    }
+  }
+  # disable this client, make the others send SYNC to it
+  $kernel->call($session, 'disable_client', $client);
+  $client->{sync_wait} = [map { $heap->{clients}->{$_}->{nick} } @$args];
+  foreach my $other (@$args) {
+    die "Cannot wait on self" if $other eq $client->{name};
+    $kernel->call($heap->{clients}->{$other}->{irc}, 'notice', $client->{nick}, 'SYNC');
+  }
+}
+
+sub cmd_expect {
+  my ($kernel, $session, $heap, $client, $args) = @_[KERNEL, SESSION, HEAP, ARG0, ARG1];
+  die "Missing argument" unless $#$args >= 0;
+  push @{$client->{expect}}, $args;
+  push @{$client->{expect_alarms}}, $kernel->delay_set('timeout_expect', EXPECT_TIMEOUT, $client);
+  $kernel->call($session, 'disable_client', $client);
+}
+
+# handlers for messages from IRC
+
+sub unexpect {
+  my ($kernel, $session, $client) = @_;
+  shift @{$client->{expect}};
+  my $alarm_id = shift @{$client->{expect_alarms}};
+  $kernel->alarm_remove($alarm_id) if $alarm_id;
+  $kernel->call($session, 'enable_client', $client) unless @{$client->{expect}};
+}
+
+sub check_expect {
+  my ($kernel, $session, $heap, $poe_sender, $sender, $text) = @_[KERNEL, SESSION, HEAP, SENDER, ARG0, ARG1];
+  my $client = $heap->{sessions}->{$poe_sender};
+  my $expected = $client->{expect}->[0];
+
+  # check sender
+  if ($expected->[0] =~ /\*(.+)/) {
+    # we expect *sessionname, so look up session's current nick
+    my $exp = $1;
+    $sender =~ /^(.+)!/;
+    return 0 if lc($heap->{clients}->{$exp}->{nick}) ne lc($1);
+  } elsif ($expected->[0] =~ /^:?(.+!.+)/) {
+    # expect :nick!user@host, so compare whole thing
+    return 0 if lc($1) ne lc($sender);
+  } else {
+    # we only expect :nick, so compare that part
+    $sender =~ /^:?(.+)!/;
+    return 0 if lc($expected->[0]) ne lc($1);
+  }
+
+  # compare text
+  return 0 if lc($text) !~ /$expected->[2]/i;
+
+  # drop expectation of event
+  unexpect($kernel, $session, $client);
+}
+
+sub irc_connected {
+  my ($kernel, $session, $heap, $sender) = @_[KERNEL, SESSION, HEAP, SENDER];
+  my $client = $heap->{sessions}->{$sender};
+  print "Client $client->{name} connected to server $_[ARG0]\n" if $heap->{verbose};
+  $kernel->call($session, 'enable_client', $client);
+}
+
+sub irc_disconnected {
+  my ($kernel, $session, $heap, $sender, $server) = @_[KERNEL, SESSION, HEAP, SENDER, ARG0];
+  my $client = $heap->{sessions}->{$sender};
+  print "Client $client->{name} disconnected from server $_[ARG0]\n" if $heap->{verbose};
+  if ($client->{quitting}) {
+    $kernel->call($sender, 'unregister', 'all');
+    delete $heap->{sessions}->{$sender};
+    delete $heap->{clients}->{$client->{name}};
+  } else {
+    if ($client->{disconnect_expected}) {
+      delete $client->{disconnect_expected};
+    } else {
+      print "Got unexpected disconnect for $client->{name} (nick $client->{nick})\n";
+    }
+    $kernel->call($session, 'disable_client', $client);
+    $kernel->delay_set('reconnect', $client->{throttled} ? THROTTLED_TIMEOUT : RECONNECT_TIMEOUT, $client);
+    delete $client->{throttled};
+  }
+}
+
+sub irc_notice {
+  my ($kernel, $session, $heap, $sender, $from, $to, $text) = @_[KERNEL, SESSION, HEAP, SENDER, ARG0, ARG1, ARG2];
+  my $client = $heap->{sessions}->{$sender};
+  if ($client->{sync_wait} and $text eq 'SYNC') {
+    $from =~ s/!.+$//;
+    my $x;
+    # find who sent it..
+    for ($x=0; $x<=$#{$client->{sync_wait}}; $x++) {
+      last if $from eq $client->{sync_wait}->[$x];
+    }
+    # exit if we don't expect them
+    if ($x>$#{$client->{sync_wait}}) {
+      print "Got unexpected SYNC from $from to $client->{name} ($client->{nick})\n";
+      return;
+    }
+    # remove from the list of people we're waiting for
+    splice @{$client->{sync_wait}}, $x, 1;
+    # re-enable client if we're done waiting
+    if ($#{$client->{sync_wait}} == -1) {
+      delete $client->{sync_wait};
+      $kernel->call($session, 'enable_client', $client);
+    }
+  } elsif (@{$client->{expect}}
+           and $client->{expect}->[0]->[1] =~ /notice/i) {
+    check_expect(@_[0..ARG0], $text);
+  }
+}
+
+sub irc_msg {
+  my ($kernel, $session, $heap, $sender, $from, $to, $text) = @_[KERNEL, SESSION, HEAP, SENDER, ARG0, ARG1, ARG2];
+  my $client = $heap->{sessions}->{$sender};
+  if (@{$client->{expect}}
+      and $client->{expect}->[0]->[1] =~ /msg/i) {
+    check_expect(@_[0..ARG0], $text);
+  }
+}
+
+sub irc_public {
+  my ($kernel, $session, $heap, $sender, $from, $to, $text) = @_[KERNEL, SESSION, HEAP, SENDER, ARG0, ARG1, ARG2];
+  my $client = $heap->{sessions}->{$sender};
+  if (@{$client->{expect}}
+      and $client->{expect}->[0]->[1] =~ /public/i
+      and grep($client->{expect}->[0]->[2], @$to)) {
+    splice @{$client->{expect}->[0]}, 2, 1;
+    check_expect(@_[0..ARG0], $text);
+  }
+}
+
+sub irc_error {
+  my ($kernel, $session, $heap, $sender, $what) = @_[KERNEL, SESSION, HEAP, SENDER, ARG0];
+  my $client = $heap->{sessions}->{$sender};
+  if (@{$client->{expect}}
+      and $client->{expect}->[0]->[1] =~ /error/i) {
+    splice @{$client->{expect}->[0]}, 2, 1;
+    unexpect($kernel, $session, $client);
+    $client->{disconnect_expected} = 1;
+  } else {
+    print "ERROR: From server to $client->{name}: $what\n";
+  }
+  $client->{throttled} = 1 if $what =~ /throttled/i;
+}
diff --git a/tests/test.cmd b/tests/test.cmd
new file mode 100644 (file)
index 0000000..09565ec
--- /dev/null
@@ -0,0 +1,15 @@
+define srv1 irc.clan-dk.org:7731
+define srv2 irc.clan-dk.org:7731
+define nickserv NickServ-Ent@srvx.clan-dk.org
+connect cl1 D00dm4n d00dm4n %srv1% :Some Dude Man
+connect cl2 D00dl4dy d00dl4dy %srv2% :Some Dude Lady
+:cl1,cl2 join #test
+sync cl1,cl2
+:cl1 privmsg #test :We're here and we're bots!
+:cl2 expect *cl1 public #test :We're here and we're bots!
+:cl2 privmsg #test :And we'll leave soon!
+:cl1 privmsg #test :Right, Annette!
+:cl1 sleep 10
+:cl2 wait cl1
+:cl1,cl2 quit