From 310e91e26bebf168b6081e91da123b4a8cfb9b67 Mon Sep 17 00:00:00 2001
From: Tilman Sauerbeck <tilman@code-monkey.de>
Date: Thu, 10 Aug 2006 18:05:32 +0200
Subject: [PATCH 1/1] Initial commit.

---
 AUTHORS                  |   1 +
 COPYING                  | 482 ++++++++++++++++++++++++++++++++++
 README                   |  21 ++
 Rakefile                 |  99 +++++++
 ext/comments.c           | 554 +++++++++++++++++++++++++++++++++++++++
 ext/comments.h           |  32 +++
 ext/ext.c                | 272 +++++++++++++++++++
 ext/vcedit.c             | 546 ++++++++++++++++++++++++++++++++++++++
 ext/vcedit.h             |  50 ++++
 lib/ogg/vorbis/tagger.rb |  22 ++
 rake/configuretask.rb    | 289 ++++++++++++++++++++
 rake/extensiontask.rb    | 189 +++++++++++++
 rake/gcc4test.rb         |  47 ++++
 test/test_main.rb        | 209 +++++++++++++++
 14 files changed, 2813 insertions(+)
 create mode 100644 AUTHORS
 create mode 100644 COPYING
 create mode 100644 README
 create mode 100644 Rakefile
 create mode 100644 ext/comments.c
 create mode 100644 ext/comments.h
 create mode 100644 ext/ext.c
 create mode 100644 ext/vcedit.c
 create mode 100644 ext/vcedit.h
 create mode 100644 lib/ogg/vorbis/tagger.rb
 create mode 100644 rake/configuretask.rb
 create mode 100644 rake/extensiontask.rb
 create mode 100644 rake/gcc4test.rb
 create mode 100644 test/test_main.rb

diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..5864b3d
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Tilman Sauerbeck (tilman at code-monkey de)
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..9c0633c
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,482 @@
+		  GNU LIBRARY GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 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.
+
+  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.
+
+		  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.
+
+  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.
+
+  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.
+
+  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.
+
+  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.
+
+  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.
+
+  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
+
+     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., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, 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/README b/README
new file mode 100644
index 0000000..f5a2a8c
--- /dev/null
+++ b/README
@@ -0,0 +1,21 @@
+= ruby-vorbistagger
+
+ruby-vorbistagger provides a read-write interface to tags used in
+Ogg Vorbis files (known as vorbiscomments).
+
+ruby-vorbistagger is maintained by:
+
+:include: AUTHORS
+
+== License
+
+ruby-vorbistagger is available under the GNU LGPL.
+
+== Dependencies
+
+ruby-vorbistagger depends on Rake[http://rake.rubyforge.org] 0.5.0
+or greater and libvorbis[http://vorbis.com].
+
+== Installation
+
+Run "rake install" to install ruby-vorbistagger.
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..4cb775c
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,99 @@
+require "rake/clean"
+require "rake/testtask"
+require "rake/rdoctask"
+require "rake/packagetask"
+require "rake/contrib/compositepublisher"
+require "rake/contrib/sshpublisher"
+
+require "rake/configuretask"
+require "rake/gcc4test"
+require "rake/extensiontask"
+
+PKG_NAME = "ruby-vorbistagger"
+PKG_VERSION = File.read("lib/ogg/vorbis/tagger.rb").
+              match(/^\s*VERSION = \"(.*)\"$/).captures.first
+
+ext_objs = [:ext, :comments, :vcedit]
+
+task :default => [:ext]
+
+config = Rake::ConfigureTask.new do |t|
+	t.tests << Rake::ConfigureTask::
+	           PkgConfigTest.new("vorbis", :is_critical => true)
+	t.tests << Rake::ConfigureTask::Gcc4Test.new("gcc4")
+end
+
+task :ext => [:pre_ext]
+
+ext = Rake::ExtensionTask.new :ext => ext_objs do |t|
+	t.dir = "ext"
+	t.lib_name = "#{t.dir}/vorbistagger_ext.so"
+end
+
+task :pre_ext => [:configure] do
+	ext.link_libs << config.vorbis.libs
+
+	cflags = [
+		ext.env[:cflags],
+		config.vorbis.cflags
+	]
+
+	unless config.gcc4.result
+		defines = "EXT_API=\"\""
+	else
+		cflags << " -fvisibility=hidden"
+		defines = "EXT_API=\"__attribute__ " +
+		          "((visibility(\\\"default\\\")))\""
+	end
+
+	ext.env.update(
+		:cflags => cflags,
+		:defines => defines
+	)
+end
+
+task :install => [:ext] do |t|
+	destdir = ENV["DESTDIR"] || ""
+
+	ddir = destdir + Config::CONFIG["sitearchdir"]
+	FileUtils::Verbose.mkdir_p(ddir) unless File.directory?(ddir)
+	FileUtils::Verbose.install(ext.lib_name, ddir, :mode => 0755)
+
+	ddir = destdir + Config::CONFIG["sitelibdir"] + "ogg/vorbis"
+	FileUtils::Verbose.mkdir_p(ddir) unless File.directory?(ddir)
+	FileUtils::Verbose.install("lib/**/*.rb", ddir, :mode => 0644)
+end
+
+task :test => [:ext]
+
+Rake::TestTask.new do |t|
+	t.libs = ["lib", "ext", "test"]
+	t.test_files = FileList["test/test_*.rb"]
+	t.warning = true
+end
+
+Rake::RDocTask.new do |t|
+	t.rdoc_dir = "doc"
+	t.title = PKG_NAME
+	t.options = ["--line-numbers", "--inline-source", "--main", "README"]
+	t.rdoc_files.include("README", "COPYING", "AUTHORS",
+	                     "ext/ext.c", "ext/comments.c", "lib/**/*.rb")
+end
+
+Rake::PackageTask.new(PKG_NAME, PKG_VERSION) do |t|
+	t.need_tar_gz = true
+	t.package_files.include("[A-Z]*", "rake/*rb", "ext/*.[ch]",
+	                        "lib/**/*.rb", "test/*.rb")
+end
+
+task :publish => [:rdoc, :package] do
+	p = Rake::CompositePublisher.new
+	p.add(Rake::SshFreshDirPublisher.new("code-monkey.de",
+	                                     "public_docs/" +
+	                                     PKG_NAME, "doc"))
+	p.add(Rake::SshFilePublisher.new("code-monkey.de",
+	                                 ".", "pkg",
+	                                 "#{PKG_NAME}-#{PKG_VERSION}.tar.gz"))
+	p.upload
+end
+
diff --git a/ext/comments.c b/ext/comments.c
new file mode 100644
index 0000000..98dd73c
--- /dev/null
+++ b/ext/comments.c
@@ -0,0 +1,554 @@
+/*
+ * Copyright (C) 2006 Tilman Sauerbeck (tilman at code-monkey de)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, version 2.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <ruby.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <assert.h>
+
+#include "vcedit.h"
+
+typedef struct {
+	vcedit_state *state;
+
+	VALUE items;
+} RbVorbisComments;
+
+static ID id_casecmp, id_replace, id_compare;
+
+void
+comments_init (VALUE self, vcedit_state *state)
+{
+	RbVorbisComments *o;
+	vorbis_comment *vc;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	o->state = state;
+	vcedit_state_ref (state);
+
+	vc = vcedit_comments (o->state);
+
+	o->items = rb_ary_new2 (vc->comments);
+
+	for (i = 0; i < vc->comments; i++) {
+		VALUE k, v;
+		char *ptr, *content = vc->user_comments[i];
+
+		ptr = strchr (content, '=');
+		assert (ptr);
+
+		k = rb_str_new (content, ptr - content);
+		OBJ_FREEZE (k);
+
+		v = rb_str_new2 (ptr + 1);
+
+		rb_ary_store (o->items, i,
+		              rb_ary_new3 (2, k, v));
+	}
+
+	rb_iv_set (self, "@items", o->items);
+}
+
+void
+comments_sync (VALUE self)
+{
+	RbVorbisComments *o;
+	vorbis_comment *vc;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	vc = vcedit_comments (o->state);
+
+	vorbis_comment_clear (vc);
+	vorbis_comment_init (vc);
+
+	items = RARRAY (o->items);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+
+		vorbis_comment_add_tag (vc,
+		                        StringValuePtr (pair->ptr[0]),
+		                        StringValuePtr (pair->ptr[1]));
+	}
+}
+
+static void
+c_mark (RbVorbisComments *o)
+{
+	rb_gc_mark (o->items);
+}
+
+static void
+c_free (RbVorbisComments *o)
+{
+	vcedit_state_unref (o->state);
+
+	ruby_xfree (o);
+}
+
+static VALUE
+c_alloc (VALUE klass)
+{
+	RbVorbisComments *o;
+
+	return Data_Make_Struct (klass, RbVorbisComments, c_mark, c_free, o);
+}
+
+/*
+ * call-seq:
+ *  object.inspect -> string
+ *
+ * Returns the contents of *object* as a string.
+ */
+static VALUE
+c_inspect (VALUE self)
+{
+	VALUE ret;
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+
+	ret = rb_str_buf_new (128);
+	rb_str_buf_cat (ret, "{", 1);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+
+		rb_str_buf_append (ret, rb_inspect (pair->ptr[0]));
+		rb_str_buf_cat (ret, "=>", 2);
+		rb_str_buf_append (ret, rb_inspect (pair->ptr[1]));
+
+		if (i < items->len - 1)
+			rb_str_buf_cat (ret, ", ", 2);
+	}
+
+	rb_str_buf_cat (ret, "}", 1);
+
+	return ret;
+}
+
+/*
+ * call-seq:
+ *  object.clear -> object
+ *
+ * Removes all elements from *object* and returns it.
+ */
+static VALUE
+c_clear (VALUE self)
+{
+	RbVorbisComments *o;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	rb_ary_clear (o->items);
+
+	return self;
+}
+
+/*
+ * call-seq:
+ *  object.delete(key) -> string or nil
+ *
+ * If a tag with the specified key exists, that tag is deleted and
+ * the tag's value is returned. Otherwise, +nil+ is returned.
+ */
+static VALUE
+c_delete (VALUE self, VALUE key)
+{
+	VALUE ret = Qnil;
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i, pos = -1;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+		VALUE tmp;
+
+		tmp = rb_funcall (pair->ptr[0], id_casecmp, 1, key);
+		if (tmp == INT2FIX (0)) {
+			ret = pair->ptr[1];
+			pos = i;
+			break;
+		}
+	}
+
+	if (pos != -1)
+		rb_ary_delete_at (o->items, pos);
+
+	return ret;
+}
+
+/*
+ * call-seq:
+ *  object.keys -> array
+ *
+ * Returns an array that contains the keys of the tags in *object*.
+ */
+static VALUE
+c_keys (VALUE self)
+{
+	VALUE ret;
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+	ret = rb_ary_new2 (items->len);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+
+		rb_ary_store (ret, i, pair->ptr[0]);
+	}
+
+	return ret;
+}
+
+/*
+ * call-seq:
+ *  object.values -> array
+ *
+ * Returns an array that contains the values of the tags in *object*.
+ */
+static VALUE
+c_values (VALUE self)
+{
+	VALUE ret;
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+	ret = rb_ary_new2 (items->len);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+
+		rb_ary_store (ret, i, pair->ptr[1]);
+	}
+
+	return ret;
+}
+
+/*
+ * call-seq:
+ *  object.length -> integer
+ *
+ * Returns the number of tags in *object*.
+ */
+static VALUE
+c_length (VALUE self)
+{
+	RbVorbisComments *o;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	return LONG2NUM (RARRAY (o->items)->len);
+}
+
+/*
+ * call-seq:
+ *  object.empty? -> true or false
+ *
+ * Returns true if *object* is empty or false otherwise.
+ */
+static VALUE
+c_get_empty (VALUE self)
+{
+	RbVorbisComments *o;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	return !RARRAY(o->items)->len;
+}
+
+/*
+ * call-seq:
+ *  object[key] -> string
+ *
+ * Returns the value of the tag with the key *key* or +nil+ if the
+ * tag cannot be found.
+ */
+static VALUE
+c_aref (VALUE self, VALUE key)
+{
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+		VALUE tmp;
+
+		tmp = rb_funcall (pair->ptr[0], id_casecmp, 1, key);
+		if (tmp == INT2FIX (0))
+			return pair->ptr[1];
+	}
+
+	return Qnil;
+}
+
+/*
+ * call-seq:
+ *  object[key] = string
+ *
+ * Sets the value of the tag with the key *key* to *string*.
+ */
+static VALUE
+c_aset (VALUE self, VALUE key, VALUE value)
+{
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+		VALUE tmp;
+
+		tmp = rb_funcall (pair->ptr[0], id_casecmp, 1, key);
+		if (tmp == INT2FIX (0)) {
+			rb_funcall (pair->ptr[1], id_replace, 1, value);
+			return pair->ptr[1];
+		}
+	}
+
+	rb_ary_push (o->items, rb_ary_new3 (2, key, value));
+
+	return value;
+}
+
+/*
+ * call-seq:
+ *  object.has_key?(key) -> true or false
+ *
+ * Returns true if a tag exists with the specified key or false
+ * otherwise.
+ */
+static VALUE
+c_has_key (VALUE self, VALUE key)
+{
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+		VALUE tmp;
+
+		tmp = rb_funcall (pair->ptr[0], id_casecmp, 1, key);
+		if (tmp == INT2FIX (0))
+			return Qtrue;
+	}
+
+	return Qfalse;
+}
+
+/*
+ * call-seq:
+ *  object <=> other -> -1, 0 or 1
+ *
+ * Compares *object* to *other* and returns -1, 0 or 1 if
+ * *object* is less than, equal or greater than *other*.
+ */
+static VALUE
+c_compare (VALUE self, VALUE other)
+{
+	RbVorbisComments *o, *o2;
+	struct RArray *a, *b;
+	int i, j;
+
+	if (rb_obj_is_kind_of (other, CLASS_OF (self)) != Qtrue)
+		rb_raise (rb_eArgError, "invalid argument");
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+	Data_Get_Struct (other, RbVorbisComments, o2);
+
+	a = RARRAY (o->items);
+	b = RARRAY (o2->items);
+
+	if (a->len < b->len)
+		return -1;
+
+	if (b->len < a->len)
+		return 1;
+
+	for (i = 0; i < a->len; i++) {
+		struct RArray *aa = RARRAY (a->ptr[i]);
+		struct RArray *bb = RARRAY (b->ptr[i]);
+
+		for (j = 0; j < 2; j++) {
+			VALUE tmp;
+
+			tmp = rb_funcall (aa->ptr[j], id_compare, 1, bb->ptr[j]);
+			if (FIX2INT (tmp) != 0)
+				return tmp;
+		}
+	}
+
+	return INT2FIX (0);
+}
+
+/*
+ * call-seq:
+ *  object.each { |key, value| block } -> object
+ *
+ * Calls _block_ once for each tag in *object*, passing the key and
+ * value of the tag.
+ * Returns *object*.
+ */
+static VALUE
+c_each (VALUE self)
+{
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+
+		rb_yield_values (2, pair->ptr[0], pair->ptr[1]);
+	}
+
+	return self;
+}
+
+/*
+ * call-seq:
+ *  object.each_key { |key| block } -> object
+ *
+ * Calls _block_ once for each tag in *object*, passing the key
+ * of the tag.
+ * Returns *object*.
+ */
+static VALUE
+c_each_key (VALUE self)
+{
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+
+		rb_yield (pair->ptr[0]);
+	}
+
+	return self;
+}
+
+/*
+ * call-seq:
+ *  object.each_value { |value| block } -> object
+ *
+ * Calls _block_ once for each tag in *object*, passing the value
+ * of the tag. Returns *object*.
+ */
+static VALUE
+c_each_value (VALUE self)
+{
+	RbVorbisComments *o;
+	struct RArray *items;
+	int i;
+
+	Data_Get_Struct (self, RbVorbisComments, o);
+
+	items = RARRAY (o->items);
+
+	for (i = 0; i < items->len; i++) {
+		struct RArray *pair = RARRAY (items->ptr[i]);
+
+		rb_yield (pair->ptr[1]);
+	}
+
+	return self;
+}
+
+VALUE
+Init_Comments (VALUE mVorbis)
+{
+	VALUE c;
+
+	c = rb_define_class_under (mVorbis, "Comments", rb_cObject);
+
+	rb_define_alloc_func (c, c_alloc);
+
+	rb_define_method (c, "inspect", c_inspect, 0);
+	rb_define_method (c, "clear", c_clear, 0);
+	rb_define_method (c, "delete", c_delete, 1);
+	rb_define_method (c, "length", c_length, 0);
+	rb_define_method (c, "has_key?", c_has_key, 1);
+	rb_define_method (c, "[]", c_aref, 1);
+	rb_define_method (c, "[]=", c_aset, 2);
+	rb_define_method (c, "empty?", c_get_empty, 0);
+	rb_define_method (c, "keys", c_keys, 0);
+	rb_define_method (c, "values", c_values, 0);
+
+	rb_include_module (c, rb_mComparable);
+	rb_define_method (c, "<=>", c_compare, 1);
+
+	rb_include_module (c, rb_mEnumerable);
+	rb_define_method (c, "each", c_each, 0);
+	rb_define_method (c, "each_key", c_each_key, 0);
+	rb_define_method (c, "each_value", c_each_value, 0);
+
+	rb_define_alias (c, "size", "length");
+	rb_define_alias (c, "each_pair", "each");
+
+	id_casecmp = rb_intern ("casecmp");
+	id_replace = rb_intern ("replace");
+	id_compare = rb_intern ("<=>");
+
+	return c;
+}
diff --git a/ext/comments.h b/ext/comments.h
new file mode 100644
index 0000000..89838f8
--- /dev/null
+++ b/ext/comments.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2006 Tilman Sauerbeck (tilman at code-monkey de)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, version 2.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#ifndef __COMMENTS_H
+#define __COMMENTS_H
+
+#include <ruby.h>
+
+#include "vcedit.h"
+
+VALUE Init_Comments (VALUE mVorbis);
+
+void comments_init (VALUE self, vcedit_state *state);
+void comments_sync (VALUE self);
+
+#endif /* __COMMENTS_H */
+
diff --git a/ext/ext.c b/ext/ext.c
new file mode 100644
index 0000000..d8bcc12
--- /dev/null
+++ b/ext/ext.c
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2006 Tilman Sauerbeck (tilman at code-monkey de)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, version 2.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <ruby.h>
+#include <stdbool.h>
+
+#include "vcedit.h"
+#include "comments.h"
+
+typedef struct {
+	VALUE io;
+	bool need_close;
+
+	vcedit_state *state;
+
+	VALUE comments;
+	VALUE io_buf;
+} RbVorbisTagger;
+
+static VALUE c_close (VALUE self);
+
+static VALUE cComments, eVTError;
+static ID id_read, id_write, id_seek, id_length;
+
+static size_t
+on_read (void *ptr, size_t size, size_t nmemb, RbVorbisTagger *o)
+{
+	struct RString *buf;
+	size_t total = size * nmemb;
+	VALUE tmp;
+
+	rb_str_resize (o->io_buf, size * nmemb);
+
+	tmp = rb_funcall (o->io, id_read, 2, LONG2NUM (total), o->io_buf);
+	if (NIL_P (tmp))
+		return 0;
+
+	buf = RSTRING (tmp);
+	memcpy (ptr, buf->ptr, buf->len);
+
+	return buf->len;
+}
+
+static size_t
+on_write (const void *ptr, size_t size, size_t nmemb, RbVorbisTagger *o)
+{
+	size_t total = size * nmemb;
+
+	rb_str_resize (o->io_buf, total);
+	memcpy (RSTRING (o->io_buf)->ptr, ptr, total);
+
+	return NUM2LONG (rb_io_write (o->io, o->io_buf));
+}
+
+static void
+c_mark (RbVorbisTagger *o)
+{
+	rb_gc_mark (o->io);
+	rb_gc_mark (o->comments);
+	rb_gc_mark (o->io_buf);
+}
+
+static void
+c_free (RbVorbisTagger *o)
+{
+	/* just in case the user forgot to call #close himself */
+	if (o->state)
+		vcedit_state_unref (o->state);
+
+	ruby_xfree (o);
+}
+
+static VALUE
+c_alloc (VALUE klass)
+{
+	RbVorbisTagger *o;
+
+	return Data_Make_Struct (klass, RbVorbisTagger, c_mark, c_free, o);
+}
+
+/*
+ * call-seq:
+ *  Ogg::Vorbis::Tagger.open(arg)                    -> object
+ *  Ogg::Vorbis::Tagger.open(arg) { |object| block } -> nil
+ *
+ * If a block isn't specified, Ogg::Vorbis::Tagger.open is a synonym
+ * for Ogg::Vorbis::Tagger.new.
+ * If a block is given, it will be invoked with the
+ * Ogg::Vorbis::Tagger object as a parameter, and the file or IO object
+ * will be automatically closed when the block terminates.
+ * The method always returns +nil+ in this case.
+ */
+static VALUE
+c_open (VALUE klass, VALUE arg)
+{
+	VALUE obj = rb_class_new_instance (1, &arg, klass);
+
+	if (rb_block_given_p ())
+		return rb_ensure (rb_yield, obj, c_close, obj);
+	else
+		return obj;
+}
+
+/*
+ * call-seq:
+ *  Ogg::Vorbis::Tagger.new(arg) -> object
+ *
+ * Returns a new Ogg::Vorbis::Tagger object for the specified argument.
+ * *arg* can either be an IO object or a filename.
+ *
+ * FIXME: add optional mode argument (read-only or read-write)
+ */
+static VALUE
+c_init (VALUE self, VALUE io)
+{
+	RbVorbisTagger *o;
+	vorbis_comment *vc;
+	int s;
+
+	Data_Get_Struct (self, RbVorbisTagger, o);
+
+	/* is this actually an IO object or a filename? */
+	if (rb_respond_to (io, id_read) &&
+	    rb_respond_to (io, id_write) &&
+	    rb_respond_to (io, id_seek))
+		o->need_close = false;
+	else if (!NIL_P (rb_check_string_type (io))) {
+		io = rb_file_open (StringValuePtr (io), "rb+");
+		o->need_close = true;
+	} else
+		rb_raise (rb_eArgError, "invalid argument");
+
+	o->io = io;
+	o->io_buf = rb_str_buf_new (BUFSIZ);
+
+	o->state = vcedit_state_new ();
+	if (!o->state)
+		rb_raise (eVTError, "vcedit_new_state() failed - %s",
+		          vcedit_error (o->state));
+
+	s = vcedit_open_callbacks (o->state, o,
+	                           (vcedit_read_func) on_read,
+	                           (vcedit_write_func) on_write);
+	if (s < 0)
+		rb_raise (eVTError, "vcedit_open_callbacks() failed - %s",
+		          vcedit_error (o->state));
+
+	vc = vcedit_comments (o->state);
+	if (!vc)
+		rb_raise (eVTError, "vcedit_comments() failed - %s",
+		          vcedit_error (o->state));
+
+	o->comments = rb_class_new_instance (0, NULL, cComments);
+
+	comments_init (o->comments, o->state);
+
+	return self;
+}
+
+/*
+ * call-seq:
+ *  object.close -> object
+ *
+ * Closes *object* and returns it.
+ */
+static VALUE
+c_close (VALUE self)
+{
+	RbVorbisTagger *o;
+
+	Data_Get_Struct (self, RbVorbisTagger, o);
+
+	vcedit_state_unref (o->state);
+	o->state = NULL;
+
+	if (o->need_close)
+		rb_io_close (o->io);
+
+	return self;
+}
+
+/*
+ * call-seq:
+ *  object.write -> integer
+ *
+ * Writes the comments from *object* back to the IO object and
+ * returns the numbers of comments written.
+ */
+static VALUE
+c_write (VALUE self)
+{
+	RbVorbisTagger *o;
+	int s;
+
+	Data_Get_Struct (self, RbVorbisTagger, o);
+
+	comments_sync (o->comments);
+
+	/* seek back to BOF */
+	rb_funcall (o->io, id_seek, 1, INT2FIX (0));
+
+	s = vcedit_write (o->state, o);
+	if (s < 0)
+		rb_raise (rb_eIOError, "write failed - %s",
+		          vcedit_error (o->state));
+
+	return rb_funcall (o->comments, id_length, 0);
+}
+
+/*
+ * call-seq:
+ *  object.comments -> comments
+ *
+ * Returns the comments collection of *object*, which is an instance of
+ * Ogg::Vorbis::Comments.
+ */
+static VALUE
+c_comments (VALUE self)
+{
+	RbVorbisTagger *o;
+
+	Data_Get_Struct (self, RbVorbisTagger, o);
+
+	return o->comments;
+}
+
+EXT_API
+void
+Init_vorbistagger_ext (void)
+{
+	VALUE mOgg, mVorbis, eOgg, eVorbis, cVT;
+
+	mOgg = rb_define_module ("Ogg");
+	mVorbis = rb_define_module_under (mOgg, "Vorbis");
+
+	eOgg = rb_define_class_under (mOgg, "OggError", rb_eStandardError);
+	eVorbis = rb_define_class_under (mVorbis, "VorbisError", eOgg);
+
+	cVT = rb_define_class_under (mVorbis, "Tagger", rb_cObject);
+
+	rb_define_alloc_func (cVT, c_alloc);
+
+	rb_define_singleton_method (cVT, "open", c_open, 1);
+	rb_define_method (cVT, "initialize", c_init, 1);
+	rb_define_method (cVT, "close", c_close, 0);
+	rb_define_method (cVT, "write", c_write, 0);
+	rb_define_method (cVT, "comments", c_comments, 0);
+
+	eVTError = rb_define_class_under (cVT, "TaggerError", eVorbis);
+
+	id_read = rb_intern ("read");
+	id_write = rb_intern ("write");
+	id_seek = rb_intern ("seek");
+	id_length = rb_intern ("length");
+
+	cComments = Init_Comments (mVorbis);
+}
diff --git a/ext/vcedit.c b/ext/vcedit.c
new file mode 100644
index 0000000..c7bd8e7
--- /dev/null
+++ b/ext/vcedit.c
@@ -0,0 +1,546 @@
+/*
+ * Copyright (C) 2000-2001 Michael Smith (msmith at xiph org)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, version 2.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <ogg/ogg.h>
+#include <vorbis/codec.h>
+
+#include "vcedit.h"
+
+#define CHUNKSIZE 4096
+
+struct vcedit_state_St {
+	int refcount;
+
+	ogg_sync_state *oy;
+	ogg_stream_state *os;
+
+	vorbis_comment *vc;
+	vorbis_info *vi;
+
+	vcedit_read_func read;
+	vcedit_write_func write;
+
+	void *in;
+	long serial;
+	unsigned char *mainbuf;
+	unsigned char *bookbuf;
+	int	mainlen;
+	int	booklen;
+	const char *lasterror;
+	char *vendor;
+	int prevW;
+	int extrapage;
+	int eosin;
+};
+
+vcedit_state *
+vcedit_state_new (void)
+{
+	vcedit_state *state;
+
+	state = malloc (sizeof (vcedit_state));
+	if (!state)
+		return NULL;
+
+	memset (state, 0, sizeof (vcedit_state));
+
+	state->refcount = 1;
+
+	return state;
+}
+
+const char *
+vcedit_error (vcedit_state *state)
+{
+	return state->lasterror;
+}
+
+vorbis_comment *
+vcedit_comments (vcedit_state *state)
+{
+	return state->vc;
+}
+
+static void
+vcedit_clear_internals (vcedit_state *state)
+{
+	const char *tmp;
+
+	if (state->vc) {
+		vorbis_comment_clear (state->vc);
+		free (state->vc);
+	}
+
+	if (state->os) {
+		ogg_stream_clear (state->os);
+		free (state->os);
+	}
+
+	if (state->oy) {
+		ogg_sync_clear (state->oy);
+		free (state->oy);
+	}
+
+	free (state->vendor);
+	free (state->mainbuf);
+	free (state->bookbuf);
+
+    if (state->vi) {
+		vorbis_info_clear (state->vi);
+        free (state->vi);
+    }
+
+    tmp = state->lasterror;
+    memset (state, 0, sizeof (vcedit_state));
+    state->lasterror = tmp;
+}
+
+void
+vcedit_state_ref (vcedit_state *state)
+{
+	state->refcount++;
+}
+
+void
+vcedit_state_unref (vcedit_state *state)
+{
+	state->refcount--;
+
+	if (!state->refcount) {
+		vcedit_clear_internals (state);
+		free (state);
+	}
+}
+
+/* Next two functions pulled straight from libvorbis, apart from one change
+ * - we don't want to overwrite the vendor string.
+ */
+static void
+_v_writestring (oggpack_buffer *o, char *s, int len)
+{
+	while (len--) {
+		oggpack_write (o, *s++, 8);
+	}
+}
+
+static int
+_commentheader_out (vorbis_comment *vc, char *vendor, ogg_packet *op)
+{
+	oggpack_buffer opb;
+
+	oggpack_writeinit (&opb);
+
+	/* preamble */
+	oggpack_write (&opb, 0x03, 8);
+	_v_writestring (&opb, "vorbis", 6);
+
+	/* vendor */
+	oggpack_write (&opb, strlen (vendor), 32);
+	_v_writestring (&opb, vendor, strlen (vendor));
+
+	/* comments */
+	oggpack_write (&opb, vc->comments, 32);
+
+	if (vc->comments) {
+		int i;
+
+		for (i = 0; i < vc->comments; i++) {
+			if (vc->user_comments[i]) {
+				oggpack_write (&opb, vc->comment_lengths[i], 32);
+				_v_writestring (&opb, vc->user_comments[i],
+				                vc->comment_lengths[i]);
+			} else
+				oggpack_write (&opb, 0, 32);
+		}
+	}
+
+	oggpack_write (&opb, 1, 1);
+
+	op->packet = _ogg_malloc (oggpack_bytes (&opb));
+	memcpy (op->packet, opb.buffer, oggpack_bytes (&opb));
+
+	op->bytes = oggpack_bytes (&opb);
+	op->b_o_s = 0;
+	op->e_o_s = 0;
+	op->granulepos = 0;
+
+	oggpack_writeclear (&opb);
+
+	return 0;
+}
+
+static int
+_blocksize (vcedit_state *s, ogg_packet *p)
+{
+	int this = vorbis_packet_blocksize (s->vi, p);
+	int ret = (this + s->prevW) / 4;
+
+	if (!s->prevW) {
+		s->prevW = this;
+		return 0;
+	}
+
+	s->prevW = this;
+
+	return ret;
+}
+
+static int
+_fetch_next_packet (vcedit_state *s, ogg_packet *p, ogg_page *page)
+{
+	char *buffer;
+	int result, bytes;
+
+	result = ogg_stream_packetout (s->os, p);
+
+	if (result > 0)
+		return 1;
+
+	if (s->eosin)
+		return 0;
+
+	while (ogg_sync_pageout (s->oy, page) <= 0) {
+		buffer = ogg_sync_buffer (s->oy, CHUNKSIZE);
+		bytes = s->read (buffer, 1, CHUNKSIZE, s->in);
+		ogg_sync_wrote (s->oy, bytes);
+
+		if (!bytes)
+			return 0;
+	}
+
+	if (ogg_page_eos (page))
+		s->eosin = 1;
+	else if (ogg_page_serialno (page) != s->serial) {
+		s->eosin = 1;
+		s->extrapage = 1;
+		return 0;
+	}
+
+	ogg_stream_pagein (s->os, page);
+
+	return _fetch_next_packet (s, p, page);
+}
+
+int
+vcedit_open_callbacks (vcedit_state *state, void *in,
+                       vcedit_read_func read_func,
+                       vcedit_write_func write_func)
+{
+	char *buffer;
+	int bytes, i;
+	int chunks = 0;
+	ogg_packet *header;
+	ogg_packet header_main, header_comments, header_codebooks;
+	ogg_page og;
+
+	state->in = in;
+	state->read = read_func;
+	state->write = write_func;
+
+	state->oy = malloc (sizeof (ogg_sync_state));
+	ogg_sync_init (state->oy);
+
+	while (1) {
+		buffer = ogg_sync_buffer (state->oy, CHUNKSIZE);
+		bytes = state->read (buffer, 1, CHUNKSIZE, state->in);
+
+		ogg_sync_wrote (state->oy, bytes);
+
+		if (ogg_sync_pageout (state->oy, &og) == 1)
+			break;
+
+		/* Bail if we don't find data in the first 40 kB */
+		if (chunks++ >= 10) {
+			if (bytes < CHUNKSIZE)
+				state->lasterror = "Input truncated or empty.";
+			else
+				state->lasterror = "Input is not an Ogg bitstream.";
+
+			goto err;
+		}
+	}
+
+	state->serial = ogg_page_serialno (&og);
+
+	state->os = malloc (sizeof (ogg_stream_state));
+	ogg_stream_init (state->os, state->serial);
+
+	state->vi = malloc (sizeof (vorbis_info));
+	vorbis_info_init (state->vi);
+
+	state->vc = malloc (sizeof (vorbis_comment));
+	vorbis_comment_init (state->vc);
+
+	if (ogg_stream_pagein (state->os, &og) < 0) {
+		state->lasterror = "Error reading first page of Ogg bitstream.";
+		goto err;
+	}
+
+	if (ogg_stream_packetout (state->os, &header_main) != 1) {
+		state->lasterror = "Error reading initial header packet.";
+		goto err;
+	}
+
+	if (vorbis_synthesis_headerin (state->vi, state->vc, &header_main) < 0) {
+		state->lasterror = "Ogg bitstream does not contain vorbis data.";
+		goto err;
+	}
+
+	state->mainlen = header_main.bytes;
+	state->mainbuf = malloc (state->mainlen);
+	memcpy (state->mainbuf, header_main.packet, header_main.bytes);
+
+	i = 0;
+	header = &header_comments;
+
+	while (i < 2) {
+		while (i < 2) {
+			int result = ogg_sync_pageout (state->oy, &og);
+
+			if (!result)
+				break; /* Too little data so far */
+
+			if (result == 1) {
+				ogg_stream_pagein (state->os, &og);
+
+				while (i < 2) {
+					result = ogg_stream_packetout (state->os, header);
+
+					if (!result)
+						break;
+
+					if (result == -1) {
+						state->lasterror = "Corrupt secondary header.";
+						goto err;
+					}
+
+					vorbis_synthesis_headerin (state->vi, state->vc, header);
+
+					if (i == 1) {
+						state->booklen = header->bytes;
+						state->bookbuf = malloc (state->booklen);
+						memcpy (state->bookbuf, header->packet, header->bytes);
+					}
+
+					i++;
+					header = &header_codebooks;
+				}
+			}
+		}
+
+		buffer = ogg_sync_buffer (state->oy, CHUNKSIZE);
+		bytes = state->read (buffer, 1, CHUNKSIZE, state->in);
+
+		if (bytes == 0 && i < 2) {
+			state->lasterror = "EOF before end of vorbis headers.";
+			goto err;
+		}
+
+		ogg_sync_wrote (state->oy, bytes);
+	}
+
+	/* Copy the vendor tag */
+	state->vendor = strdup (state->vc->vendor);
+
+	/* Headers are done! */
+	return 0;
+
+err:
+	vcedit_clear_internals (state);
+
+	return -1;
+}
+
+int
+vcedit_write (vcedit_state *state, void *out)
+{
+	ogg_stream_state streamout;
+	ogg_packet header_main, header_comments, header_codebooks, op;
+	ogg_page ogout, ogin;
+	ogg_int64_t granpos = 0;
+	int result, bytes, needflush = 0, needout = 0;
+	char *buffer;
+	size_t tmp;
+
+	state->eosin = 0;
+	state->extrapage = 0;
+
+	header_main.bytes = state->mainlen;
+	header_main.packet = state->mainbuf;
+	header_main.b_o_s = 1;
+	header_main.e_o_s = 0;
+	header_main.granulepos = 0;
+
+	header_codebooks.bytes = state->booklen;
+	header_codebooks.packet = state->bookbuf;
+	header_codebooks.b_o_s = 0;
+	header_codebooks.e_o_s = 0;
+	header_codebooks.granulepos = 0;
+
+	ogg_stream_init (&streamout, state->serial);
+
+	_commentheader_out (state->vc, state->vendor, &header_comments);
+
+	ogg_stream_packetin (&streamout, &header_main);
+	ogg_stream_packetin (&streamout, &header_comments);
+	ogg_stream_packetin (&streamout, &header_codebooks);
+
+	while ((result = ogg_stream_flush (&streamout, &ogout))) {
+		tmp = state->write (ogout.header, 1, ogout.header_len, out);
+		if (tmp != (size_t) ogout.header_len)
+			goto cleanup;
+
+		tmp = state->write (ogout.body, 1, ogout.body_len, out);
+		if (tmp != (size_t) ogout.body_len)
+			goto cleanup;
+	}
+
+	while (_fetch_next_packet (state, &op, &ogin)) {
+		int size;
+
+		size = _blocksize (state, &op);
+		granpos += size;
+
+		if (needflush) {
+			if (ogg_stream_flush (&streamout, &ogout)) {
+				tmp = state->write (ogout.header, 1, ogout.header_len, out);
+				if (tmp != (size_t) ogout.header_len)
+					goto cleanup;
+
+				tmp = state->write (ogout.body, 1, ogout.body_len, out);
+				if (tmp != (size_t) ogout.body_len)
+					goto cleanup;
+			}
+		} else if (needout) {
+			if (ogg_stream_pageout (&streamout, &ogout)) {
+				tmp = state->write (ogout.header, 1, ogout.header_len, out);
+				if (tmp != (size_t) ogout.header_len)
+					goto cleanup;
+
+				tmp = state->write (ogout.body, 1, ogout.body_len, out);
+				if (tmp != (size_t) ogout.body_len)
+					goto cleanup;
+			}
+		}
+
+		needflush = needout = 0;
+
+		if (op.granulepos == -1) {
+			op.granulepos = granpos;
+			ogg_stream_packetin (&streamout, &op);
+		} else {
+			/* granulepos is set, validly. Use it, and force a flush to
+			 * account for shortened blocks (vcut) when appropriate
+			 */
+			if (granpos > op.granulepos) {
+				granpos = op.granulepos;
+				ogg_stream_packetin (&streamout, &op);
+				needflush = 1;
+			} else {
+				ogg_stream_packetin (&streamout, &op);
+				needout = 1;
+			}
+		}
+	}
+
+	streamout.e_o_s = 1;
+
+	while (ogg_stream_flush (&streamout, &ogout)) {
+		tmp = state->write (ogout.header, 1, ogout.header_len, out);
+		if (tmp != (size_t) ogout.header_len)
+			goto cleanup;
+
+		tmp = state->write (ogout.body, 1, ogout.body_len, out);
+		if (tmp != (size_t) ogout.body_len)
+			goto cleanup;
+	}
+
+	if (state->extrapage) {
+		tmp = state->write (ogin.header, 1, ogin.header_len, out);
+		if (tmp != (size_t) ogin.header_len)
+			goto cleanup;
+
+		tmp = state->write (ogin.body, 1, ogin.body_len, out);
+		if (tmp != (size_t) ogin.body_len)
+			goto cleanup;
+	}
+
+	/* clear it, because not all paths to here do */
+	state->eosin = 0;
+
+	while (!state->eosin) { /* We reached eos, not eof */
+		/* We copy the rest of the stream (other logical streams)
+		 * through, a page at a time.
+		 */
+		while (1) {
+			result = ogg_sync_pageout (state->oy, &ogout);
+
+			if (!result)
+                break;
+
+			if (result < 0)
+				state->lasterror = "Corrupt or missing data, continuing...";
+			else {
+				/* Don't bother going through the rest, we can just
+				 * write the page out now
+				 */
+				tmp = state->write (ogout.header,1,ogout.header_len, out);
+				if (tmp != (size_t) ogout.header_len)
+					goto cleanup;
+
+				tmp = state->write (ogout.body,1,ogout.body_len, out);
+				if (tmp != (size_t) ogout.body_len)
+					goto cleanup;
+			}
+		}
+
+		buffer = ogg_sync_buffer (state->oy, CHUNKSIZE);
+		bytes = state->read (buffer, 1, CHUNKSIZE, state->in);
+		ogg_sync_wrote (state->oy, bytes);
+
+		if (!bytes) {
+			state->eosin = 1;
+			break;
+		}
+	}
+
+cleanup:
+	ogg_stream_clear (&streamout);
+
+    /* We don't ogg_packet_clear() this, because the memory was
+	 * allocated in _commentheader_out(), so we mirror that here
+	 */
+    _ogg_free (header_comments.packet);
+
+	free (state->mainbuf);
+	free (state->bookbuf);
+
+    state->mainbuf = state->bookbuf = NULL;
+
+	if (!state->eosin) {
+		state->lasterror = "Error writing stream to output. "
+		                   "Output stream may be corrupted or truncated.";
+		return -1;
+	}
+
+	return 0;
+}
diff --git a/ext/vcedit.h b/ext/vcedit.h
new file mode 100644
index 0000000..a8aa33c
--- /dev/null
+++ b/ext/vcedit.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2000-2001 Michael Smith (msmith at xiph org)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, version 2.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#ifndef __VCEDIT_H
+#define __VCEDIT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+#include <ogg/ogg.h>
+#include <vorbis/codec.h>
+
+typedef size_t (*vcedit_read_func)(void *, size_t, size_t, void *);
+typedef size_t (*vcedit_write_func)(const void *, size_t, size_t, void *);
+
+typedef struct vcedit_state_St vcedit_state;
+
+vcedit_state *vcedit_state_new (void);
+void vcedit_state_ref (vcedit_state *state);
+void vcedit_state_unref (vcedit_state *state);
+vorbis_comment *vcedit_comments (vcedit_state *state);
+int	vcedit_open_callbacks (vcedit_state *state, void *in,
+                           vcedit_read_func read_func,
+                           vcedit_write_func write_func);
+int vcedit_write (vcedit_state *state, void *out);
+const char *vcedit_error (vcedit_state *state);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __VCEDIT_H */
+
diff --git a/lib/ogg/vorbis/tagger.rb b/lib/ogg/vorbis/tagger.rb
new file mode 100644
index 0000000..ebc297f
--- /dev/null
+++ b/lib/ogg/vorbis/tagger.rb
@@ -0,0 +1,22 @@
+#
+# Copyright (C) 2006 Tilman Sauerbeck (tilman at code-monkey de)
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation, version 2.
+#
+# 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301 USA
+
+require "vorbistagger_ext"
+
+class Ogg::Vorbis::Tagger
+	VERSION = "0.0.1"
+end
diff --git a/rake/configuretask.rb b/rake/configuretask.rb
new file mode 100644
index 0000000..9a20f79
--- /dev/null
+++ b/rake/configuretask.rb
@@ -0,0 +1,289 @@
+#
+# Copyright (c) 2005, 2006 Tilman Sauerbeck (tilman at code-monkey de)
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require "rake/tasklib"
+require "rake/clean"
+require "yaml"
+require "tempfile"
+require "fileutils"
+
+module Rake
+	class ConfigureTask < TaskLib
+		CACHE_FILE = ".configure_state.yaml"
+
+		attr_reader :tests
+
+		def initialize # :yield: self
+			@tests = TestList.new(load_tests || [])
+
+			yield self if block_given?
+
+			define
+		end
+
+		# returns the test with the specified name
+		def [](name)
+			@tests.find { |t| t.name == name }
+		end
+
+		def method_missing(m)
+			self[m.to_s]
+		end
+
+		private
+		def load_tests
+			r = YAML.load(File.read(CACHE_FILE)) rescue nil
+
+			r.is_a?(TestList) ? r : nil
+		end
+
+		def define
+			desc "Remove configure results"
+			task :clobber_configure do
+				FileUtils::Verbose.rm_f(CACHE_FILE)
+			end
+
+			task :clobber => :clobber_configure
+
+			desc "Configure this package"
+			task :configure => [CACHE_FILE]
+
+			file CACHE_FILE do
+				@tests.each do |t|
+					t.on_checking.reverse_each { |b| b.call }
+
+					if t.invoke
+						t.on_success.reverse_each { |b| b.call }
+					else
+						t.on_failure.reverse_each { |b| b.call }
+					end
+				end
+
+				# store the test results in CACHE_FILE
+				File.open(CACHE_FILE, "w") { |f| YAML.dump(@tests, f) }
+			end
+		end
+
+		class TestList < Array
+			def initialize(stored_tests)
+				@stored_tests = stored_tests
+			end
+
+			def <<(arg)
+				assign_result(arg)
+				super
+			end
+
+			def push(*args)
+				args.each { |a| assign_result(a) }
+				super
+			end
+
+			def unshift(arg)
+				assign_result(arg)
+				super
+			end
+
+			private
+			def assign_result(test)
+				st = @stored_tests.find { |st| st.name == test.name }
+				test.result = st.result unless st.nil?
+			end
+		end
+
+		class Test
+			attr_reader :name, :on_checking, :on_success, :on_failure
+			attr_accessor :result
+
+			def initialize(name, opts = {}) # :yield: self
+				@name = name
+				@opts = opts
+
+				@result = nil
+				@on_checking = []
+				@on_success = []
+				@on_failure = []
+
+				if opts[:is_critical]
+					@on_failure << lambda { raise }
+				end
+
+				yield self if block_given?
+			end
+
+			def to_yaml_properties
+				["@name", "@result"]
+			end
+
+			def invoke
+			end
+
+			protected
+			def can_exec_binary?(bin)
+				fork do
+					tf = Tempfile.open("configuretask")
+					STDOUT.reopen(tf)
+					STDERR.reopen(tf)
+
+					begin
+						exec(bin)
+					rescue SystemCallError
+						exit 0xb00bface
+					end
+				end
+
+				Process.wait
+
+				$?.exitstatus != 0xb00bface
+			end
+		end
+
+		class FooConfigTest < Test
+			def initialize(name, opts = {})
+				super
+
+				@result = {}
+				@command = "#{name}-config"
+
+				@on_checking << lambda do
+					print "checking for #{name}... "
+					STDOUT.flush
+				end
+
+				@on_success << lambda { puts "yes (#{version})" }
+				@on_failure << lambda { puts "no" }
+			end
+
+			def method_missing(m)
+				@result[m]
+			end
+
+			def invoke
+				return false unless can_exec_command?
+
+				[:version, :cflags, :libs].each do |f|
+					@result[f] = lookup_flags(f)
+				end
+
+				true
+			end
+
+			protected
+			def lookup_flags(f)
+				tmp = `#{@command} --#{f}`.strip
+				$?.exitstatus.zero? ? tmp : nil
+			end
+
+			private
+			def can_exec_command?
+				can_exec_binary?(@command)
+			end
+		end
+
+		class PkgConfigTest < FooConfigTest
+			def initialize(name, opts = {})
+				super
+
+				@command = "pkg-config --silence-errors"
+			end
+
+			protected
+			def lookup_flags(f)
+				f = :modversion if f == :version
+
+				tmp = `#{@command} --#{f} #{@name}`.strip.tr("\n", "/")
+				$?.exitstatus.zero? ? tmp : nil
+			end
+		end
+
+		class CompileTest < Test
+			TMP_FILE = ".compile_test"
+
+			def CompileTest.cflags
+				@@cflags
+			end
+
+			def CompileTest.cflags=(f)
+				@@cflags = f
+			end
+
+			def initialize(name, code, opts = {})
+				super(name, opts)
+
+				@code = code
+			end
+
+			def invoke
+				@result = false
+
+				cc = ENV["CC"] || "cc"
+				flags = (ENV["CFLAGS"] || "").dup
+				flags << " -I" + Config::CONFIG["archdir"]
+
+				unless @opts[:try_link]
+					flags << " -c"
+				end
+
+				File.open(TMP_FILE + ".c", "w") do |f|
+					f << @code << "\n"
+				end
+
+				`#{cc} #{flags} #{TMP_FILE}.c -o #{TMP_FILE}.o > /dev/null 2>&1`
+				@result = $?.exitstatus.zero?
+			ensure
+				FileUtils.rm_f("#{TMP_FILE}.c")
+				FileUtils.rm_f("#{TMP_FILE}.o")
+			end
+		end
+
+		class HaveFuncTest < CompileTest
+			def initialize(name, includes = [], opts = {})
+				super(name, assemble_code(name, includes), opts)
+
+				@on_checking << lambda do
+					print "checking for #{name}... "
+					STDOUT.flush
+				end
+
+				@on_success << lambda { puts "yes" }
+				@on_failure << lambda { puts "no" }
+			end
+
+			private
+			def assemble_code(func, includes)
+				header = includes.inject("") do |a, h|
+					a << "#include <#{h}>\n"
+				end
+
+				body =<<EOF
+int main () {
+	void *foo = (void *) #{func};
+	foo = (void *) 0;
+	return 0;
+}
+EOF
+
+				header + body
+			end
+		end
+	end
+end
diff --git a/rake/extensiontask.rb b/rake/extensiontask.rb
new file mode 100644
index 0000000..baff54b
--- /dev/null
+++ b/rake/extensiontask.rb
@@ -0,0 +1,189 @@
+require 'rake'
+require 'rake/clean'
+require 'rake/tasklib'
+
+module Rake
+
+  # Create a build task that will generate a Ruby extension (e.g. .so) from one or more
+  # C (.c) or C++ (.cc, .cpp, .cxx) files, and is intended as a replcaement for mkmf.
+  # It determines platform-specific settings (e.g. file extensions, compiler flags, etc.)
+  # from rbconfig (note: examples assume *nix file extensions).
+  #
+  # *Note*: Strings vs Symbols
+  # In places where filenames are expected (e.g. lib_name and objs), Strings are used
+  # as verbatim filenames, while, Symbols have the platform-dependant extension
+  # appended (e.g. '.so' for libraries and '.o' for objects).  Also, only Symbols
+  # have #dir prepended to them.
+  #
+  # Example:
+  #   desc "build sample extension"
+  #   # build sample.so (from foo.{c,cc,cxx,cpp}, through foo.o)
+  #   Rake::ExtensionTask.new :sample => :foo do |t|
+  #     # all extension files under this directory
+  #     t.dir = 'ext'
+  #     # link libraries (libbar.so)
+  #     t.link_libs << 'bar'
+  #   end
+  #
+  # Author::    Steve Sloan (mailto:steve@finagle.org)
+  # Copyright:: Copyright (c) 2006 Steve Sloan
+  # License::   GPL
+
+  class ExtensionTask < Rake::TaskLib
+    # The name of the extension
+    attr_accessor :name
+
+    # The filename of the extension library file (e.g. 'extension.so')
+    attr_accessor :lib_name
+
+    # Object files to build and link into the extension.
+    attr_accessor :objs
+
+    # The directory where the extension files (source, output, and
+    # intermediate) are stored.
+    attr_accessor :dir
+
+    # Environment configuration -- i.e. CONFIG from rbconfig, with a few other
+    # settings, and converted to lowercase-symbols.
+    attr_accessor :env
+
+    # Additional link libraries
+    attr_accessor :link_libs
+
+    # Same arguments as Rake::define_task
+    def initialize( args, &blk )
+      @env = @@DefaultEnv.dup
+      @name, @objs = resolve_args(args)
+      set_defaults
+      yield self  if block_given?
+      define_tasks
+    end
+
+    # Generate default values.  This is called from initialize _before_ the
+    # yield block.
+    #
+    # Defaults:
+    # - lib_name: name.so
+    # - objs: name.o (<- name.{c,cxx,cpp,cc})
+    # - dir: .
+    # - link_libs: <none>
+    def set_defaults
+      @lib_name ||= name.to_sym
+      @objs ||= [name.to_sym]
+      @dir ||= '.'
+      @link_libs ||= []
+    end
+
+    # Defines the library task.
+    def define_tasks
+      output_objs = @objs.collect { |obj| filepath obj, :objext }
+      output_lib = filepath lib_name, :dlext
+
+      task name => output_lib
+
+      file output_lib => output_objs do |t|
+        sh_cmd :ldshared, :dldflags, :ldflags,
+               {'-L' => :libdirs}, '-o', output_lib,
+               output_objs.join(' '),
+               link_libs.join(' '),
+               :libs, :dldlibs, :librubyarg_shared
+      end
+
+      CLEAN.include output_objs
+      CLOBBER.include output_lib
+      define_rules
+    end
+
+    # Defines C and C++ source-to-object rules, using the source extensions from env.
+    def define_rules
+      for ext in env[:c_exts]
+        Rake::Task.create_rule '.'+env[:objext] => '.'+ext do |r|
+          sh_cmd :cc, :cflags, :cppflags, {'-D' => :defines}, {'-I' => :includedirs}, {'-I' => :topdir},
+                '-c', '-o', r.name, r.sources
+        end
+      end
+
+      for ext in env[:cpp_exts]
+        Rake::Task.create_rule '.'+env[:objext] => '.'+ext do |r|
+          sh_cmd :cxx, :cxxflags, :cppflags, {'-D' => :defines}, {'-I' => :includedirs}, {'-I' => :topdir},
+                '-o', r.name, '-c', r.sources
+        end
+      end
+    end
+
+    class << self
+      # The default environment for all extensions.
+      @@DefaultEnv = {}
+      def env
+        @@DefaultEnv
+      end
+      def env=(e)
+        @@DefaultEnv = e
+      end
+
+      Config::CONFIG.merge(ENV).each { |k, v| @@DefaultEnv[k.downcase.to_sym] = v }
+      @@DefaultEnv = {
+        :cxx => 'c++',
+        :cxxflags => '',
+        :c_exts => ['c'],
+        :cpp_exts => ['cc', 'cxx', 'cpp'],
+        :includedirs => [],
+        :libdirs => [],
+      }.update(@@DefaultEnv)
+    end
+
+  protected
+
+    # Handles convenience filenames:
+    # * f (String) => f
+    # * f (Symbol) => dir/f.ext
+    def filepath( f, ext )
+      ext = env[ext]  if Symbol === ext
+      Symbol === f ? File.join( dir, "#{f}.#{ext}" ) : f
+    end
+
+    # Convenience function for cnstructing command lines for build tools.
+    def optify( *opts )
+      return optify(*opts.first)  if opts.size == 1 and opts.first.kind_of? Array
+      opts.collect do |opt|
+        case opt
+          when String then  opt
+          when Symbol then  optify env[opt]
+          when Hash
+            opt.collect do |k, v|
+              v = env[v]  if v.kind_of? Symbol
+              if v.kind_of? Array
+                optify v.collect { |w| k.to_s + w.to_s }
+              elsif v
+                k.to_s + v.to_s
+              end
+            end
+          else
+            opt.to_s
+        end
+      end.join(' ').squeeze(' ')
+    end
+
+    def sh_cmd( cmd, *opts )
+      sh optify( cmd, *opts )
+    end
+
+    # For some reason, Rake::TaskManager.resolve_args can't be found, so snarf it.
+    def resolve_args(args)
+      case args
+      when Hash
+        fail "Too Many Task Names: #{args.keys.join(' ')}" if args.size > 1
+        fail "No Task Name Given" if args.size < 1
+        task_name = args.keys[0]
+        deps = args[task_name]
+        deps = [deps] if (String===deps) || (Regexp===deps) || (Proc===deps)
+      else
+        task_name = args
+        deps = []
+      end
+      [task_name, deps]
+    end
+
+  end
+
+end
diff --git a/rake/gcc4test.rb b/rake/gcc4test.rb
new file mode 100644
index 0000000..a38c492
--- /dev/null
+++ b/rake/gcc4test.rb
@@ -0,0 +1,47 @@
+#
+# Copyright (c) 2006 Tilman Sauerbeck (tilman at code-monkey de)
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+module Rake
+	class ConfigureTask
+		class Gcc4Test < CompileTest
+			def initialize(name)
+				code =<<EOF
+void
+#ifdef __GNUC__
+# if __GNUC__ >= 4
+foo() {};
+# endif
+#endif
+EOF
+				super(name, code)
+
+				@on_checking << lambda do
+					print "checking for gcc >= 4... "
+					STDOUT.flush
+				end
+
+				@on_success << lambda { puts "yes" }
+				@on_failure << lambda { puts "no" }
+			end
+		end
+	end
+end
diff --git a/test/test_main.rb b/test/test_main.rb
new file mode 100644
index 0000000..da3621c
--- /dev/null
+++ b/test/test_main.rb
@@ -0,0 +1,209 @@
+require "test/unit"
+require "ogg/vorbis/tagger"
+require "fileutils"
+require "stringio"
+require "open3"
+
+class MainTest < Test::Unit::TestCase
+	OGG_FILE = "test/test.ogg"
+
+	def setup
+		tmp =<<EOF
+-c "artist=Bolt Thrower" -c "album=...For Victory" -c "date=1994"
+EOF
+
+		@ogg_buf = StringIO.new
+		cmd = "oggenc -q 0 #{tmp.strip} -r -"
+
+		Open3.popen3(cmd) do |sin, sout, _|
+			sin.write("\0\0\0\0")
+			sin.close
+
+			begin
+				tmp = sout.read
+				@ogg_buf << tmp
+			end while !tmp.length.zero?
+		end
+
+		@ogg_buf.seek(0)
+	end
+
+	def teardown
+		@ogg_buf.close
+	end
+
+	def test_read
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			# make sure the keys are returned in the correct order
+			assert_equal(["artist", "album", "date"], t.comments.keys)
+			assert_equal(["Bolt Thrower", "...For Victory", "1994"],
+			             t.comments.values)
+
+			assert_equal(3, t.comments.length)
+			assert_equal(3, t.comments.size)
+
+			assert_equal("Bolt Thrower", t.comments["artist"])
+			assert_equal("...For Victory", t.comments["album"])
+			assert_equal("1994", t.comments["date"])
+		end
+	end
+
+	def test_write_stable_order
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert_equal(3, t.write)
+		end
+
+		@ogg_buf.seek(0)
+
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert_equal(["artist", "album", "date"], t.comments.keys)
+		end
+	end
+
+	def test_write_stable_order_change
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			t.comments["artist"] = "Ballista"
+			assert_equal(3, t.write)
+		end
+
+		@ogg_buf.seek(0)
+
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert_equal(["artist", "album", "date"], t.comments.keys)
+		end
+	end
+
+	def test_append
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			t.comments["genre"] = "Death Metal"
+			assert_equal(4, t.write)
+		end
+
+		@ogg_buf.seek(0)
+
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert_equal("Death Metal", t.comments["genre"])
+		end
+	end
+
+	def test_delete
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert_equal("...For Victory", t.comments.delete("album"))
+			assert_nil(t.comments.delete("foo"))
+			assert_equal(2, t.write)
+		end
+
+		@ogg_buf.seek(0)
+
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert_equal(["artist", "date"], t.comments.keys)
+		end
+	end
+
+	def test_clear
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			t.comments.clear
+			assert_equal(0, t.write)
+		end
+
+		@ogg_buf.seek(0)
+
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert(t.comments.empty?)
+		end
+	end
+
+	def test_empty
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert(!t.comments.empty?)
+
+			t.comments.delete("artist")
+			t.comments.delete("album")
+			t.comments.delete("date")
+
+			assert_equal(0, t.write)
+		end
+
+		@ogg_buf.seek(0)
+
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert(t.comments.empty?)
+		end
+	end
+
+	def test_each
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			a = {
+				"artist" => "Bolt Thrower",
+				"album" => "...For Victory",
+				"date" => "1994"
+			}
+			b = {}
+
+			t.comments.each do |k, v|
+				b[k] = v
+			end
+
+			assert_equal(a, b)
+		end
+	end
+
+	def test_each_key
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			b = []
+
+			t.comments.each_key do |k|
+				b << k
+			end
+
+			assert_equal(["artist", "album", "date"], b)
+		end
+	end
+
+	def test_each_value
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			b = []
+
+			t.comments.each_value do |v|
+				b << v
+			end
+
+			assert_equal(["Bolt Thrower", "...For Victory", "1994"], b)
+		end
+	end
+
+	def test_inspect
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			tmp=<<EOF
+{"artist"=>"Bolt Thrower", "album"=>"...For Victory", "date"=>"1994"}
+EOF
+			assert_equal(tmp.strip, t.comments.inspect)
+		end
+	end
+
+	def test_has_key
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			assert(t.comments.has_key?("artist"))
+			assert(!t.comments.has_key?("foo"))
+		end
+	end
+
+	def test_compare
+		a = nil
+		b = nil
+
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			a = t.comments
+		end
+
+		@ogg_buf.seek(0)
+
+		Ogg::Vorbis::Tagger.open(@ogg_buf) do |t|
+			b = t.comments
+		end
+
+		assert_equal(0, a <=> b)
+		b["artist"] = "Foo"
+		assert_equal(-1, a <=> b)
+	end
+end
-- 
2.30.2