Re-implemented Object#to_eet in C.
[ruby-eet.git] / ext / ext.c
1 /*
2  * $Id: ext.c 49 2005-05-30 19:52:36Z tilman $
3  *
4  * Copyright (c) 2005 Tilman Sauerbeck (tilman at code-monkey de)
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining
7  * a copy of this software and associated documentation files (the
8  * "Software"), to deal in the Software without restriction, including
9  * without limitation the rights to use, copy, modify, merge, publish,
10  * distribute, sublicense, and/or sell copies of the Software, and to
11  * permit persons to whom the Software is furnished to do so, subject to
12  * the following conditions:
13  *
14  * The above copyright notice and this permission notice shall be
15  * included in all copies or substantial portions of the Software.
16  *
17  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20  * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21  * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22  * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23  * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24  */
25
26 #include <Eet.h>
27 #include <ruby.h>
28 #include <st.h>
29
30 #define CHECK_KEY(key) \
31         if (rb_funcall (key, id_include, 1, INT2FIX (0)) == Qtrue) \
32                 rb_raise (rb_eArgError, "key must not contain binary zeroes");
33
34 #define CHECK_CLOSED(ef) \
35         if (!*(ef)) \
36                 rb_raise (rb_eIOError, "closed stream");
37
38 #ifdef WORDS_BIGENDIAN
39 # define BSWAP32(x) \
40         ((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >> 8) | \
41         (((x) & 0x0000ff00) << 8) | (((x) & 0x000000ff) << 24))
42 #else /* !WORDS_BIGENDIAN */
43 # define BSWAP32(x) (x)
44 #endif /* WORDS_BIGENDIAN */
45
46 static VALUE c_close (VALUE self);
47
48 static VALUE cStream, cChunk,
49              eEetError, eNameError, ePropError;
50 static ID id_include, id_serialize, id_push, id_empty,
51           id_to_eet_chunks, id_to_eet_name, id_to_eet_properties,
52           id_tag, id_data;
53
54 static void
55 c_free (Eet_File **ef)
56 {
57         if (*ef) {
58                 eet_close (*ef);
59                 *ef = NULL;
60
61                 eet_shutdown ();
62         }
63
64         free (ef);
65 }
66
67 static VALUE
68 c_alloc (VALUE klass)
69 {
70         Eet_File **ef = NULL;
71
72         return Data_Make_Struct (klass, Eet_File *, NULL, c_free, ef);
73 }
74
75 /*
76  * call-seq:
77  *  Eet::File.open(file [, mode] )                -> ef or nil
78  *  Eet::File.open(file [, mode] ) { |ef| block } -> nil
79  *
80  * If a block isn't specified, Eet::File.open is a synonym for
81  * Eet::File.new.
82  * If a block is given, it will be invoked with the
83  * Eet::File object as a parameter, and the file will be
84  * automatically closed when the block terminates. The call always
85  * returns +nil+ in this case.
86  */
87 static VALUE
88 c_open (int argc, VALUE *argv, VALUE klass)
89 {
90         VALUE obj = rb_class_new_instance (argc, argv, klass);
91
92         if (rb_block_given_p ())
93                 return rb_ensure (rb_yield, obj, c_close, obj);
94         else
95                 return obj;
96 }
97
98 /*
99  * call-seq:
100  *  Eet::File.new(file [, mode] ) -> ef or nil
101  *
102  * Creates an Eet::File object for _file_.
103  *
104  * _file_ is opened with the specified mode (defaulting to "r").
105  * Possible values for _mode_ are "r" for read-only access,
106  * "w" for write-only access and "r+" for read/write access.
107  */
108 static VALUE
109 c_init (int argc, VALUE *argv, VALUE self)
110 {
111         VALUE file = Qnil, mode = Qnil;
112         Eet_File **ef = NULL;
113         Eet_File_Mode m = EET_FILE_MODE_READ;
114         const char *tmp, *cfile;
115
116         Data_Get_Struct (self, Eet_File *, ef);
117
118         rb_scan_args (argc, argv, "11", &file, &mode);
119
120         cfile = StringValuePtr (file);
121
122         if (!NIL_P (mode)) {
123                 tmp = StringValuePtr (mode);
124                 if (!strcmp (tmp, "r+"))
125                         m = EET_FILE_MODE_READ_WRITE;
126                 else if (!strcmp (tmp, "w"))
127                         m = EET_FILE_MODE_WRITE;
128                 else if (strcmp (tmp, "r"))
129                         rb_raise (rb_eArgError, "illegal access mode %s", tmp);
130         }
131
132         eet_init ();
133
134         *ef = eet_open (cfile, m);
135         if (!*ef) {
136                 switch (m) {
137                         case EET_FILE_MODE_READ_WRITE:
138                         case EET_FILE_MODE_WRITE:
139                                 tmp = "Permission denied - %s";
140                                 break;
141                         default:
142                                 tmp = "File not found - %s";
143                                 break;
144                 }
145
146                 rb_raise (rb_eRuntimeError, tmp, cfile);
147         }
148
149         return self;
150 }
151
152 /*
153  * call-seq:
154  *  ef.close -> ef
155  *
156  * Closes _ef_ and flushes any pending writes.
157  * _ef_ is unavailable for any further data operations;
158  * an +IOError+ is raised if such an attempt is made.
159  *
160  * Eet::File objects are automatically closed when they
161  * are claimed by the garbage collector.
162  */
163 static VALUE
164 c_close (VALUE self)
165 {
166         Eet_File **ef = NULL;
167
168         Data_Get_Struct (self, Eet_File *, ef);
169         CHECK_CLOSED (ef);
170
171         eet_close (*ef);
172         *ef = NULL;
173
174         eet_shutdown ();
175
176         return self;
177 }
178
179 /*
180  * call-seq:
181  *  ef.list([glob]) -> array
182  *
183  * Returns an Array of entries in _ef_ that match the shell glob
184  * _glob_ (defaulting to "*").
185  */
186 static VALUE
187 c_list (int argc, VALUE *argv, VALUE self)
188 {
189         VALUE glob = Qnil, ret;
190         Eet_File **ef = NULL;
191         char **entries, *tmp = "*";
192         int i, count = 0;
193
194         Data_Get_Struct (self, Eet_File *, ef);
195         CHECK_CLOSED (ef);
196
197         switch (eet_mode_get (*ef)) {
198                 case EET_FILE_MODE_READ:
199                 case EET_FILE_MODE_READ_WRITE:
200                         break;
201                 default:
202                         rb_raise (rb_eIOError, "cannot list entries");
203         }
204
205         rb_scan_args (argc, argv, "01", &glob);
206
207         if (!NIL_P (glob))
208                 tmp = StringValuePtr (glob);
209
210         ret = rb_ary_new ();
211
212         entries = eet_list (*ef, tmp, &count);
213
214         for (i = 0; i < count; i++)
215                 rb_ary_push (ret, rb_str_new2 (entries[i]));
216
217         free (entries);
218
219         return ret;
220 }
221
222 /*
223  * call-seq:
224  *  ef.delete(key) -> ef
225  *
226  * Deletes the entry from _ef_ that is stored as _key_.
227  * If the entry cannot be deleted, an +IOError+ is raised,
228  * otherwise _ef_ is returned.
229  */
230 static VALUE
231 c_delete (VALUE self, VALUE key)
232 {
233         Eet_File **ef = NULL;
234         char *ckey;
235
236         Data_Get_Struct (self, Eet_File *, ef);
237         CHECK_CLOSED (ef);
238
239         ckey = StringValuePtr (key);
240         CHECK_KEY (key);
241
242         if (!eet_delete (*ef, ckey))
243                 rb_raise (rb_eIOError, "cannot delete entry - %s", ckey);
244
245         return self;
246 }
247
248 /*
249  * call-seq:
250  *  ef.read(key) -> string
251  *
252  * Reads an entry from _ef_ that is stored as _key_.
253  * If the data cannot be read, an +IOError+ is raised,
254  * otherwise a String is returned that contains the data.
255  */
256 static VALUE
257 c_read (VALUE self, VALUE key)
258 {
259         VALUE ret;
260         Eet_File **ef = NULL;
261         void *data;
262         char *ckey;
263         int size = 0;
264
265         Data_Get_Struct (self, Eet_File *, ef);
266         CHECK_CLOSED (ef);
267
268         ckey = StringValuePtr (key);
269         CHECK_KEY (key);
270
271         data = eet_read (*ef, ckey, &size);
272         if (!data)
273                 rb_raise (rb_eIOError, "cannot read entry - %s", ckey);
274
275         ret = rb_str_new (data, size);
276
277         free (data);
278
279         return ret;
280 }
281
282 /*
283  * call-seq:
284  *  ef.write(key, data [, compress] ) -> integer
285  *
286  * Stores _data_ in _ef_ as _key_.
287  * If _compress_ is true (which is the default), the data will be
288  * compressed.
289  * If the data cannot be written, an +IOError+ is raised,
290  * otherwise a the number of bytes written is returned.
291  */
292 static VALUE
293 c_write (int argc, VALUE *argv, VALUE self)
294 {
295         VALUE key = Qnil, buf = Qnil, comp = Qnil;
296         Eet_File **ef = NULL;
297         char *ckey, *cbuf;
298         int n;
299
300         Data_Get_Struct (self, Eet_File *, ef);
301         CHECK_CLOSED (ef);
302
303         rb_scan_args (argc, argv, "21", &key, &buf, &comp);
304
305         if (NIL_P (comp))
306                 comp = Qtrue;
307
308         ckey = StringValuePtr (key);
309         CHECK_KEY (key);
310         cbuf = StringValuePtr (buf);
311
312         n = eet_write (*ef, ckey,
313                        cbuf, RSTRING (buf)->len,
314                        comp == Qtrue);
315         if (!n)
316                 rb_raise (rb_eIOError, "couldn't write to file");
317         else
318                 return INT2FIX (n);
319 }
320
321 /*
322  * call-seq:
323  *  ef.read_image(key) -> array
324  *
325  * Reads an image entry from _ef_ that is stored as _key_.
326  * If the data cannot be read, an +IOError+ is raised,
327  * otherwise an Array is returned that contains the image data,
328  * the image width, the image height, a boolean that indicates
329  * whether the image has an alpha channel or not and a hash
330  * that contains the compression options that were used to store
331  * the image (see Eet::File#write_image).
332  */
333 static VALUE
334 c_read_image (VALUE self, VALUE key)
335 {
336         VALUE ret, comp;
337         Eet_File **ef = NULL;
338         void *data;
339         char *ckey;
340         int w = 0, h = 0, has_alpha = 0, level = 0, quality = 0, lossy = 0;
341
342         Data_Get_Struct (self, Eet_File *, ef);
343         CHECK_CLOSED (ef);
344
345         ckey = StringValuePtr (key);
346         CHECK_KEY (key);
347
348         data = eet_data_image_read (*ef, ckey, &w, &h,
349                                     &has_alpha, &level, &quality,
350                                     &lossy);
351         if (!data)
352                 rb_raise (rb_eIOError, "cannot read entry - %s", ckey);
353
354         comp = rb_hash_new ();
355         rb_hash_aset (comp, ID2SYM (rb_intern ("lossy")), INT2FIX (lossy));
356         rb_hash_aset (comp, ID2SYM (rb_intern ("level")), INT2FIX (level));
357         rb_hash_aset (comp, ID2SYM (rb_intern ("quality")), INT2FIX (quality));
358
359         ret = rb_ary_new3 (5, rb_str_new (data, w * h * 4),
360                                INT2FIX (w), INT2FIX (h),
361                                has_alpha ? Qtrue : Qfalse, comp);
362         free (data);
363
364         return ret;
365 }
366
367 /*
368  * call-seq:
369  *  ef.write_image(key, image_data, w, h [, alpha] [, comp] ) -> integer
370  *
371  * Stores _image_data_ with width _w_ and height _h_ in _ef_ as _key_.
372  * Pass true for _alpha_ if the image contains an alpha channel.
373  * You can control how the image is compressed by passing _comp_, which
374  * is a hash whose :lossy entry is true if the image should be
375  * compressed lossily. If :lossy is true, the :quality entry
376  * (0-100, default 100) sets the compression quality.
377  * If :lossy is false, the :level entry (0-9, default 9) sets the
378  * compression level. If _comp_ isn't passed, then the
379  * image is stored losslessly.
380  * If the data cannot be written, an +IOError+ is raised,
381  * otherwise a the number of bytes written is returned.
382  */
383 static VALUE
384 c_write_image (int argc, VALUE *argv, VALUE self)
385 {
386         VALUE key = Qnil, buf = Qnil, w = Qnil, h = Qnil, has_alpha = Qnil;
387         VALUE comp = Qnil, tmp;
388         Eet_File **ef = NULL;
389         char *ckey, *cbuf;
390         int n, lossy = 0, level = 9, quality = 100;
391
392         Data_Get_Struct (self, Eet_File *, ef);
393         CHECK_CLOSED (ef);
394
395         rb_scan_args (argc, argv, "42", &key, &buf, &w, &h, &has_alpha,
396                       &comp);
397
398         if (NIL_P (has_alpha))
399                 has_alpha = Qfalse;
400
401         ckey = StringValuePtr (key);
402         CHECK_KEY (key);
403         cbuf = StringValuePtr (buf);
404         Check_Type (w, T_FIXNUM);
405         Check_Type (h, T_FIXNUM);
406
407         if (!NIL_P (comp)) {
408                 Check_Type (comp, T_HASH);
409
410                 tmp = rb_hash_aref (comp, ID2SYM (rb_intern ("lossy")));
411                 if (!NIL_P (tmp))
412                         lossy = FIX2INT (tmp);
413
414                 tmp = rb_hash_aref (comp, ID2SYM (rb_intern ("level")));
415                 if (!NIL_P (tmp))
416                         level = FIX2INT (tmp);
417
418                 tmp = rb_hash_aref (comp, ID2SYM (rb_intern ("quality")));
419                 if (!NIL_P (tmp))
420                         quality = FIX2INT (tmp);
421         }
422
423         if (!RSTRING (buf)->len)
424                 return INT2FIX (0);
425
426         n = eet_data_image_write (*ef, ckey, cbuf,
427                                   FIX2INT (w), FIX2INT (h),
428                                   has_alpha == Qtrue,
429                                   level, quality, lossy);
430         if (!n)
431                 rb_raise (rb_eIOError, "couldn't write to file");
432         else
433                 return INT2FIX (n);
434 }
435
436 static VALUE
437 stream_serialize (VALUE self)
438 {
439         VALUE ret;
440         struct RArray *stream;
441         long i;
442
443         ret = rb_str_new2 ("");
444
445         stream = RARRAY (self);
446         if (!stream->len)
447                 return ret;
448
449         for (i = 0; i < stream->len; i++) {
450                 VALUE str = rb_funcall (stream->ptr[i], id_serialize, 0, NULL);
451
452                 rb_str_append (ret, str);
453         }
454
455         return ret;
456 }
457
458 static VALUE
459 chunk_init (VALUE self, VALUE tag, VALUE data)
460 {
461         unsigned long len;
462
463         StringValue (tag);
464         StringValue (data);
465
466         if (rb_funcall (tag, id_include, 1, INT2FIX (0)) == Qtrue) \
467                 rb_raise (rb_eArgError, "tag must not contain binary zeroes");
468
469         /* libeet uses a signed 32bit integer to store the
470          * chunk size, so make sure we don't overflow it
471          */
472         len = RSTRING (tag)->len + 1 + RSTRING (data)->len;
473         if (len < 0 || len >= 2147483647L)
474                 rb_raise (rb_eArgError, "tag or data too long");
475
476         rb_ivar_set (self, id_tag, rb_str_dup_frozen (tag));
477         rb_ivar_set (self, id_data, rb_str_dup_frozen (data));
478
479         return self;
480 }
481
482 static VALUE
483 chunk_serialize (VALUE self)
484 {
485         VALUE tmp, ret;
486         unsigned int size, buf_len;
487         unsigned char *buf;
488         struct RString *tag, *data;
489
490         tmp = rb_ivar_get (self, id_tag);
491         StringValue (tmp);
492         tag = RSTRING (tmp);
493
494         tmp = rb_ivar_get (self, id_data);
495         StringValue (tmp);
496         data = RSTRING (tmp);
497
498         buf_len = 9 + tag->len + data->len;
499         ret = rb_str_buf_new (buf_len);
500
501         buf = RSTRING (ret)->ptr;
502         RSTRING (ret)->len = buf_len;
503
504         memcpy (buf, "CHnK", 4);
505         buf += 4;
506
507         size = tag->len + data->len + 1;
508         size = BSWAP32 (size);
509         memcpy (buf, &size, 4);
510         buf += 4;
511
512         memcpy (buf, tag->ptr, tag->len);
513         buf += tag->len;
514
515         *buf++ = 0;
516
517         memcpy (buf, data->ptr, data->len);
518
519         return ret;
520 }
521
522 static int
523 for_each_prop (VALUE tag, VALUE arg, VALUE stream)
524 {
525         VALUE value, type, chunks;
526         long len, i;
527
528         if (rb_obj_is_kind_of (arg, rb_cArray) == Qfalse)
529                 rb_raise (ePropError, "hash value is not an array");
530
531         value = rb_ary_entry (arg, 0);
532         if (NIL_P (value))
533                 return ST_CONTINUE;
534
535         type = rb_ary_entry (arg, 1);
536         chunks = rb_funcall (value, id_to_eet_chunks, 2, tag, type);
537
538         len = RARRAY (chunks)->len;
539
540         for (i = 0; i < len; i++)
541                 rb_funcall (stream, id_push, 1, rb_ary_entry (chunks, i));
542
543         return ST_CONTINUE;
544 }
545
546 /*
547  * :call-seq:
548  *  object.to_eet -> string
549  *
550  * Serializes the receiver to EET format.
551  */
552 static VALUE
553 c_to_eet (VALUE self)
554 {
555         VALUE props, name, stream, chunk, args[2];
556
557         props = rb_funcall (self, id_to_eet_properties, 0);
558
559         if (rb_obj_is_kind_of (props, rb_cHash) == Qfalse ||
560             rb_funcall (props, id_empty, 0) == Qtrue)
561                 rb_raise (ePropError, "invalid EET properties");
562
563         name = rb_funcall (self, id_to_eet_name, 0);
564         StringValue (name);
565
566         if (rb_funcall (name, id_include, 1, INT2FIX (0)) == Qtrue ||
567             rb_funcall (name, id_empty, 0) == Qtrue)
568                 rb_raise (eNameError, "invalid EET name");
569
570         stream = rb_class_new_instance (0, NULL, cStream);
571
572         rb_hash_foreach (props, for_each_prop, stream);
573
574         args[0] = name;
575         args[1] = rb_funcall (stream, id_serialize, 0);
576         chunk = rb_class_new_instance (2, args, cChunk);
577
578         stream = rb_class_new_instance (1, &chunk, cStream);
579
580         return rb_funcall (stream, id_serialize, 0);
581 }
582
583 void
584 Init_eet_ext ()
585 {
586         VALUE m, c;
587
588         m = rb_define_module ("Eet");
589
590         c = rb_define_class_under (m, "File", rb_cObject);
591         rb_define_alloc_func (c, c_alloc);
592         rb_define_singleton_method (c, "open", c_open, -1);
593         rb_define_method (c, "initialize", c_init, -1);
594         rb_define_method (c, "close", c_close, 0);
595         rb_define_method (c, "list", c_list, -1);
596         rb_define_method (c, "delete", c_delete, 1);
597         rb_define_method (c, "read", c_read, 1);
598         rb_define_method (c, "write", c_write, -1);
599         rb_define_method (c, "read_image", c_read_image, 1);
600         rb_define_method (c, "write_image", c_write_image, -1);
601
602         cStream = rb_define_class_under (m, "Stream", rb_cArray);
603         rb_define_method (cStream, "serialize", stream_serialize, 0);
604
605         cChunk = rb_define_class_under (m, "Chunk", rb_cObject);
606         rb_define_method (cChunk, "initialize", chunk_init, 2);
607         rb_define_method (cChunk, "serialize", chunk_serialize, 0);
608
609         rb_define_attr (cChunk, "tag", 1, 0);
610         rb_define_attr (cChunk, "data", 1, 0);
611
612         rb_define_method (rb_cObject, "to_eet", c_to_eet, 0);
613
614         eEetError = rb_define_class_under (m, "EetError", rb_eStandardError);
615         eNameError = rb_define_class_under (m, "NameError", eEetError);
616         ePropError = rb_define_class_under (m, "PropertyError", eEetError);
617
618         id_include = rb_intern ("include?");
619         id_serialize = rb_intern ("serialize");
620         id_push = rb_intern ("push");
621         id_empty = rb_intern ("empty?");
622         id_to_eet_chunks = rb_intern ("to_eet_chunks");
623         id_to_eet_name = rb_intern ("to_eet_name");
624         id_to_eet_properties = rb_intern ("to_eet_properties");
625         id_tag = rb_intern ("@tag");
626         id_data = rb_intern ("@data");
627 }