From f54bec7b431be7885c49b2077e9116898298d2dd Mon Sep 17 00:00:00 2001 From: Tilman Sauerbeck Date: Sat, 26 Mar 2005 01:45:38 +0000 Subject: [PATCH] Initial commit. --- AUTHORS | 5 + COPYING | 24 +++ ChangeLog | 123 +++++++++++ README | 93 ++++++++ Rakefile | 71 ++++++ ext/ext.c | 416 ++++++++++++++++++++++++++++++++++++ lib/eet.rb | 233 ++++++++++++++++++++ test/common.rb | 32 +++ test/test_basic.rb | 68 ++++++ test/test_broken_classes.rb | 86 ++++++++ test/test_chunk.rb | 49 +++++ test/test_hash.rb | 86 ++++++++ test/test_list.rb | 82 +++++++ test/test_misc.rb | 177 +++++++++++++++ test/test_stream.rb | 42 ++++ test/test_sub.rb | 59 +++++ 16 files changed, 1646 insertions(+) create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 README create mode 100644 Rakefile create mode 100644 ext/ext.c create mode 100644 lib/eet.rb create mode 100644 test/common.rb create mode 100644 test/test_basic.rb create mode 100644 test/test_broken_classes.rb create mode 100644 test/test_chunk.rb create mode 100644 test/test_hash.rb create mode 100644 test/test_list.rb create mode 100644 test/test_misc.rb create mode 100644 test/test_stream.rb create mode 100644 test/test_sub.rb diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..4f7f7d1 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,5 @@ +-- +$Id: AUTHORS 1 2005-03-26 01:45:38Z tilman $ +++ + +Tilman Sauerbeck (tilman at code-monkey de) diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..e800483 --- /dev/null +++ b/COPYING @@ -0,0 +1,24 @@ +-- +$Id: COPYING 1 2005-03-26 01:45:38Z tilman $ +++ + +Copyright (c) 2005 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. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e251520 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,123 @@ +-- +$Id: ChangeLog 1 2005-03-26 01:45:38Z tilman $ +++ + +2005-03-26 Tilman Sauerbeck (tilman at code-monkey de) + * README, Rakefile: Require Rake 0.5.0 or greater + * Rakefile: Added a PackageTask + +2005-03-25 Tilman Sauerbeck (tilman at code-monkey de) + * test/test_broken_classes.rb: Code cleanup + +2005-03-23 Tilman Sauerbeck (tilman at code-monkey de) + * ext/ext.c, test/test_misc.rb: Eet::File methods check whether + EET entry keys contain binary zeroes + +2005-03-16 Tilman Sauerbeck (tilman at code-monkey de) + * lib/eet.rb: Fixed Eet::Stream#initialize + +2005-03-06 Tilman Sauerbeck (tilman at code-monkey de) + * Rakefile: fixed file permissions for eet.rb + +2005-03-05 Tilman Sauerbeck (tilman at code-monkey de) + * Rakefile: use FileUtils to install eet.rb and eet_ext.so + +2005-03-03 Tilman Sauerbeck (tilman at code-monkey de) + * ext/ext.c: EET_FILE_MODE_RW has been renamed to + EET_FILE_MODE_READ_WRITE + +2005-02-15 Tilman Sauerbeck (tilman at code-monkey de) + * lib/eet.rb, test/hash.rb: Removed the :hash format hack + * test/basic.rb: Renamed to test_basic.rb + * test/broken_classes.rb: Renamed to test_broken_classes.rb + * test/chunk.rb: Renamed to test_chunk.rb + * test/hash.rb: Renamed to test_hash.rb + * test/list.rb: Renamed to test_list.rb + * test/misc.rb: Renamed to test_misc.rb + * test/stream.rb: Renamed to test_stream.rb + * test/sub.rb: Renamed to test_sub.rb + * README: Removed documentation for the :hash format hack + * lib/eet.rb: Added :sub format specifier to Array#to_eet_chunks + * lib/eet.rb, README: Removed edd_name entry from property hash + +2005-02-14 Tilman Sauerbeck (tilman at code-monkey de) + * ext/ext.c: Enable highest compression for images + * lib/eet.rb: Added Eet::VERSION + +2005-02-13 Tilman Sauerbeck (tilman at code-monkey de) + * lib/eet.rb: If a hash is encoded with the :hash type + specifier, make sure that the keys/values are strings + * README: Documentation update + * Rakefile: Split out eet-config calls, to catch missing libeet + early + +2005-02-10 Tilman Sauerbeck (tilman at code-monkey de) + * lib/eet.rb, test/broken_classes.rb: + Implemented Object#to_eet_name and Object#to_eet_properties. + Adapted Object#to_eet's error checking and the broken classes + test. + * test/misc.rb: Use the new implementations of + #to_eet_name and #to_eet_properties + * test/misc.rb: Added a test for Object#to_eet_name and + Object#to_eet_properties + * Rakefile, test/hash.[rb,c], test/utils.c: + Reworked the hash test to not depend on hash.c/utils.c + anymore. + * lib/eet.rb: Added documentation for Object#to_eet_name and + Object#to_eet_properties + +2005-02-09 Tilman Sauerbeck (tilman at code-monkey de) + * test/sub.rb, test/sub.c: Reworked the sub test to not + depend on sub.c anymore + * test/list.rb, test/list.c: Reworked the list test to not + depend on list.c anymore + +2005-02-08 Tilman Sauerbeck (tilman at code-monkey de) + * lib/eet.rb: Eet::Chunk.new checks the tag for binary zeroes + again + * test/chunk.rb: Added a test for binary zeroes in tags + * lib/eet.rb: Added Eet::Chunk.deserialize + * test/chunk.rb: Added a test for Eet::Chunk.deserialize + * lib/eet.rb: Make sure Eet::Chunk.new is passed strings only + * lib/eet.rb: Added Eet::ChunkError + * test/chunk.rb: Added tests for the error handling in + Eet::Chunk.deserialize + * lib/eet.rb, test/stream.rb: Added Eet::Stream.deserialize + * test/basic.rb, test/basic.c: Rewrote basic test to not + depend on basic.c anymore + +2005-02-07 Tilman Sauerbeck (tilman at code-monkey de) + * test/list.rb, test/sub.rb: Make sure to use an array as + the hash values in Object#to_eet_properties + * lib/eet.rb: Perform more aggressive type checking in + Object#to_eet. Added Eet::EetError, Eet::PropertyError and + Eet::NameError. + * test/chunk.rb, test/stream.rb: Added tests for the Eet::Chunk + and Eet::Stream classes + * ext/ext.c, test/misc.rb: Raise IOError if Eet::File#list is + called in write-only mode + * test/broken_classes.rb: Added tests for the errors that + Object#to_eet may raise + * test/basic.[c,rb]: Added a test for 64bit integers + * lib/eet.rb: Pack 32bit integers with the "V" specifier instead + of "i", to enforce little-endian format + +2005-02-05 Tilman Sauerbeck (tilman at code-monkey de) + * ext/ext.c: Added Eet::File#read_image + +2005-02-04 Tilman Sauerbeck (tilman at code-monkey de) + * test/misc.rb: Removed nested assertions + * ext/ext.c: raise IOError on zero writes + * test/misc.rb: added test_zero_write + +2005-02-03 Tilman Sauerbeck (tilman at code-monkey de) + * test/misc.rb: Added some test for Eet::File#read and improved + other tests + * lib/eet.rb: Don't check for binary zeroes in tags + +2005-02-01 Tilman Sauerbeck (tilman at code-monkey de) + * lib/eet.rb: Reworked Stream/Chunk interfaces + * lib/eet.rb: Validate input data in Chunk.new + +2005-01-20 Tilman Sauerbeck (tilman at code-monkey de) + * Added some documentation diff --git a/README b/README new file mode 100644 index 0000000..55ce271 --- /dev/null +++ b/README @@ -0,0 +1,93 @@ +-- +$Id: README 1 2005-03-26 01:45:38Z tilman $ +++ + += ruby-eet -- Ruby bindings for EET + +ruby-eet allows you to dump Ruby objects to disk in the EET file format. +Compatibility with EET Data Descriptors, as used in the C API of libeet, +is given. + +ruby-eet is maintained by: + +:include: AUTHORS + +== License + +ruby-eet is available under an MIT-style license. + +:include: COPYING + +== Download + +The latest version of ruby-eet can be found at +http://code-monkey.de/projects/ruby-efl.html + +== Dependencies + +ruby-eet depends on Rake[http://rake.rubyforge.org] 0.5.0 or greater +and EET[http://www.enlightenment.org]. + +== Installation + +Run "rake install" to install ruby-eet. + +== Usage + +=== Basics + +Each entry in an EET file consists of an unique key and the data that's +associated with that key. + +First, you have to open an EET file by calling Eet::File.open. + +Now, you can store arbitrary data in the EET file with Eet::File#write. +To read the data from the EET file, use Eet::File#read. + +If you want to store/retrieve image data, see Eet::File#read_image and +Eet::File#write_image. + +=== Serializing objects + +ruby-eet also supports the serialization of objects which uses the same +format as the C API uses for its EET data descriptors. +At the time of this writing, deserialization, i.e. read support, isn't +implemented yet, though. + +Example: + + class Foo + def initialize + @str = "bar" + @int = 1024 + end + end + +Now, Foo.new.to_eet will serialize the object into EET format. +All of the objects instance variables will be serialized, and the class +name will be used as the tag. + +To override the tag, you just need to implement Foo#to_eet_name: + + class Foo + def to_eet_name + "Blah" + end + end + +To control what information is stored for an object, override the method +to_eet_properties. + +to_eet_properties returns a hash containing keys that are the names of +the stored properties. Each hash value is an array that contains the +value to store at least. Optionally, it can also contain a type specifier +to enforce specific encoding. + +==== Type specifiers + +For fixnums and bignums, the valid type specifiers are :char, :short, +:long_long which enforce encoding as 1 byte, 2 byte or 8 byte value +respectively. The default is to encode the value as a 4 byte value. + +For floats, the valid type specifier is :double, which enforces encoding +as an 8 byte value. The default is to encode the value as a 4 byte value. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..ab2676f --- /dev/null +++ b/Rakefile @@ -0,0 +1,71 @@ +# $Id: Rakefile 1 2005-03-26 01:45:38Z tilman $ + +require "rbconfig" +require "rake/clean" +require "rake/testtask" +require "rake/rdoctask" +require "rake/packagetask" + +# make sure we run at least Rake 0.5.0 +if (RAKEVERSION.split(".").map { |p| p.to_i } <=> [0, 5, 0]) < 0 + fail("Rake 0.5.0 or greater required") +end + +ext_obj = "ext/ext.o" +ext_lib = "ext/eet_ext.so" + +CLOBBER.include(ext_obj) +CLOBBER.include(ext_lib) +CLOBBER.include(FileList["test/*.eet"]) + +archdir = Config::CONFIG["archdir"] +sitearchdir = Config::CONFIG["sitearchdir"] +libdir = Config::CONFIG["libdir"] +sitelibdir = Config::CONFIG["sitelibdir"] +rubylib = Config::CONFIG["LIBRUBYARG_SHARED"] +destdir = "#{ENV["DESTDIR"]}" +eet_cflags = `eet-config --cflags`.strip +eet_libs = `eet-config --libs`.strip +cflags = "#{ENV["CFLAGS"]} #{eet_cflags}" +ldflags = "#{ENV["LDFLAGS"]} #{eet_libs}" + +task :default => [ext_lib] + +rule ".o" => [".c"] do |t| + sh "cc #{cflags} -fPIC -I #{archdir} #{t.source} -c -o #{t.name}" +end + +file ext_lib => ext_obj do |t| + sh "cc #{ldflags} -shared -Wl -L #{libdir} #{rubylib} " + + "#{t.prerequisites.join(" ")} -o #{t.name}" +end + +task :install => [ext_lib] do |t| + d = destdir + sitearchdir + + FileUtils::Verbose.install(ext_lib, d, :mode => 0755) + FileUtils::Verbose.install("lib/eet.rb", d, :mode => 0644) +end + +task :test => [ext_lib] + +Rake::TestTask.new do |t| + t.libs << "ext" + t.test_files = FileList["test/test_*.rb"] + t.verbose = true +end + +Rake::RDocTask.new do |t| + t.rdoc_dir = "doc" + t.title = "ruby-eet - EET bindings for Ruby" + t.options = ["--line-numbers", "--inline-source", "--main", "README"] + t.rdoc_files.include("README", "COPYING", "AUTHORS", "ChangeLog", + "ext/ext.c", "lib/eet.rb") +end + +Rake::PackageTask.new("ruby-eet") do |t| + md = `grep VERSION lib/eet.rb`.match(/VERSION = \"(.*)\"/) + t.version = md.captures.first + t.need_tar_gz = true + t.package_files.include("[A-Z]*", "ext/*.c", "lib/*.rb", "test/*.rb") +end diff --git a/ext/ext.c b/ext/ext.c new file mode 100644 index 0000000..f7860d6 --- /dev/null +++ b/ext/ext.c @@ -0,0 +1,416 @@ +/* + * $Id: ext.c 1 2005-03-26 01:45:38Z tilman $ + * + * Copyright (c) 2005 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. + */ + +#include +#include + +#define CHECK_KEY(key) \ + Check_Type ((key), T_STRING); \ + if (rb_funcall ((key), rb_intern ("include?"), \ + 1, INT2FIX (0)) == Qtrue) \ + rb_raise (rb_eArgError, "key must not contain binary zeroes"); + +static VALUE c_close (VALUE self); + +static void +c_free (Eet_File **ef) +{ + if (*ef) { + eet_close (*ef); + *ef = NULL; + + eet_shutdown (); + } + + free (ef); +} + +static VALUE +c_alloc (VALUE klass) +{ + Eet_File **ef = NULL; + + return Data_Make_Struct (klass, Eet_File *, NULL, c_free, ef); +} + +/* + * call-seq: + * Eet::File.open(file [, mode] ) -> ef or nil + * Eet::File.open(file [, mode] ) { |ef| block } -> nil + * + * If a block isn't specified, Eet::File.open is a synonym for + * Eet::File.new. + * If a block is given, it will be invoked with the + * Eet::File object as a parameter, and the file will be + * automatically closed when the block terminates. The call always + * returns +nil+ in this case. + */ +static VALUE +c_open (int argc, VALUE *argv, VALUE klass) +{ + VALUE obj = rb_class_new_instance (argc, argv, klass); + + if (rb_block_given_p ()) + return rb_ensure (rb_yield, obj, c_close, obj); + else + return obj; +} + +/* + * call-seq: + * Eet::File.new(file [, mode] ) -> ef or nil + * + * Creates an Eet::File object for _file_. + * + * _file_ is opened with the specified mode (defaulting to "r"). + * Possible values for _mode_ are "r" for read-only access, + * "w" for write-only access and "r+" for read/write access. + */ +static VALUE +c_init (int argc, VALUE *argv, VALUE self) +{ + VALUE file = Qnil, mode = Qnil; + Eet_File **ef = NULL; + Eet_File_Mode m = EET_FILE_MODE_READ; + const char *tmp; + + Data_Get_Struct (self, Eet_File *, ef); + + rb_scan_args (argc, argv, "11", &file, &mode); + + Check_Type (file, T_STRING); + + if (!NIL_P (mode)) { + Check_Type (mode, T_STRING); + + tmp = StringValuePtr (mode); + if (!strcmp (tmp, "r+")) + m = EET_FILE_MODE_READ_WRITE; + else if (!strcmp (tmp, "w")) + m = EET_FILE_MODE_WRITE; + else if (strcmp (tmp, "r")) + rb_raise (rb_eArgError, "illegal access mode %s", tmp); + } + + eet_init (); + + *ef = eet_open (StringValuePtr (file), m); + if (!*ef) { + switch (m) { + case EET_FILE_MODE_READ_WRITE: + case EET_FILE_MODE_WRITE: + tmp = "Permission denied - %s"; + break; + default: + tmp = "File not found - %s"; + break; + } + + rb_raise (rb_eRuntimeError, tmp, file); + } + + return self; +} + +/* + * call-seq: + * ef.close -> ef + * + * Closes _ef_ and flushes any pending writes. + * _ef_ is unavailable for any further data operations; + * an +IOError+ is raised if such an attempt is made. + * + * Eet::File objects are automatically closed when they + * are claimed by the garbage collector. + */ +static VALUE +c_close (VALUE self) +{ + Eet_File **ef = NULL; + + Data_Get_Struct (self, Eet_File *, ef); + + if (!*ef) + rb_raise (rb_eIOError, "closed stream"); + else { + eet_close (*ef); + *ef = NULL; + + eet_shutdown (); + } + + return self; +} + +/* + * call-seq: + * ef.list([glob]) -> array + * + * Returns an Array of entries in _ef_ that match the shell glob + * _glob_ (defaulting to "*"). + */ +static VALUE +c_list (int argc, VALUE *argv, VALUE self) +{ + VALUE glob = Qnil, ret; + Eet_File **ef = NULL; + char **entries, *tmp = "*"; + int i, count = 0; + + Data_Get_Struct (self, Eet_File *, ef); + + if (!*ef) + rb_raise (rb_eIOError, "closed stream"); + + switch (eet_mode_get (*ef)) { + case EET_FILE_MODE_READ: + case EET_FILE_MODE_READ_WRITE: + break; + default: + rb_raise (rb_eIOError, "cannot list entries"); + } + + rb_scan_args (argc, argv, "01", &glob); + + if (!NIL_P (glob)) { + Check_Type (glob, T_STRING); + tmp = StringValuePtr (glob); + } + + ret = rb_ary_new (); + + entries = eet_list (*ef, tmp, &count); + + for (i = 0; i < count; i++) + rb_ary_push (ret, rb_str_new2 (entries[i])); + + free (entries); + + return ret; +} + +/* + * call-seq: + * ef.delete(key) -> ef + * + * Deletes the entry from _ef_ that is stored as _key_. + * If the entry cannot be deleted, an +IOError+ is raised, + * otherwise _ef_ is returned. + */ +static VALUE +c_delete (VALUE self, VALUE key) +{ + Eet_File **ef = NULL; + char *tmp; + + Data_Get_Struct (self, Eet_File *, ef); + + if (!*ef) + rb_raise (rb_eIOError, "closed stream"); + + CHECK_KEY (key); + + tmp = StringValuePtr (key); + + if (!eet_delete (*ef, tmp)) + rb_raise (rb_eIOError, "cannot delete entry - %s", tmp); + + return self; +} + +/* + * call-seq: + * ef.read(key) -> string + * + * Reads an entry from _ef_ that is stored as _key_. + * If the data cannot be read, an +IOError+ is raised, + * otherwise a String is returned that contains the data. + */ +static VALUE +c_read (VALUE self, VALUE key) +{ + VALUE ret; + Eet_File **ef = NULL; + void *data; + int size = 0; + + Data_Get_Struct (self, Eet_File *, ef); + + if (!*ef) + rb_raise (rb_eIOError, "closed stream"); + + CHECK_KEY (key); + + data = eet_read (*ef, StringValuePtr (key), &size); + if (!data) + rb_raise (rb_eIOError, "cannot read entry - %s", key); + + ret = rb_str_new (data, size); + + free (data); + + return ret; +} + +/* + * call-seq: + * ef.write(key, data [, compress] ) -> integer + * + * Stores _data_ in _ef_ as _key_. + * If _compress_ is true (which is the default), the data will be + * compressed. + * If the data cannot be written, an +IOError+ is raised, + * otherwise a the number of bytes written is returned. + */ +static VALUE +c_write (int argc, VALUE *argv, VALUE self) +{ + VALUE key = Qnil, buf = Qnil, comp = Qnil; + Eet_File **ef = NULL; + int n; + + Data_Get_Struct (self, Eet_File *, ef); + + if (!*ef) + rb_raise (rb_eIOError, "closed stream"); + + rb_scan_args (argc, argv, "21", &key, &buf, &comp); + + if (NIL_P (comp)) + comp = Qtrue; + + CHECK_KEY (key); + Check_Type (buf, T_STRING); + + n = eet_write (*ef, StringValuePtr (key), + StringValuePtr (buf), RSTRING (buf)->len, + comp == Qtrue); + if (!n) + rb_raise (rb_eIOError, "couldn't write to file"); + else + return INT2FIX (n); +} + +/* + * call-seq: + * ef.read_image(key) -> array + * + * Reads an image entry from _ef_ that is stored as _key_. + * If the data cannot be read, an +IOError+ is raised, + * otherwise an Array is returned that contains the image data, + * the image width, the image height and a boolean that indicates + * whether the image has an alpha channel or not. + */ +static VALUE +c_read_image (VALUE self, VALUE key) +{ + VALUE ret; + Eet_File **ef = NULL; + void *data; + int w = 0, h = 0, has_alpha = 0; + + Data_Get_Struct (self, Eet_File *, ef); + + if (!*ef) + rb_raise (rb_eIOError, "closed stream"); + + CHECK_KEY (key); + + data = eet_data_image_read (*ef, StringValuePtr (key), &w, &h, + &has_alpha, NULL, NULL, NULL); + if (!data) + rb_raise (rb_eIOError, "cannot read entry - %s", key); + + ret = rb_ary_new3 (4, rb_str_new (data, w * h * 4), + INT2FIX (w), INT2FIX (h), + has_alpha ? Qtrue : Qfalse); + free (data); + + return ret; +} + +/* + * call-seq: + * ef.write_image(key, image_data, w, h [, alpha] ) -> integer + * + * Stores _image_data_ with width _w_ and height _h_ in _ef_ as _key_. + * Pass true for _alpha_ if the image contains an alpha channel. + * If the data cannot be written, an +IOError+ is raised, + * otherwise a the number of bytes written is returned. + */ +static VALUE +c_write_image (int argc, VALUE *argv, VALUE self) +{ + VALUE key = Qnil, buf = Qnil, w = Qnil, h = Qnil, has_alpha = Qnil; + Eet_File **ef = NULL; + int n; + + Data_Get_Struct (self, Eet_File *, ef); + + if (!*ef) + rb_raise (rb_eIOError, "closed stream"); + + rb_scan_args (argc, argv, "41", &key, &buf, &w, &h, &has_alpha); + + if (NIL_P (has_alpha)) + has_alpha = Qfalse; + + CHECK_KEY (key); + Check_Type (buf, T_STRING); + Check_Type (w, T_FIXNUM); + Check_Type (h, T_FIXNUM); + + if (!RSTRING (buf)->len) + return INT2FIX (0); + + n = eet_data_image_write (*ef, StringValuePtr (key), + StringValuePtr (buf), + FIX2INT (w), FIX2INT (h), + has_alpha == Qtrue, 9, 0, 0); + if (!n) + rb_raise (rb_eIOError, "couldn't write to file"); + else + return INT2FIX (n); +} + +void +Init_eet_ext () +{ + VALUE m, c; + + m = rb_define_module ("Eet"); + + c = rb_define_class_under (m, "File", rb_cObject); + rb_define_alloc_func (c, c_alloc); + rb_define_singleton_method (c, "open", c_open, -1); + rb_define_method (c, "initialize", c_init, -1); + rb_define_method (c, "close", c_close, 0); + rb_define_method (c, "list", c_list, -1); + rb_define_method (c, "delete", c_delete, 1); + rb_define_method (c, "read", c_read, 1); + rb_define_method (c, "write", c_write, -1); + rb_define_method (c, "read_image", c_read_image, 1); + rb_define_method (c, "write_image", c_write_image, -1); +} diff --git a/lib/eet.rb b/lib/eet.rb new file mode 100644 index 0000000..abf8814 --- /dev/null +++ b/lib/eet.rb @@ -0,0 +1,233 @@ +#-- +# $Id: eet.rb 1 2005-03-26 01:45:38Z tilman $ +# +# Copyright (c) 2005 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 "eet_ext" + +class Object + # :call-seq: + # object.to_eet -> string + # + # Serializes the receiver to EET format. + def to_eet + props = to_eet_properties + + unless props.is_a?(Hash) && !props.empty? + raise(Eet::PropertyError, "invalid EET properties") + end + + eet_name = to_eet_name + + if eet_name.to_str.length < 1 || eet_name.to_str.include?(0) + raise(Eet::NameError, "invalid EET name") + end + + stream = Eet::Stream.new + + props.each_pair do |tag, arg| + unless arg.is_a?(Array) + raise(Eet::PropertyError, "hash value not an array") + end + + value, type = arg + next if value.nil? + + stream.push(*value.to_eet_chunks(tag, type)) + end + + chunk = Eet::Chunk.new(eet_name, stream.serialize) + Eet::Stream.new(chunk).serialize + end + + def to_eet_chunks(tag, type = nil) # :nodoc: + [Eet::Chunk.new(tag, to_eet)] + end + + protected + + # :call-seq: + # object.to_eet_name -> string + # + # Returns the tag that's stored with the data for _object_. + # If your class doesn't override this method, the class name will be + # used. + def to_eet_name + self.class.name + end + + # :call-seq: + # object.to_eet_properties -> hash + # + # Returns a hash that contains the properties that are stored for + # _object_. + # If your class doesn't override this method, all instance variables + # of _object_ will be stored. + def to_eet_properties + instance_variables.inject({}) do |h, var| + h[var[1..-1]] = [instance_variable_get(var)] + h + end + end +end + +class Integer # :nodoc: + def to_eet_chunks(tag, type = nil) + fmt = case type + when :char: "c" + when :short: "v" + when :long_long: "q" + else "V" + end + + data = [self].pack(fmt) + [Eet::Chunk.new(tag, data)] + end +end + +class Float # :nodoc: + def to_eet_chunks(tag, type = nil) + fmt = case type + when :double: "%32.32f" + else "%16.16f" + end + + data = fmt % self + [Eet::Chunk.new(tag, data + "\0")] + end +end + +class String # :nodoc: + def to_eet_chunks(tag, type = nil) + [Eet::Chunk.new(tag, self + "\0")] + end +end + +class TrueClass # :nodoc: + def to_eet_chunks(tag, type = nil) + [Eet::Chunk.new(tag, [1].pack("c"))] + end +end + +class FalseClass # :nodoc: + def to_eet_chunks(tag, type = nil) + [Eet::Chunk.new(tag, [0].pack("c"))] + end +end + +class Array # :nodoc: + def to_eet_chunks(tag, type = nil) + case type + when :sub + [Eet::Chunk.new(tag, self.to_eet)] + else + # lists always hold subtypes + map { |item| Eet::Chunk.new(tag, item.to_eet) } + end + end +end + +class Hash # :nodoc: + def to_eet_chunks(tag, type = nil) + # lists always hold subtypes + map { |(key, value)| Eet::Chunk.new(tag, value.to_eet) } + end +end + +module Eet + VERSION = "0.0.1" + + class EetError < StandardError; end + class NameError < EetError; end + class PropertyError < EetError; end + class ChunkError < EetError; end + + class Stream < Array # :nodoc: + def initialize(chunk = nil) + super(chunk.nil? ? 0 : 1, chunk) + end + + def serialize + inject("") { |a, c| a << c.serialize } + end + + def Stream.deserialize(data) + data = data.to_str.dup + s = Stream.new + + while data.length > 0 + s << Chunk.deserialize(data) + end + + s + end + end + + class Chunk # :nodoc: + attr_reader :tag, :data + + def initialize(tag, data) + if tag.to_str.include?(0) + raise(ArgumentError, + "tag must not contain binary zeroes") + end + + @tag = tag.to_str.dup.freeze + @data = data.to_str.dup.freeze + + @size = @tag.length + 1 + @data.length + + # libeet uses a signed 32bit integer to store the + # chunk size, so make sure we don't overflow it + if @size >= (1 << 31) + raise(ArgumentError, "tag or data too long") + end + end + + def serialize + buf = "CHnK" + buf << [@size].pack("V") + buf << @tag << "\0" << @data + end + + def Chunk.deserialize(data) + if data.length < 8 || data[0, 4] != "CHnK" + raise(ChunkError, "invalid data") + end + + size = data[4, 4].unpack("V").first + if size >= (1 << 31) || size > data.length - 8 + raise(ChunkError, "invalid chunk size") + end + + unless data[8, size].include?(0) + raise(ChunkError, "invalid chunk data") + end + + c = Chunk.new(*data[8, size].split("\0", 2)) + + data.replace(data[8 + size..-1] || "") + + c + end + end +end diff --git a/test/common.rb b/test/common.rb new file mode 100644 index 0000000..877ecf4 --- /dev/null +++ b/test/common.rb @@ -0,0 +1,32 @@ +# $Id: common.rb 1 2005-03-26 01:45:38Z tilman $ + +class WrappedString + def initialize(blah) + @blah = blah + end + + private + def to_eet_name + "String" + end + + def to_eet_properties + {"buf" => [@blah]} + end +end + +module Test + module Unit + module Assertions + def assert_run_successful(cmd, msg = nil) + `#{cmd}` + st = $?.exitstatus + + message = build_message(msg, <. +EOT + assert_block(message) { st == 0 } + end + end + end +end diff --git a/test/test_basic.rb b/test/test_basic.rb new file mode 100644 index 0000000..73a30bc --- /dev/null +++ b/test/test_basic.rb @@ -0,0 +1,68 @@ +# $Id: test_basic.rb 1 2005-03-26 01:45:38Z tilman $ + +require "eet" +require "test/unit" +require "common" + +class BasicTestData + def initialize + @name = "moo" + @short = 512 + @int = 1024 + @long_long = (2 << 63) - 1 + @flag = true + @float = 1234.0 + @double = 12341234.0 + end + + private + def to_eet_name + "BasicTest" + end + + def to_eet_properties + {"name" => [@name], + "i16" => [@short, :short], + "i32" => [@int], + "i64" => [@long_long, :long_long], + "flag" => [@flag], + "f32" => [@float], + "f64" => [@double, :double]} + end +end + +class BasicTest < Test::Unit::TestCase + def test_basic + data = BasicTestData.new.to_eet + assert_not_nil(data) + + stream = nil + + assert_nothing_raised do + stream = Eet::Stream.deserialize(data) + end + + assert_equal(1, stream.length) + assert_equal("BasicTest", stream.first.tag) + + assert_nothing_raised do + stream = Eet::Stream.deserialize(stream.first.data) + end + + assert_equal(7, stream.length) + + values = {"name" => "moo\0", + "i16" => "\0\2", "i32" => "\0\4\0\0", + "i64" => "\377" * 8, "flag" => "\1", + "f32" => "1234." + ("0" * 16) + "\0", + "f64" => "12341234." + ("0" * 32) + "\0"} + values.each do |k, v| + found = stream.find { |c| c.tag == k } + assert_not_nil(found, "chunk not found - #{k}") + assert_equal(v, found.data) + stream.delete(found) + end + + assert_equal([], stream) + end +end diff --git a/test/test_broken_classes.rb b/test/test_broken_classes.rb new file mode 100644 index 0000000..54db171 --- /dev/null +++ b/test/test_broken_classes.rb @@ -0,0 +1,86 @@ +# $Id: test_broken_classes.rb 1 2005-03-26 01:45:38Z tilman $ + +class BrokenTestData1 +end + +class BrokenTestData2 + def to_eet_properties + end +end + +class BrokenTestData3 + def to_eet_properties + ["blah"] + end +end + +class BrokenTestData4 + def to_eet_properties + {} + end +end + +class BrokenTestData5 + def to_eet_name + "blah" + end + + def to_eet_properties + {"" => nil} + end +end + +class BrokenTestData6 + def to_eet_name + "blah" + end + + def to_eet_properties + {"blah" => nil} + end +end + +class BrokenTestData7 + def to_eet_properties + {"blah" => [nil]} + end +end + +class BrokenTestData8 + def to_eet_name + end + + def to_eet_properties + {"blah" => [nil]} + end +end + +class BrokenTestData9 + def to_eet_name + "" + end + + def to_eet_properties + {"blah" => [nil]} + end +end + +class BrokenTest < Test::Unit::TestCase + def test_broken_properties + (1..6).each do |i| + assert_raise(Eet::PropertyError) do + Object.const_get("BrokenTestData#{i}").new.to_eet + end + end + end + + def test_broken_eet_names + assert_raise(NoMethodError) do + BrokenTestData8.new.to_eet + end + + assert_raise(Eet::NameError) do + BrokenTestData9.new.to_eet + end + end +end diff --git a/test/test_chunk.rb b/test/test_chunk.rb new file mode 100644 index 0000000..2891dba --- /dev/null +++ b/test/test_chunk.rb @@ -0,0 +1,49 @@ +# $Id: test_chunk.rb 1 2005-03-26 01:45:38Z tilman $ + +require "eet" +require "test/unit" + +class ChunkTest < Test::Unit::TestCase + def setup + @data = Eet::Chunk.new("tag", "foo\0bar\0baz").serialize + end + + def test_serialize + assert_equal("CHnK\017\0\0\0tag\0foo\0bar\0baz", @data) + end + + def test_deserialize + chunk = nil + + assert_nothing_raised do + chunk = Eet::Chunk.deserialize(@data) + end + + assert_equal("tag", chunk.tag) + assert_equal("foo\0bar\0baz", chunk.data) + end + + def test_deserialize_invalid + assert_raise(Eet::ChunkError) do + Eet::Chunk.deserialize("foobar" << "\377" * 4 << "tagdata") + end + + assert_raise(Eet::ChunkError) do + Eet::Chunk.deserialize("CHnK" << "\377" * 4 << "tagdata") + end + + assert_raise(Eet::ChunkError) do + Eet::Chunk.deserialize("CHnK\010\0\0\0tag0data") + end + + assert_nothing_raised do + Eet::Chunk.deserialize("CHnK\10\0\0\0tag\0data") + end + end + + def test_catch_invalid_tag + assert_raise(ArgumentError) do + Eet::Chunk.new("foo\0bar", "data") + end + end +end diff --git a/test/test_hash.rb b/test/test_hash.rb new file mode 100644 index 0000000..b77dac3 --- /dev/null +++ b/test/test_hash.rb @@ -0,0 +1,86 @@ +# $Id: test_hash.rb 1 2005-03-26 01:45:38Z tilman $ + +require "eet" +require "test/unit" +require "common" + +class HashEntry + def initialize(key, value) + @key = key.to_str.dup.freeze + @value = value.to_str.dup.freeze + end +end + +class HashTestData + def initialize + @hash = {"artist" => HashEntry.new("artist", "Amon Amarth"), + "title" => HashEntry.new("title", "Death In Fire")} + end + + private + def to_eet_name + "HashTest" + end + + def to_eet_properties + {"hash" => [@hash]} + end +end + +class HashTest < Test::Unit::TestCase + def test_hash + data = HashTestData.new.to_eet + assert_not_nil(data) + + stream = nil + + assert_nothing_raised do + stream = Eet::Stream.deserialize(data) + end + + assert_equal(1, stream.length) + assert_equal("HashTest", stream.first.tag) + + + assert_nothing_raised do + stream = Eet::Stream.deserialize(stream.first.data) + end + + assert_equal(2, stream.length) + + 2.times do + hashlist = stream.find_all { |c| c.tag == "hash" } + assert_equal(2, hashlist.length) + + hash = {"artist" => "Amon Amarth", + "title" => "Death In Fire"} + hash.each do |key, value| + str_stream = nil + + assert_nothing_raised do + d = hashlist.shift.data + str_stream = Eet::Stream.deserialize(d) + end + + assert_equal(1, str_stream.length) + assert_equal("HashEntry", str_stream.first.tag) + + foo = nil + + assert_nothing_raised do + foo = Eet::Stream.deserialize(str_stream.first.data) + end + + assert_equal(2, foo.length) + + {"key" => key, "value" => value}.each do |tag, data| + found = foo.find { |i| i.tag == tag } + assert_not_nil(found) + assert_equal(data + "\0", found.data) + end + end + + assert_equal([], hashlist) + end + end +end diff --git a/test/test_list.rb b/test/test_list.rb new file mode 100644 index 0000000..782ba97 --- /dev/null +++ b/test/test_list.rb @@ -0,0 +1,82 @@ +# $Id: test_list.rb 1 2005-03-26 01:45:38Z tilman $ + +require "eet" +require "test/unit" +require "common" + +class ListTestData + def initialize + @strings = {} + @strings2 = [] + + 5.times do |i| + @strings[i] = WrappedString.new("rubyrocks%i" % i) + end + + #(6..10).each do |i| + 5.times do |i| + @strings2 << WrappedString.new("rubyrocks%i" % i) + end + end + + private + def to_eet_name + "ListTest" + end + + def to_eet_properties + {"stringlist_a" => [@strings], + "stringlist_h" => [@strings2]} + end +end + +class ListTest < Test::Unit::TestCase + def test_list + data = ListTestData.new.to_eet + assert_not_nil(data) + + stream = nil + + assert_nothing_raised do + stream = Eet::Stream.deserialize(data) + end + + assert_equal(1, stream.length) + assert_equal("ListTest", stream.first.tag) + + assert_nothing_raised do + stream = Eet::Stream.deserialize(stream.first.data) + end + + assert_equal(10, stream.length) + + ["stringlist_a", "stringlist_h"].each do |tag| + stringlist = stream.find_all { |c| c.tag == tag } + assert_equal(5, stringlist.length) + + (0..4).each do |i| + str_stream = nil + + assert_nothing_raised do + d = stringlist.shift.data + str_stream = Eet::Stream.deserialize(d) + end + + assert_equal(1, str_stream.length) + assert_equal("String", str_stream.first.tag) + + foo = nil + + assert_nothing_raised do + foo = Eet::Stream.deserialize(str_stream.first.data) + end + + assert_equal(1, foo.length) + assert_equal("buf", foo.first.tag) + assert_equal("rubyrocks%i\0" % i, foo.first.data) + end + + assert_equal([], stringlist) + end + end +end diff --git a/test/test_misc.rb b/test/test_misc.rb new file mode 100644 index 0000000..e97634a --- /dev/null +++ b/test/test_misc.rb @@ -0,0 +1,177 @@ +# $Id: test_misc.rb 1 2005-03-26 01:45:38Z tilman $ + +require "eet" +require "test/unit" +require "ftools" + +class Foo + def initialize + @foo = "foo" + @bar = true + @baz = 512 + end +end + +class MiscTest < Test::Unit::TestCase + def setup + @keys = ["key_foo", "key_bar", "key_baz"].sort + @dest = __FILE__.sub(/\.rb$/, ".eet") + @foo_data = Foo.new.to_eet + + assert_not_nil(@foo_data) + + File.rm_f(@dest) + + Eet::File.open(@dest, "w") do |ef| + @keys.each do |k| + assert(ef.write(k, @foo_data) > 0) + end + end + end + + def test_stream_chunks + stream = nil + + assert_nothing_raised do + stream = Eet::Stream.deserialize(@foo_data) + end + + assert_equal(1, stream.length) + assert_equal("Foo", stream.first.tag) + + assert_nothing_raised do + stream = Eet::Stream.deserialize(stream.first.data) + end + + assert_equal(3, stream.length) + ["foo", "bar", "baz"].each do |tag| + found = stream.find { |c| c.tag == tag } + assert_not_nil(found) + end + end + + def test_create_new + assert_raise(RuntimeError) do + Eet::File.open("/foo/bar/baz/xyzzy", "w") { |ef| } + end + end + + def test_catch_invalid_open_mode + assert_raise(ArgumentError) do + Eet::File.open(@dest, @keys[0]) { |ef| } + end + end + + def test_zero_write + assert_raise(IOError) do + Eet::File.open(@dest, "w") do |ef| + ef.write("empty", "") + end + end + end + + def test_open_existing_ro + # if mode isn't specified, the file must be opened read-only. + # assert that we cannot write in read-only mode. + Eet::File.open(@dest) do |ef| + assert_raise(IOError) do + ef.write("xyzzy", "data") + end + end + end + + def test_open_existing_w + # if the file is opened in write-only or read-write mode, + # we must be able to write to it. + ["w", "r+"].each do |mode| + Eet::File.open(@dest, mode) do |ef| + assert_equal(4, ef.write("xyzzy", "data")) + end + end + end + + def test_accessing_closed_file + ef = Eet::File.open(@dest, "r") + ef.close + + assert_raise(IOError) do + ef.read(@keys[0]) + end + end + + def test_delete_entry_ro + assert_raise(IOError) do + Eet::File.open(@dest, "r") do |ef| + ef.delete(@keys[0]) + end + end + end + + def test_delete_nonexisting_entry + assert_raise(IOError) do + Eet::File.open(@dest, "w") do |ef| + ef.delete("nonexistingkey") + end + end + end + + def test_read + Eet::File.open(@dest, "r") do |ef| + @keys.each do |k| + assert_equal(@foo_data, ef.read(k)) + end + end + + assert_raise(IOError) do + Eet::File.open(@dest, "w") do |ef| + ef.read(@keys[0]) + end + end + end + + def test_list + ["r", "r+"].each do |mode| + Eet::File.open(@dest, mode) do |ef| + assert_equal(@keys, ef.list.sort) + assert_equal(@keys, ef.list("*").sort, @keys) + assert_equal([@keys[0]], ef.list(@keys[0])) + end + end + + assert_raise(IOError) do + Eet::File.open(@dest, "w") do |ef| + ef.list + end + end + end + + def test_delete + assert_nothing_raised do + Eet::File.open(@dest, "r+") do |ef| + ef.delete(@keys[0]) + end + end + + Eet::File.open(@dest) do |ef| + tmp = @keys.shift + assert_equal(@keys, ef.list.sort) + @keys.unshift(tmp) + end + end + + def test_invalid_key + Eet::File.open(@dest, "r+") do |ef| + assert_raise(ArgumentError) do + ef.read("key_\0foo") + end + + assert_raise(ArgumentError) do + ef.write("key_\0foo", "data") + end + + assert_raise(ArgumentError) do + ef.delete("key_\0foo") + end + end + end +end diff --git a/test/test_stream.rb b/test/test_stream.rb new file mode 100644 index 0000000..68e7ea4 --- /dev/null +++ b/test/test_stream.rb @@ -0,0 +1,42 @@ +# $Id: test_stream.rb 1 2005-03-26 01:45:38Z tilman $ + +require "eet" +require "test/unit" + +class StreamTest < Test::Unit::TestCase + def setup + stream = Eet::Stream.new + stream << Eet::Chunk.new("tag", "foo") + stream << Eet::Chunk.new("tag2", "bar") + stream << Eet::Chunk.new("tag23", "baz") + + @data = stream.serialize + assert_not_nil(@data) + end + + def test_serialize + assert_equal("CHnK\007\0\0\0tag\0foo" + + "CHnK\010\0\0\0tag2\0bar" + + "CHnK\011\0\0\0tag23\0baz", @data) + end + + def test_deserialize + stream = nil + + assert_nothing_raised do + stream = Eet::Stream.deserialize(@data) + end + + assert_equal(3, stream.length) + + values = {"tag" => "foo", "tag2" => "bar", "tag23" => "baz"} + values.each do |k, v| + found = stream.find { |c| c.tag == k } + assert_not_nil(found, "chunk not found - #{k}") + assert_equal(found.data, v) + stream.delete(found) + end + + assert_equal([], stream) + end +end diff --git a/test/test_sub.rb b/test/test_sub.rb new file mode 100644 index 0000000..a92e5b4 --- /dev/null +++ b/test/test_sub.rb @@ -0,0 +1,59 @@ +# $Id: test_sub.rb 1 2005-03-26 01:45:38Z tilman $ + +require "eet" +require "test/unit" +require "common" + +class SubTestData + def initialize + @string = WrappedString.new("foobar") + end + + private + def to_eet_name + "SubTest" + end + + def to_eet_properties + {"sub_test" => [@string], + "nil_test" => [nil]} + end +end + +class SubTest < Test::Unit::TestCase + def test_sub + data = SubTestData.new.to_eet + assert_not_nil(data) + + stream = nil + + assert_nothing_raised do + stream = Eet::Stream.deserialize(data) + end + + assert_equal(1, stream.length) + assert_equal("SubTest", stream.first.tag) + + assert_nothing_raised do + stream = Eet::Stream.deserialize(stream.first.data) + end + + assert_equal(1, stream.length) + assert_equal("sub_test", stream.first.tag) + + assert_nothing_raised do + stream = Eet::Stream.deserialize(stream.first.data) + end + + assert_equal(1, stream.length) + assert_equal("String", stream.first.tag) + + assert_nothing_raised do + stream = Eet::Stream.deserialize(stream.first.data) + end + + assert_equal(1, stream.length) + assert_equal("buf", stream.first.tag) + assert_equal("foobar\0", stream.first.data) + end +end -- 2.30.2