Initial commit.
authorTilman Sauerbeck <tilman@code-monkey.de>
Thu, 10 Aug 2006 16:05:32 +0000 (18:05 +0200)
committerTilman Sauerbeck <tilman@code-monkey.de>
Thu, 10 Aug 2006 16:05:32 +0000 (18:05 +0200)
14 files changed:
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
README [new file with mode: 0644]
Rakefile [new file with mode: 0644]
ext/comments.c [new file with mode: 0644]
ext/comments.h [new file with mode: 0644]
ext/ext.c [new file with mode: 0644]
ext/vcedit.c [new file with mode: 0644]
ext/vcedit.h [new file with mode: 0644]
lib/ogg/vorbis/tagger.rb [new file with mode: 0644]
rake/configuretask.rb [new file with mode: 0644]
rake/extensiontask.rb [new file with mode: 0644]
rake/gcc4test.rb [new file with mode: 0644]
test/test_main.rb [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
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 (file)
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.
+\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., 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 (file)
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 (file)
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 (file)
index 0000000..98dd73c
--- /dev/null
@@ -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 (file)
index 0000000..89838f8
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..c7bd8e7
--- /dev/null
@@ -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 (file)
index 0000000..a8aa33c
--- /dev/null
@@ -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 (file)
index 0000000..ebc297f
--- /dev/null
@@ -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 (file)
index 0000000..9a20f79
--- /dev/null
@@ -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 (file)
index 0000000..baff54b
--- /dev/null
@@ -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 (file)
index 0000000..a38c492
--- /dev/null
@@ -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 (file)
index 0000000..da3621c
--- /dev/null
@@ -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