1 /* nbdkit
2  * Copyright (C) 2013-2020 Red Hat Inc.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  * * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *
11  * * Redistributions in binary form must reproduce the above copyright
12  * notice, this list of conditions and the following disclaimer in the
13  * documentation and/or other materials provided with the distribution.
14  *
15  * * Neither the name of Red Hat nor the names of its contributors may be
16  * used to endorse or promote products derived from this software without
17  * specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
20  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22  * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
23  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
29  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30  * SUCH DAMAGE.
31  */
32 
33 #include <config.h>
34 
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <assert.h>
38 #include <errno.h>
39 
40 #include <nbdkit-plugin.h>
41 
42 #include <ruby.h>
43 #ifdef HAVE_RUBY_VERSION_H
44 #include <ruby/version.h>
45 #endif
46 
47 static VALUE nbdkit_module = Qnil;
48 static int last_error;
49 
50 static VALUE
set_error(VALUE self,VALUE arg)51 set_error (VALUE self, VALUE arg)
52 {
53   int err;
54   VALUE v;
55 
56   if (TYPE(arg) == T_CLASS) {
57     v = rb_const_get (arg, rb_intern ("Errno"));
58     err = NUM2INT (v);
59   } else if (TYPE (arg) == T_OBJECT) {
60     v = rb_funcall (arg, rb_intern ("errno"), 0);
61     err = NUM2INT (v);
62   } else {
63     err = NUM2INT (arg);
64   }
65   last_error = err;
66   nbdkit_set_error (err);
67   return Qnil;
68 }
69 
70 static void
plugin_rb_load(void)71 plugin_rb_load (void)
72 {
73   RUBY_INIT_STACK;
74   ruby_init ();
75   ruby_init_loadpath ();
76 
77   nbdkit_module = rb_define_module ("Nbdkit");
78   rb_define_module_function (nbdkit_module, "set_error", set_error, 1);
79 }
80 
81 /* https://stackoverflow.com/questions/11086549/how-to-rb-protect-everything-in-ruby */
82 #define MAX_ARGS 16
83 struct callback_data {
84   VALUE receiver;               /* object being called */
85   ID method_id;                 /* method on object being called */
86   int argc;                     /* number of args */
87   VALUE argv[MAX_ARGS];         /* list of args */
88 };
89 
90 static VALUE
callback_dispatch(VALUE datav)91 callback_dispatch (VALUE datav)
92 {
93   struct callback_data *data = (struct callback_data *) datav;
94   return rb_funcall2 (data->receiver, data->method_id, data->argc, data->argv);
95 }
96 
97 enum exception_class {
98   NO_EXCEPTION = 0,
99   EXCEPTION_NO_METHOD_ERROR,
100   EXCEPTION_OTHER,
101 };
102 
103 static VALUE
funcall2(VALUE receiver,ID method_id,int argc,volatile VALUE * argv,enum exception_class * exception_happened)104 funcall2 (VALUE receiver, ID method_id, int argc, volatile VALUE *argv,
105           enum exception_class *exception_happened)
106 {
107   struct callback_data data;
108   size_t i, len;
109   int state = 0;
110   volatile VALUE ret, exn, message, backtrace, b;
111 
112   assert (argc <= MAX_ARGS);
113 
114   data.receiver = receiver;
115   data.method_id = method_id;
116   data.argc = argc;
117   for (i = 0; i < argc; ++i)
118     data.argv[i] = argv[i];
119 
120   ret = rb_protect (callback_dispatch, (VALUE) &data, &state);
121   if (state) {
122     /* An exception was thrown.  Get the per-thread exception. */
123     exn = rb_errinfo ();
124 
125     /* We treat NoMethodError specially. */
126     if (rb_obj_is_kind_of (exn, rb_eNoMethodError)) {
127       if (exception_happened)
128         *exception_happened = EXCEPTION_NO_METHOD_ERROR;
129     }
130     else {
131       if (exception_happened)
132         *exception_happened = EXCEPTION_OTHER;
133 
134       /* Print the exception. */
135       message = rb_funcall (exn, rb_intern ("to_s"), 0);
136       nbdkit_error ("ruby: %s", StringValueCStr (message));
137 
138       /* Try to print the backtrace (a list of strings) if it exists. */
139       backtrace = rb_funcall (exn, rb_intern ("backtrace"), 0);
140       if (! NIL_P (backtrace)) {
141         len = RARRAY_LEN (backtrace);
142         for (i = 0; i < len; ++i) {
143           b = rb_ary_entry (backtrace, i);
144           nbdkit_error ("ruby: frame #%zu %s", i, StringValueCStr (b));
145         }
146       }
147     }
148 
149     /* Reset the current thread exception. */
150     rb_set_errinfo (Qnil);
151     return Qnil;
152   }
153   else {
154     if (exception_happened)
155       *exception_happened = NO_EXCEPTION;
156     return ret;
157   }
158 }
159 
160 static const char *script = NULL;
161 static void *code = NULL;
162 
163 static void
plugin_rb_unload(void)164 plugin_rb_unload (void)
165 {
166   if (ruby_cleanup (0) != 0)
167     nbdkit_error ("ruby_cleanup failed");
168 }
169 
170 static void
plugin_rb_dump_plugin(void)171 plugin_rb_dump_plugin (void)
172 {
173 #ifdef RUBY_API_VERSION_MAJOR
174   printf ("ruby_api_version=%d", RUBY_API_VERSION_MAJOR);
175 #ifdef RUBY_API_VERSION_MINOR
176   printf (".%d", RUBY_API_VERSION_MINOR);
177 #ifdef RUBY_API_VERSION_TEENY
178   printf (".%d", RUBY_API_VERSION_TEENY);
179 #endif
180 #endif
181   printf ("\n");
182 #endif
183 
184   if (!script)
185     return;
186 
187   assert (code != NULL);
188 
189   (void) funcall2 (Qnil, rb_intern ("dump_plugin"), 0, NULL, NULL);
190 }
191 
192 static int
plugin_rb_config(const char * key,const char * value)193 plugin_rb_config (const char *key, const char *value)
194 {
195   /* The first parameter must be "script". */
196   if (!script) {
197     int state;
198 
199     if (strcmp (key, "script") != 0) {
200       nbdkit_error ("the first parameter must be script=/path/to/ruby/script.rb");
201       return -1;
202     }
203     script = value;
204 
205     nbdkit_debug ("ruby: loading script %s", script);
206 
207     /* Load the Ruby script into the interpreter. */
208     const char *options[] = { "--", script };
209     code = ruby_options (sizeof options / sizeof options[0],
210                          (char **) options);
211 
212     /* Check if we managed to compile the Ruby script to code. */
213     if (!ruby_executable_node (code, &state)) {
214       nbdkit_error ("could not compile ruby script (%s, state=%d)",
215                     script, state);
216       return -1;
217     }
218 
219     /* Execute the Ruby script. */
220     state = ruby_exec_node (code);
221     if (state) {
222       nbdkit_error ("could not execute ruby script (%s, state=%d)",
223                     script, state);
224       return -1;
225     }
226 
227     return 0;
228   }
229   else {
230     volatile VALUE argv[2];
231     enum exception_class exception_happened;
232 
233     argv[0] = rb_str_new2 (key);
234     argv[1] = rb_str_new2 (value);
235     (void) funcall2 (Qnil, rb_intern ("config"), 2, argv, &exception_happened);
236     if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
237       /* No config method, emulate what core nbdkit does if the
238        * config callback is NULL.
239        */
240       nbdkit_error ("%s: this plugin does not need command line configuration",
241                     script);
242       return -1;
243     }
244     else if (exception_happened == EXCEPTION_OTHER)
245       return -1;
246 
247     return 0;
248   }
249 }
250 
251 static int
plugin_rb_config_complete(void)252 plugin_rb_config_complete (void)
253 {
254   enum exception_class exception_happened;
255 
256   if (!script) {
257     nbdkit_error ("the first parameter must be script=/path/to/ruby/script.rb");
258     return -1;
259   }
260 
261   assert (code != NULL);
262 
263   (void) funcall2 (Qnil, rb_intern ("config_complete"), 0, NULL,
264                    &exception_happened);
265   if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
266     return 0;          /* no config_complete method defined, ignore */
267   else if (exception_happened == EXCEPTION_OTHER)
268     return -1;
269 
270   return 0;
271 }
272 
273 static void *
plugin_rb_open(int readonly)274 plugin_rb_open (int readonly)
275 {
276   volatile VALUE argv[1];
277   volatile VALUE rv;
278   enum exception_class exception_happened;
279 
280   argv[0] = readonly ? Qtrue : Qfalse;
281   rv = funcall2 (Qnil, rb_intern ("open"), 1, argv, &exception_happened);
282   if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
283     nbdkit_error ("%s: missing callback: %s", script, "open");
284     return NULL;
285   }
286   else if (exception_happened == EXCEPTION_OTHER)
287     return NULL;
288 
289   return (void *) rv;
290 }
291 
292 static void
plugin_rb_close(void * handle)293 plugin_rb_close (void *handle)
294 {
295   volatile VALUE argv[1];
296 
297   argv[0] = (VALUE) handle;
298   (void) funcall2 (Qnil, rb_intern ("close"), 1, argv, NULL);
299   /* OK to ignore exceptions here, if they are important then an error
300    * was printed already.
301    */
302 }
303 
304 static int64_t
plugin_rb_get_size(void * handle)305 plugin_rb_get_size (void *handle)
306 {
307   volatile VALUE argv[1];
308   volatile VALUE rv;
309   enum exception_class exception_happened;
310 
311   argv[0] = (VALUE) handle;
312   rv = funcall2 (Qnil, rb_intern ("get_size"), 1, argv, &exception_happened);
313   if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
314     nbdkit_error ("%s: missing callback: %s", script, "get_size");
315     return -1;
316   }
317   else if (exception_happened == EXCEPTION_OTHER)
318     return -1;
319 
320   return NUM2ULL (rv);
321 }
322 
323 static int
plugin_rb_pread(void * handle,void * buf,uint32_t count,uint64_t offset)324 plugin_rb_pread (void *handle, void *buf,
325                  uint32_t count, uint64_t offset)
326 {
327   volatile VALUE argv[3];
328   volatile VALUE rv;
329   enum exception_class exception_happened;
330 
331   argv[0] = (VALUE) handle;
332   argv[1] = ULL2NUM (count);
333   argv[2] = ULL2NUM (offset);
334   rv = funcall2 (Qnil, rb_intern ("pread"), 3, argv, &exception_happened);
335   if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
336     nbdkit_error ("%s: missing callback: %s", script, "pread");
337     return -1;
338   }
339   else if (exception_happened == EXCEPTION_OTHER)
340     return -1;
341 
342   if (RSTRING_LEN (rv) < count) {
343     nbdkit_error ("%s: byte array returned from pread is too small",
344                   script);
345     return -1;
346   }
347 
348   memcpy (buf, RSTRING_PTR (rv), count);
349   return 0;
350 }
351 
352 static int
plugin_rb_pwrite(void * handle,const void * buf,uint32_t count,uint64_t offset)353 plugin_rb_pwrite (void *handle, const void *buf,
354                   uint32_t count, uint64_t offset)
355 {
356   volatile VALUE argv[3];
357   enum exception_class exception_happened;
358 
359   argv[0] = (VALUE) handle;
360   argv[1] = rb_str_new (buf, count);
361   argv[2] = ULL2NUM (offset);
362   (void) funcall2 (Qnil, rb_intern ("pwrite"), 3, argv, &exception_happened);
363   if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
364     nbdkit_error ("%s: missing callback: %s", script, "pwrite");
365     return -1;
366   }
367   else if (exception_happened == EXCEPTION_OTHER)
368     return -1;
369 
370   return 0;
371 }
372 
373 static int
plugin_rb_flush(void * handle)374 plugin_rb_flush (void *handle)
375 {
376   volatile VALUE argv[1];
377   enum exception_class exception_happened;
378 
379   argv[0] = (VALUE) handle;
380   (void) funcall2 (Qnil, rb_intern ("flush"), 1, argv, &exception_happened);
381   if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
382     nbdkit_error ("%s: not implemented: %s", script, "flush");
383     return -1;
384   }
385   else if (exception_happened == EXCEPTION_OTHER)
386     return -1;
387 
388   return 0;
389 }
390 
391 static int
plugin_rb_trim(void * handle,uint32_t count,uint64_t offset)392 plugin_rb_trim (void *handle, uint32_t count, uint64_t offset)
393 {
394   volatile VALUE argv[3];
395   enum exception_class exception_happened;
396 
397   argv[0] = (VALUE) handle;
398   argv[1] = ULL2NUM (count);
399   argv[2] = ULL2NUM (offset);
400   (void) funcall2 (Qnil, rb_intern ("trim"), 3, argv, &exception_happened);
401   if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
402     nbdkit_error ("%s: not implemented: %s", script, "trim");
403     return -1;
404   }
405   else if (exception_happened == EXCEPTION_OTHER)
406     return -1;
407 
408   return 0;
409 }
410 
411 static int
plugin_rb_zero(void * handle,uint32_t count,uint64_t offset,int may_trim)412 plugin_rb_zero (void *handle, uint32_t count, uint64_t offset, int may_trim)
413 {
414   volatile VALUE argv[4];
415   enum exception_class exception_happened;
416 
417   argv[0] = (VALUE) handle;
418   argv[1] = ULL2NUM (count);
419   argv[2] = ULL2NUM (offset);
420   argv[3] = may_trim ? Qtrue : Qfalse;
421   last_error = 0;
422   (void) funcall2 (Qnil, rb_intern ("zero"), 4, argv, &exception_happened);
423   if (last_error == EOPNOTSUPP || last_error == ENOTSUP ||
424       exception_happened == EXCEPTION_NO_METHOD_ERROR) {
425     nbdkit_debug ("zero falling back to pwrite");
426     nbdkit_set_error (EOPNOTSUPP);
427     return -1;
428   }
429   else if (exception_happened == EXCEPTION_OTHER)
430     return -1;
431 
432   return 0;
433 }
434 
435 static int
plugin_rb_can_write(void * handle)436 plugin_rb_can_write (void *handle)
437 {
438   volatile VALUE argv[1];
439   volatile VALUE rv;
440   enum exception_class exception_happened;
441 
442   argv[0] = (VALUE) handle;
443   rv = funcall2 (Qnil, rb_intern ("can_write"), 1, argv, &exception_happened);
444   if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
445     /* Fall back to checking if the pwrite method exists. */
446     rv = rb_funcall (Qnil, rb_intern ("respond_to?"),
447                      2, ID2SYM (rb_intern ("pwrite")), Qtrue);
448   else if (exception_happened == EXCEPTION_OTHER)
449     return -1;
450 
451   return RTEST (rv);
452 }
453 
454 static int
plugin_rb_can_flush(void * handle)455 plugin_rb_can_flush (void *handle)
456 {
457   volatile VALUE argv[1];
458   volatile VALUE rv;
459   enum exception_class exception_happened;
460 
461   argv[0] = (VALUE) handle;
462   rv = funcall2 (Qnil, rb_intern ("can_flush"), 1, argv, &exception_happened);
463   if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
464     /* Fall back to checking if the flush method exists. */
465     rv = rb_funcall (Qnil, rb_intern ("respond_to?"),
466                      2, ID2SYM (rb_intern ("flush")), Qtrue);
467   else if (exception_happened == EXCEPTION_OTHER)
468     return -1;
469 
470   return RTEST (rv);
471 }
472 
473 static int
plugin_rb_is_rotational(void * handle)474 plugin_rb_is_rotational (void *handle)
475 {
476   volatile VALUE argv[1];
477   volatile VALUE rv;
478   enum exception_class exception_happened;
479 
480   argv[0] = (VALUE) handle;
481   rv = funcall2 (Qnil, rb_intern ("is_rotational"), 1, argv,
482                  &exception_happened);
483   if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
484     return 0;
485   else if (exception_happened == EXCEPTION_OTHER)
486     return -1;
487 
488   return RTEST (rv);
489 }
490 
491 static int
plugin_rb_can_trim(void * handle)492 plugin_rb_can_trim (void *handle)
493 {
494   volatile VALUE argv[1];
495   volatile VALUE rv;
496   enum exception_class exception_happened;
497 
498   argv[0] = (VALUE) handle;
499   rv = funcall2 (Qnil, rb_intern ("can_trim"), 1, argv, &exception_happened);
500   if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
501     /* Fall back to checking if the trim method exists. */
502     rv = rb_funcall (Qnil, rb_intern ("respond_to?"),
503                      2, ID2SYM (rb_intern ("trim")), Qtrue);
504   else if (exception_happened == EXCEPTION_OTHER)
505     return -1;
506 
507   return RTEST (rv);
508 }
509 
510 #define plugin_rb_config_help \
511   "script=<FILENAME>     (required) The Ruby plugin to run.\n" \
512   "[other arguments may be used by the plugin that you load]"
513 
514 /* Ruby is inherently unsafe to call in parallel from multiple
515  * threads.
516  */
517 #define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_ALL_REQUESTS
518 
519 static struct nbdkit_plugin plugin = {
520   .name              = "ruby",
521   .version           = PACKAGE_VERSION,
522 
523   .load              = plugin_rb_load,
524   .unload            = plugin_rb_unload,
525   .dump_plugin       = plugin_rb_dump_plugin,
526 
527   .config            = plugin_rb_config,
528   .config_complete   = plugin_rb_config_complete,
529   .config_help       = plugin_rb_config_help,
530 
531   .open              = plugin_rb_open,
532   .close             = plugin_rb_close,
533 
534   .get_size          = plugin_rb_get_size,
535   .can_write         = plugin_rb_can_write,
536   .can_flush         = plugin_rb_can_flush,
537   .is_rotational     = plugin_rb_is_rotational,
538   .can_trim          = plugin_rb_can_trim,
539 
540   .pread             = plugin_rb_pread,
541   .pwrite            = plugin_rb_pwrite,
542   .flush             = plugin_rb_flush,
543   .trim              = plugin_rb_trim,
544   .zero              = plugin_rb_zero,
545 };
546 
547 NBDKIT_REGISTER_PLUGIN(plugin)
548