mini-max-rb 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/bootsnap-1.24.6/CHANGELOG.md +473 -0
  3. data/bootsnap-1.24.6/LICENSE.txt +22 -0
  4. data/bootsnap-1.24.6/README.md +391 -0
  5. data/bootsnap-1.24.6/exe/bootsnap +5 -0
  6. data/bootsnap-1.24.6/ext/bootsnap/bootsnap.c +1235 -0
  7. data/bootsnap-1.24.6/ext/bootsnap/extconf.rb +34 -0
  8. data/bootsnap-1.24.6/lib/bootsnap/bundler.rb +16 -0
  9. data/bootsnap-1.24.6/lib/bootsnap/cli/worker_pool.rb +208 -0
  10. data/bootsnap-1.24.6/lib/bootsnap/cli.rb +258 -0
  11. data/bootsnap-1.24.6/lib/bootsnap/compile_cache/iseq.rb +229 -0
  12. data/bootsnap-1.24.6/lib/bootsnap/compile_cache/ruby_bug_22023_canary.rb +1 -0
  13. data/bootsnap-1.24.6/lib/bootsnap/compile_cache/yaml.rb +344 -0
  14. data/bootsnap-1.24.6/lib/bootsnap/compile_cache.rb +47 -0
  15. data/bootsnap-1.24.6/lib/bootsnap/explicit_require.rb +56 -0
  16. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/cache.rb +241 -0
  17. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/change_observer.rb +84 -0
  18. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +42 -0
  19. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +19 -0
  20. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/loaded_features_index.rb +159 -0
  21. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/path.rb +143 -0
  22. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/path_scanner.rb +127 -0
  23. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/store.rb +132 -0
  24. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache.rb +80 -0
  25. data/bootsnap-1.24.6/lib/bootsnap/rake.rb +14 -0
  26. data/bootsnap-1.24.6/lib/bootsnap/setup.rb +5 -0
  27. data/bootsnap-1.24.6/lib/bootsnap/version.rb +5 -0
  28. data/bootsnap-1.24.6/lib/bootsnap.rb +202 -0
  29. data/mini-max-rb.gemspec +12 -0
  30. metadata +69 -0
@@ -0,0 +1,1235 @@
1
+ /*
2
+ * Suggested reading order:
3
+ * 1. Skim Init_bootsnap
4
+ * 2. Skim bs_fetch
5
+ * 3. The rest of everything
6
+ *
7
+ * Init_bootsnap sets up the ruby objects and binds bs_fetch to
8
+ * Bootsnap::CompileCache::Native.fetch.
9
+ *
10
+ * bs_fetch is the ultimate caller for for just about every other function in
11
+ * here.
12
+ */
13
+
14
+ #include "ruby.h"
15
+ #include <stdint.h>
16
+ #include <stdbool.h>
17
+ #include <sys/types.h>
18
+ #include <errno.h>
19
+ #include <fcntl.h>
20
+ #include <unistd.h>
21
+ #include <sys/stat.h>
22
+ #include <dirent.h>
23
+
24
+ #ifndef RBIMPL_ATTR_NORETURN
25
+ #define RBIMPL_ATTR_NORETURN()
26
+ #endif
27
+
28
+ #ifdef __APPLE__
29
+ // The symbol is present, however not in the headers
30
+ // See: https://github.com/rails/bootsnap/issues/470
31
+ extern int fdatasync(int);
32
+ #endif
33
+
34
+ #ifndef O_NOATIME
35
+ #define O_NOATIME 0
36
+ #endif
37
+
38
+ /* 1000 is an arbitrary limit; FNV64 plus some slashes brings the cap down to
39
+ * 981 for the cache dir */
40
+ #define MAX_CACHEPATH_SIZE 1000
41
+ #define MAX_CACHEDIR_SIZE 981
42
+
43
+ #define KEY_SIZE 32
44
+
45
+ #define MAX_CREATE_TEMPFILE_ATTEMPT 3
46
+
47
+ #ifndef RB_UNLIKELY
48
+ #define RB_UNLIKELY(x) (x)
49
+ #endif
50
+
51
+ /*
52
+ * An instance of this key is written as the first `KEY_SIZE` bytes of each cache file.
53
+ * The mtime and size members track whether the file contents have changed, and
54
+ * the ruby_version_digest (bootsnap_cache_version + RUBY_DESCRIPTION) and compile_option
55
+ * members track changes to the environment that could invalidate compile results without
56
+ * file contents having changed. The data_size member is not truly part of the
57
+ * "key". Really, this could be called a "header" with the first six members
58
+ * being an embedded "key" struct and an additional data_size member.
59
+ *
60
+ * The data_size indicates the remaining number of bytes in the cache file
61
+ * after the header (the size of the cached artifact).
62
+ */
63
+ struct bs_cache_key {
64
+ uint64_t ruby_version_digest;
65
+ uint64_t mtime;
66
+ uint64_t digest;
67
+ uint32_t size;
68
+ uint32_t data_size;
69
+ } __attribute__((packed));
70
+
71
+ /*
72
+ * If the struct padding isn't correct to pad the key to `KEY_SIZE` bytes, refuse to
73
+ * compile.
74
+ */
75
+ #define STATIC_ASSERT(X) STATIC_ASSERT2(X,__LINE__)
76
+ #define STATIC_ASSERT2(X,L) STATIC_ASSERT3(X,L)
77
+ #define STATIC_ASSERT3(X,L) STATIC_ASSERT_MSG(X,at_line_##L)
78
+ #define STATIC_ASSERT_MSG(COND,MSG) typedef char static_assertion_##MSG[(!!(COND))*2-1]
79
+ STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
80
+
81
+ /* Effectively a schema version. Bumping invalidates all previous caches */
82
+ static const uint32_t bootsnap_cache_version = 7;
83
+
84
+ /* Invalidates cache when switching ruby version, platform or ABI */
85
+ static uint64_t base_ruby_version_digest = 0;
86
+
87
+ // `base_ruby_version_digest` combined with RubyVM::InstructionSequence.compile_option
88
+ // Invalidates cache when RubyVM::InstructionSequence.compile_option changes
89
+ static uint64_t current_ruby_version_digest = 0;
90
+
91
+ /* Current umask */
92
+ static mode_t current_umask;
93
+
94
+ /* Bootsnap::CompileCache::{Native, Uncompilable} */
95
+ static VALUE rb_mBootsnap;
96
+ static VALUE rb_mBootsnap_CompileCache_Native;
97
+ static VALUE rb_cBootsnap_CompileCache_UNCOMPILABLE;
98
+ static ID instrumentation_method;
99
+ static VALUE sym_hit, sym_miss, sym_stale, sym_revalidated;
100
+ static bool instrumentation_enabled = false;
101
+ static bool readonly = false;
102
+ static bool revalidation = false;
103
+ static bool perm_issue = false;
104
+
105
+ static ID id_storage_to_output, id_input_to_output, id_input_to_storage;
106
+
107
+ /* Functions exposed as module functions on Bootsnap::CompileCache::Native */
108
+ static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
109
+ static VALUE bs_readonly_set(VALUE self, VALUE enabled);
110
+ static VALUE bs_revalidation_set(VALUE self, VALUE enabled);
111
+ static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
112
+ static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args);
113
+ static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler);
114
+
115
+ /* Helpers */
116
+ enum cache_status {
117
+ miss,
118
+ hit,
119
+ stale,
120
+ };
121
+ static void bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_path)[MAX_CACHEPATH_SIZE]);
122
+ static int bs_read_key(int fd, struct bs_cache_key * key);
123
+ static enum cache_status cache_key_equal_fast_path(struct bs_cache_key * k1, struct bs_cache_key * k2);
124
+ static int cache_key_equal_slow_path(struct bs_cache_key * current_key, struct bs_cache_key * cached_key, const VALUE input_data);
125
+ static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_key *old_key, int cache_fd, const char ** errno_provenance);
126
+
127
+ static void bs_cache_key_digest(struct bs_cache_key * key, const VALUE input_data);
128
+ static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args);
129
+ static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler);
130
+ static int open_current_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance);
131
+ static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance);
132
+ static uint64_t get_ruby_version_digest(void);
133
+
134
+ /*
135
+ * Helper functions to call ruby methods on handler object without crashing on
136
+ * exception.
137
+ */
138
+ static int bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data);
139
+ static VALUE prot_input_to_output(VALUE arg);
140
+ static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * output_data, int * exception_tag);
141
+ static int bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data);
142
+ struct s2o_data;
143
+ struct i2o_data;
144
+ struct i2s_data;
145
+
146
+ static VALUE
147
+ bs_rb_get_path(VALUE self, VALUE fname)
148
+ {
149
+ return rb_get_path(fname);
150
+ }
151
+
152
+ #ifdef HAVE_FSTATAT
153
+
154
+ RBIMPL_ATTR_NORETURN()
155
+ static void
156
+ bs_syserr_fail_path(const char *func_name, int n, VALUE path)
157
+ {
158
+ rb_syserr_fail_str(n, rb_sprintf("%s @ %s", func_name, RSTRING_PTR(path)));
159
+ }
160
+
161
+ RBIMPL_ATTR_NORETURN()
162
+ static void
163
+ bs_syserr_fail_dir_entry(const char *func_name, int n, VALUE dir, const char *d_name)
164
+ {
165
+ rb_syserr_fail_str(n, rb_sprintf("%s @ %s/%s", func_name, RSTRING_PTR(dir), d_name));
166
+ }
167
+
168
+ static VALUE
169
+ bs_rb_scan_dir(VALUE self, VALUE abspath)
170
+ {
171
+ Check_Type(abspath, T_STRING);
172
+
173
+ VALUE dirs = rb_ary_new();
174
+ VALUE requirables = rb_ary_new();
175
+ VALUE result = rb_ary_new_from_args(2, requirables, dirs);
176
+
177
+ DIR *dirp = opendir(RSTRING_PTR(abspath));
178
+ if (dirp == NULL) {
179
+ if (errno == ENOTDIR || errno == ENOENT) {
180
+ return result;
181
+ }
182
+
183
+ bs_syserr_fail_path("opendir", errno, abspath);
184
+ return Qundef;
185
+ }
186
+
187
+ struct dirent *entry;
188
+ struct stat st;
189
+ int dfd = -1;
190
+
191
+ while (1) {
192
+ errno = 0;
193
+
194
+ entry = readdir(dirp);
195
+ if (entry == NULL) break;
196
+
197
+ if (entry->d_name[0] == '.') continue;
198
+
199
+ if (RB_UNLIKELY(entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK)) {
200
+ // Note: the original implementation of LoadPathCache did follow symlink.
201
+ // So this is replicated here, but I'm not sure it's a good idea.
202
+ if (dfd < 0) {
203
+ dfd = dirfd(dirp);
204
+ if (dfd < 0) {
205
+ int err = errno;
206
+ closedir(dirp);
207
+ bs_syserr_fail_path("dirfd", err, abspath);
208
+ return Qundef;
209
+ }
210
+ }
211
+
212
+ if (fstatat(dfd, entry->d_name, &st, 0)) {
213
+ if (errno == ENOENT) continue; // Broken symlink
214
+
215
+ int err = errno;
216
+ closedir(dirp);
217
+ bs_syserr_fail_dir_entry("fstatat", err, abspath, entry->d_name);
218
+ return Qundef;
219
+ }
220
+
221
+ if (S_ISREG(st.st_mode)) {
222
+ entry->d_type = DT_REG;
223
+ } else if (S_ISDIR(st.st_mode)) {
224
+ entry->d_type = DT_DIR;
225
+ }
226
+ }
227
+
228
+ if (entry->d_type == DT_DIR) {
229
+ rb_ary_push(dirs, rb_utf8_str_new_cstr(entry->d_name));
230
+ continue;
231
+ } else if (entry->d_type == DT_REG) {
232
+ size_t len = strlen(entry->d_name);
233
+ bool is_requirable = (
234
+ // Comparing 4B allows compiler to optimize this into a single 32b integer comparison.
235
+ (len > 3 && memcmp(entry->d_name + (len - 3), ".rb", 4) == 0)
236
+ || (len > DLEXT_MAXLEN && memcmp(entry->d_name + (len - DLEXT_MAXLEN), DLEXT, DLEXT_MAXLEN + 1) == 0)
237
+ #ifdef DLEXT2
238
+ || (len > DLEXT2_MAXLEN && memcmp(entry->d_name + (len - DLEXT2_MAXLEN), DLEXT2, DLEXT2_MAXLEN + 1) == 0)
239
+ #endif
240
+ );
241
+ if (is_requirable) {
242
+ rb_ary_push(requirables, rb_utf8_str_new(entry->d_name, len));
243
+ }
244
+ }
245
+ }
246
+
247
+ if (errno) {
248
+ int err = errno;
249
+ closedir(dirp);
250
+ bs_syserr_fail_path("readdir", err, abspath);
251
+ } else if (closedir(dirp)) {
252
+ bs_syserr_fail_path("closedir", errno, abspath);
253
+ return Qundef;
254
+ }
255
+
256
+ return result;
257
+ }
258
+ #endif
259
+
260
+ /*
261
+ * Ruby C extensions are initialized by calling Init_<extname>.
262
+ *
263
+ * This sets up the module hierarchy and attaches functions as methods.
264
+ *
265
+ * We also populate some semi-static information about the current OS and so on.
266
+ */
267
+ void
268
+ Init_bootsnap(void)
269
+ {
270
+ rb_mBootsnap = rb_define_module("Bootsnap");
271
+
272
+ id_storage_to_output = rb_intern("storage_to_output");
273
+ id_input_to_output = rb_intern("input_to_output");
274
+ id_input_to_storage = rb_intern("input_to_storage");
275
+
276
+ rb_define_singleton_method(rb_mBootsnap, "rb_get_path", bs_rb_get_path, 1);
277
+
278
+ #ifdef HAVE_FSTATAT
279
+ VALUE rb_mBootsnap_LoadPathCache = rb_define_module_under(rb_mBootsnap, "LoadPathCache");
280
+ VALUE rb_mBootsnap_LoadPathCache_Native = rb_define_module_under(rb_mBootsnap_LoadPathCache, "Native");
281
+
282
+ rb_define_singleton_method(rb_mBootsnap_LoadPathCache_Native, "scan_dir", bs_rb_scan_dir, 1);
283
+ #endif
284
+
285
+ VALUE rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache");
286
+ rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native");
287
+ rb_cBootsnap_CompileCache_UNCOMPILABLE = rb_const_get(rb_mBootsnap_CompileCache, rb_intern("UNCOMPILABLE"));
288
+ rb_global_variable(&rb_cBootsnap_CompileCache_UNCOMPILABLE);
289
+
290
+ base_ruby_version_digest = get_ruby_version_digest();
291
+
292
+ instrumentation_method = rb_intern("_instrument");
293
+
294
+ sym_hit = ID2SYM(rb_intern("hit"));
295
+ sym_miss = ID2SYM(rb_intern("miss"));
296
+ sym_stale = ID2SYM(rb_intern("stale"));
297
+ sym_revalidated = ID2SYM(rb_intern("revalidated"));
298
+
299
+ rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
300
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1);
301
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "revalidation=", bs_revalidation_set, 1);
302
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 5);
303
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 4);
304
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
305
+
306
+ current_umask = umask(0777);
307
+ umask(current_umask);
308
+ }
309
+
310
+ static VALUE
311
+ bs_instrumentation_enabled_set(VALUE self, VALUE enabled)
312
+ {
313
+ instrumentation_enabled = RTEST(enabled);
314
+ return enabled;
315
+ }
316
+
317
+ static inline void
318
+ bs_instrumentation(VALUE event, VALUE path)
319
+ {
320
+ if (RB_UNLIKELY(instrumentation_enabled)) {
321
+ rb_funcall(rb_mBootsnap, instrumentation_method, 2, event, path);
322
+ }
323
+ }
324
+
325
+ static VALUE
326
+ bs_readonly_set(VALUE self, VALUE enabled)
327
+ {
328
+ readonly = RTEST(enabled);
329
+ return enabled;
330
+ }
331
+
332
+ static VALUE
333
+ bs_revalidation_set(VALUE self, VALUE enabled)
334
+ {
335
+ revalidation = RTEST(enabled);
336
+ return enabled;
337
+ }
338
+
339
+ static uint64_t
340
+ fnv1a_64_iter(uint64_t h, const unsigned char *s, size_t len)
341
+ {
342
+ const unsigned char *str_end = s + len;
343
+
344
+ while (s < str_end) {
345
+ h ^= (uint64_t)*s++;
346
+ h += (h << 1) + (h << 4) + (h << 5) + (h << 7) + (h << 8) + (h << 40);
347
+ }
348
+
349
+ return h;
350
+ }
351
+
352
+ static uint64_t
353
+ fnv1a_64_iter_str(uint64_t h, const VALUE str)
354
+ {
355
+ Check_Type(str, T_STRING);
356
+ return fnv1a_64_iter(h, (unsigned char *)RSTRING_PTR(str), RSTRING_LEN(str));
357
+ }
358
+
359
+ static uint64_t
360
+ fnv1a_64_str(const VALUE str)
361
+ {
362
+ uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
363
+ return fnv1a_64_iter_str(h, str);
364
+ }
365
+
366
+ /*
367
+ * Bootsnap's ruby code registers a hook that notifies us via this function
368
+ * when compile_option changes. These changes invalidate all existing caches.
369
+ *
370
+ * Note that on 32-bit platforms, a CRC32 can't be represented in a Fixnum, but
371
+ * can be represented by a uint.
372
+ */
373
+ static VALUE
374
+ bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
375
+ {
376
+ if (!RB_TYPE_P(crc32_v, T_BIGNUM) && !RB_TYPE_P(crc32_v, T_FIXNUM)) {
377
+ Check_Type(crc32_v, T_FIXNUM);
378
+ }
379
+ uint32_t crc32 = (uint32_t)NUM2UINT(crc32_v);
380
+ current_ruby_version_digest = fnv1a_64_iter(base_ruby_version_digest, (unsigned char *)&crc32, sizeof(crc32));
381
+ return Qnil;
382
+ }
383
+
384
+ static uint64_t
385
+ get_ruby_version_digest(void)
386
+ {
387
+ uint64_t hash = fnv1a_64_str(rb_const_get(rb_cObject, rb_intern("RUBY_DESCRIPTION")));
388
+ hash = fnv1a_64_iter(hash, (unsigned char *)&bootsnap_cache_version, sizeof(bootsnap_cache_version));
389
+ return hash;
390
+ }
391
+
392
+ /*
393
+ * Given a cache root directory and the full path to a file being cached,
394
+ * generate a path under the cache directory at which the cached artifact will
395
+ * be stored.
396
+ *
397
+ * The path will look something like: <cachedir>/12/34567890abcdef
398
+ */
399
+ static void
400
+ bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_path)[MAX_CACHEPATH_SIZE])
401
+ {
402
+ FilePathValue(path_v);
403
+
404
+ Check_Type(cachedir_v, T_STRING);
405
+ Check_Type(path_v, T_STRING);
406
+
407
+ long namespace_len = 0;
408
+ if (!NIL_P(namespace_v)) {
409
+ Check_Type(namespace_v, T_STRING);
410
+ namespace_len = RSTRING_LEN(namespace_v);
411
+ }
412
+
413
+ if (RSTRING_LEN(cachedir_v) + namespace_len > MAX_CACHEDIR_SIZE) {
414
+ rb_raise(rb_eArgError, "cachedir too long");
415
+ }
416
+
417
+ const char * cachedir = RSTRING_PTR(cachedir_v);
418
+ const char * namespace = NIL_P(namespace_v) ? "" : RSTRING_PTR(namespace_v);
419
+
420
+ uint64_t hash = fnv1a_64_str(path_v);
421
+ uint8_t first_byte = (hash >> (64 - 8));
422
+ uint64_t remainder = hash & 0x00ffffffffffffff;
423
+
424
+ sprintf(*cache_path, "%s%s/%02"PRIx8"/%014"PRIx64, cachedir, namespace, first_byte, remainder);
425
+ }
426
+
427
+ /*
428
+ * Test whether a newly-generated cache key based on the file as it exists on
429
+ * disk matches the one that was generated when the file was cached (or really
430
+ * compare any two keys).
431
+ *
432
+ * The data_size member is not compared, as it serves more of a "header"
433
+ * function.
434
+ */
435
+ static enum cache_status cache_key_equal_fast_path(struct bs_cache_key *k1,
436
+ struct bs_cache_key *k2) {
437
+ if (k1->ruby_version_digest == k2->ruby_version_digest &&
438
+ k1->size == k2->size) {
439
+ if (k1->mtime == k2->mtime) {
440
+ return hit;
441
+ }
442
+ if (revalidation) {
443
+ return stale;
444
+ }
445
+ }
446
+ return miss;
447
+ }
448
+
449
+ static int cache_key_equal_slow_path(struct bs_cache_key *current_key,
450
+ struct bs_cache_key *cached_key,
451
+ const VALUE input_data)
452
+ {
453
+ bs_cache_key_digest(current_key, input_data);
454
+ return current_key->digest == cached_key->digest;
455
+ }
456
+
457
+ static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_key *old_key, int cache_fd, const char ** errno_provenance)
458
+ {
459
+ old_key->mtime = current_key->mtime;
460
+ lseek(cache_fd, 0, SEEK_SET);
461
+ ssize_t nwrite = write(cache_fd, old_key, KEY_SIZE);
462
+ if (nwrite < 0) {
463
+ *errno_provenance = "update_cache_key:write";
464
+ return -1;
465
+ }
466
+
467
+ #ifdef HAVE_FDATASYNC
468
+ if (fdatasync(cache_fd) < 0) {
469
+ *errno_provenance = "update_cache_key:fdatasync";
470
+ return -1;
471
+ }
472
+ #endif
473
+
474
+ return 0;
475
+ }
476
+
477
+ /*
478
+ * Fills the cache key digest.
479
+ */
480
+ static void bs_cache_key_digest(struct bs_cache_key *key,
481
+ const VALUE input_data) {
482
+ if (key->digest)
483
+ return;
484
+ key->digest = fnv1a_64_str(input_data);
485
+ }
486
+
487
+ /*
488
+ * Entrypoint for Bootsnap::CompileCache::Native.fetch. The real work is done
489
+ * in bs_fetch; this function just performs some basic typechecks and
490
+ * conversions on the ruby VALUE arguments before passing them along.
491
+ */
492
+ static VALUE
493
+ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args)
494
+ {
495
+ char cache_path[MAX_CACHEPATH_SIZE];
496
+
497
+ /* generate cache path to cache_path */
498
+ bs_cache_path(cachedir_v, namespace_v, path_v, &cache_path);
499
+
500
+ return bs_fetch(RSTRING_PTR(path_v), path_v, cache_path, handler, args);
501
+ }
502
+
503
+ /*
504
+ * Entrypoint for Bootsnap::CompileCache::Native.precompile.
505
+ * Similar to fetch, but it only generate the cache if missing
506
+ * and doesn't return the content.
507
+ */
508
+ static VALUE
509
+ bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler)
510
+ {
511
+ char cache_path[MAX_CACHEPATH_SIZE];
512
+ /* generate cache path to cache_path */
513
+ bs_cache_path(cachedir_v, namespace_v, path_v, &cache_path);
514
+
515
+ return bs_precompile(RSTRING_PTR(path_v), path_v, cache_path, handler);
516
+ }
517
+
518
+ static int bs_open_noatime(const char *path, int flags) {
519
+ int fd = 1;
520
+ if (!perm_issue) {
521
+ fd = open(path, flags | O_NOATIME);
522
+ if (fd < 0 && errno == EPERM) {
523
+ errno = 0;
524
+ perm_issue = true;
525
+ }
526
+ }
527
+
528
+ if (perm_issue) {
529
+ fd = open(path, flags);
530
+ }
531
+ return fd;
532
+ }
533
+
534
+ /*
535
+ * Open the file we want to load/cache and generate a cache key for it if it
536
+ * was loaded.
537
+ */
538
+ static int
539
+ open_current_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance)
540
+ {
541
+ struct stat statbuf;
542
+ int fd;
543
+
544
+ fd = bs_open_noatime(path, O_RDONLY);
545
+ if (fd < 0) {
546
+ *errno_provenance = "bs_fetch:open_current_file:open";
547
+ return fd;
548
+ }
549
+ #ifdef _WIN32
550
+ setmode(fd, O_BINARY);
551
+ #endif
552
+
553
+ if (fstat(fd, &statbuf) < 0) {
554
+ *errno_provenance = "bs_fetch:open_current_file:fstat";
555
+ int previous_errno = errno;
556
+ close(fd);
557
+ errno = previous_errno;
558
+ return -1;
559
+ }
560
+
561
+ key->ruby_version_digest = current_ruby_version_digest;
562
+
563
+ // We're limited to file of 4GiB or less. Hopefully that's enough for everyone.
564
+ if (statbuf.st_size > (uint32_t)-1) {
565
+ *errno_provenance = "bs_fetch:open_current_file:file_too_big";
566
+ close(fd);
567
+ errno = EFBIG;
568
+ return -1;
569
+ }
570
+
571
+ key->size = (uint32_t)statbuf.st_size;
572
+ key->mtime = (uint64_t)statbuf.st_mtime;
573
+ key->digest = 0;
574
+
575
+ return fd;
576
+ }
577
+
578
+ #define ERROR_WITH_ERRNO -1
579
+ #define CACHE_MISS -2
580
+ #define CACHE_STALE -3
581
+ #define CACHE_UNCOMPILABLE -4
582
+
583
+ /*
584
+ * Read the cache key from the given fd, which must have position 0 (e.g.
585
+ * freshly opened file).
586
+ *
587
+ * Possible return values:
588
+ * - 0 (OK, key was loaded)
589
+ * - ERROR_WITH_ERRNO (-1, errno is set)
590
+ * - CACHE_MISS (-2)
591
+ * - CACHE_STALE (-3)
592
+ */
593
+ static int
594
+ bs_read_key(int fd, struct bs_cache_key * key)
595
+ {
596
+ ssize_t nread = read(fd, key, KEY_SIZE);
597
+ if (nread < 0) return ERROR_WITH_ERRNO;
598
+ if (nread < KEY_SIZE) return CACHE_STALE;
599
+ return 0;
600
+ }
601
+
602
+ /*
603
+ * Open the cache file at a given path, if it exists, and read its key into the
604
+ * struct.
605
+ *
606
+ * Possible return values:
607
+ * - 0 (OK, key was loaded)
608
+ * - CACHE_MISS (-2)
609
+ * - CACHE_STALE (-3)
610
+ * - ERROR_WITH_ERRNO (-1, errno is set)
611
+ */
612
+ static int
613
+ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance)
614
+ {
615
+ int fd, res;
616
+
617
+ if (readonly || !revalidation) {
618
+ fd = bs_open_noatime(path, O_RDONLY);
619
+ } else {
620
+ fd = bs_open_noatime(path, O_RDWR);
621
+ }
622
+
623
+ if (fd < 0) {
624
+ *errno_provenance = "bs_fetch:open_cache_file:open";
625
+ return CACHE_MISS;
626
+ }
627
+ #ifdef _WIN32
628
+ setmode(fd, O_BINARY);
629
+ #endif
630
+
631
+ res = bs_read_key(fd, key);
632
+ if (res < 0) {
633
+ *errno_provenance = "bs_fetch:open_cache_file:read";
634
+ close(fd);
635
+ return res;
636
+ }
637
+
638
+ return fd;
639
+ }
640
+
641
+ /*
642
+ * The cache file is laid out like:
643
+ * 0...`KEY_SIZE` : bs_cache_key
644
+ * `KEY_SIZE`..-1 : cached artifact
645
+ *
646
+ * This function takes a file descriptor whose position is pre-set to `KEY_SIZE`, and
647
+ * the data_size (corresponding to the remaining number of bytes) listed in the
648
+ * cache header.
649
+ *
650
+ * We load the text from this file into a buffer, and pass it to the ruby-land
651
+ * handler with exception handling via the exception_tag param.
652
+ *
653
+ * Data is returned via the output_data parameter, which, if there's no error
654
+ * or exception, will be the final data returnable to the user.
655
+ */
656
+ static int
657
+ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance)
658
+ {
659
+ ssize_t nread;
660
+ int ret;
661
+
662
+ VALUE storage_data;
663
+
664
+ if (data_size > 100000000000) {
665
+ *errno_provenance = "bs_fetch:fetch_cached_data:datasize";
666
+ errno = EINVAL; /* because wtf? */
667
+ ret = ERROR_WITH_ERRNO;
668
+ goto done;
669
+ }
670
+ storage_data = rb_str_buf_new(data_size);
671
+ nread = read(fd, RSTRING_PTR(storage_data), data_size);
672
+ if (nread < 0) {
673
+ *errno_provenance = "bs_fetch:fetch_cached_data:read";
674
+ ret = ERROR_WITH_ERRNO;
675
+ goto done;
676
+ }
677
+ if (nread != data_size) {
678
+ ret = CACHE_STALE;
679
+ goto done;
680
+ }
681
+
682
+ rb_str_set_len(storage_data, nread);
683
+
684
+ *exception_tag = bs_storage_to_output(handler, args, storage_data, output_data);
685
+ if (*output_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
686
+ ret = CACHE_UNCOMPILABLE;
687
+ goto done;
688
+ }
689
+ ret = 0;
690
+ done:
691
+ return ret;
692
+ }
693
+
694
+ /*
695
+ * Like mkdir -p, this recursively creates directory parents of a file. e.g.
696
+ * given /a/b/c, creates /a and /a/b.
697
+ */
698
+ static int
699
+ mkpath(char * file_path, mode_t mode)
700
+ {
701
+ /* It would likely be more efficient to count back until we
702
+ * find a component that *does* exist, but this will only run
703
+ * at most 256 times, so it seems not worthwhile to change. */
704
+ char * p;
705
+ for (p = strchr(file_path + 1, '/'); p; p = strchr(p + 1, '/')) {
706
+ *p = '\0';
707
+ #ifdef _WIN32
708
+ if (mkdir(file_path) == -1) {
709
+ #else
710
+ if (mkdir(file_path, mode) == -1) {
711
+ #endif
712
+ if (errno != EEXIST) {
713
+ *p = '/';
714
+ return -1;
715
+ }
716
+ }
717
+ *p = '/';
718
+ }
719
+ return 0;
720
+ }
721
+
722
+ /*
723
+ * Write a cache header/key and a compiled artifact to a given cache path by
724
+ * writing to a tmpfile and then renaming the tmpfile over top of the final
725
+ * path.
726
+ */
727
+ static int
728
+ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, const char ** errno_provenance)
729
+ {
730
+ char template[MAX_CACHEPATH_SIZE + 20];
731
+ char * tmp_path;
732
+ int fd, ret, attempt;
733
+ ssize_t nwrite;
734
+
735
+ for (attempt = 0; attempt < MAX_CREATE_TEMPFILE_ATTEMPT; ++attempt) {
736
+ tmp_path = strncpy(template, path, MAX_CACHEPATH_SIZE);
737
+ strcat(tmp_path, ".tmp.XXXXXX");
738
+
739
+ // mkstemp modifies the template to be the actual created path
740
+ fd = mkstemp(tmp_path);
741
+ if (fd > 0) break;
742
+
743
+ if (attempt == 0 && mkpath(tmp_path, 0775) < 0) {
744
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:mkpath";
745
+ return -1;
746
+ }
747
+ }
748
+ if (fd < 0) {
749
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:mkstemp";
750
+ return -1;
751
+ }
752
+
753
+ if (chmod(tmp_path, 0644) < 0) {
754
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:chmod";
755
+ return -1;
756
+ }
757
+
758
+ #ifdef _WIN32
759
+ setmode(fd, O_BINARY);
760
+ #endif
761
+
762
+ uint64_t data_size = RSTRING_LEN(data);
763
+ if (data_size > (uint32_t)-1) {
764
+ return 0; // Don't cache.
765
+ }
766
+
767
+ key->data_size = (uint32_t)data_size;
768
+ nwrite = write(fd, key, KEY_SIZE);
769
+ if (nwrite < 0) {
770
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:write";
771
+ return -1;
772
+ }
773
+ if (nwrite != KEY_SIZE) {
774
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:keysize";
775
+ errno = EIO; /* Lies but whatever */
776
+ return -1;
777
+ }
778
+
779
+ nwrite = write(fd, RSTRING_PTR(data), RSTRING_LEN(data));
780
+ if (nwrite < 0) return -1;
781
+ if (nwrite != RSTRING_LEN(data)) {
782
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:writelength";
783
+ errno = EIO; /* Lies but whatever */
784
+ return -1;
785
+ }
786
+
787
+ close(fd);
788
+ ret = rename(tmp_path, path);
789
+ if (ret < 0) {
790
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:rename";
791
+ return -1;
792
+ }
793
+ ret = chmod(path, 0664 & ~current_umask);
794
+ if (ret < 0) {
795
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:chmod";
796
+ }
797
+ return ret;
798
+ }
799
+
800
+
801
+ /* Read contents from an fd, whose contents are asserted to be +size+ bytes
802
+ * long, returning a Ruby string on success and Qfalse on failure */
803
+ static VALUE
804
+ bs_read_contents(int fd, size_t size, const char ** errno_provenance)
805
+ {
806
+ VALUE contents;
807
+ ssize_t nread;
808
+ contents = rb_str_buf_new(size);
809
+ nread = read(fd, RSTRING_PTR(contents), size);
810
+
811
+ if (nread < 0) {
812
+ *errno_provenance = "bs_fetch:bs_read_contents:read";
813
+ return Qfalse;
814
+ } else {
815
+ rb_str_set_len(contents, nread);
816
+ return contents;
817
+ }
818
+ }
819
+
820
+ /*
821
+ * This is the meat of the extension. bs_fetch is
822
+ * Bootsnap::CompileCache::Native.fetch.
823
+ *
824
+ * There are three "formats" in use here:
825
+ * 1. "input" format, which is what we load from the source file;
826
+ * 2. "storage" format, which we write to the cache;
827
+ * 3. "output" format, which is what we return.
828
+ *
829
+ * E.g., For ISeq compilation:
830
+ * input: ruby source, as text
831
+ * storage: binary string (RubyVM::InstructionSequence#to_binary)
832
+ * output: Instance of RubyVM::InstructionSequence
833
+ *
834
+ * And for YAML:
835
+ * input: yaml as text
836
+ * storage: MessagePack or Marshal text
837
+ * output: ruby object, loaded from yaml/messagepack/marshal
838
+ *
839
+ * A handler<I,S,O> passed in must support three messages:
840
+ * * storage_to_output(S) -> O
841
+ * * input_to_output(I) -> O
842
+ * * input_to_storage(I) -> S
843
+ * (input_to_storage may raise Bootsnap::CompileCache::Uncompilable, which
844
+ * will prevent caching and cause output to be generated with
845
+ * input_to_output)
846
+ *
847
+ * The semantics of this function are basically:
848
+ *
849
+ * return storage_to_output(cache[path]) if cache[path]
850
+ * storage = input_to_storage(input)
851
+ * cache[path] = storage
852
+ * return storage_to_output(storage)
853
+ *
854
+ * Or expanded a bit:
855
+ *
856
+ * - Check if the cache file exists and is up to date.
857
+ * - If it is, load this data to storage_data.
858
+ * - return storage_to_output(storage_data)
859
+ * - Read the file to input_data
860
+ * - Generate storage_data using input_to_storage(input_data)
861
+ * - Write storage_data data, with a cache key, to the cache file.
862
+ * - Return storage_to_output(storage_data)
863
+ */
864
+ static VALUE
865
+ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args)
866
+ {
867
+ struct bs_cache_key cached_key, current_key;
868
+ int cache_fd = -1, current_fd = -1;
869
+ int res, valid_cache = 0, exception_tag = 0;
870
+ const char * errno_provenance = NULL;
871
+
872
+ VALUE status = Qfalse;
873
+ VALUE input_data = Qfalse; /* data read from source file, e.g. YAML or ruby source */
874
+ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
875
+ VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */
876
+
877
+ VALUE exception; /* ruby exception object to raise instead of returning */
878
+ VALUE exception_message; /* ruby exception string to use instead of errno_provenance */
879
+
880
+ /* Open the source file and generate a cache key for it */
881
+ current_fd = open_current_file(path, &current_key, &errno_provenance);
882
+ if (current_fd < 0) {
883
+ exception_message = path_v;
884
+ goto fail_errno;
885
+ }
886
+
887
+ /* Open the cache key if it exists, and read its cache key in */
888
+ cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
889
+ if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) {
890
+ /* This is ok: valid_cache remains false, we re-populate it. */
891
+ bs_instrumentation(cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v);
892
+ } else if (cache_fd < 0) {
893
+ exception_message = rb_str_new_cstr(cache_path);
894
+ goto fail_errno;
895
+ } else {
896
+ /* True if the cache existed and no invalidating changes have occurred since
897
+ * it was generated. */
898
+
899
+ switch(cache_key_equal_fast_path(&current_key, &cached_key)) {
900
+ case hit:
901
+ status = sym_hit;
902
+ valid_cache = true;
903
+ break;
904
+ case miss:
905
+ valid_cache = false;
906
+ break;
907
+ case stale:
908
+ valid_cache = false;
909
+ if ((input_data = bs_read_contents(current_fd, current_key.size,
910
+ &errno_provenance)) == Qfalse) {
911
+ exception_message = path_v;
912
+ goto fail_errno;
913
+ }
914
+ valid_cache = cache_key_equal_slow_path(&current_key, &cached_key, input_data);
915
+ if (valid_cache) {
916
+ if (!readonly) {
917
+ if (update_cache_key(&current_key, &cached_key, cache_fd, &errno_provenance)) {
918
+ exception_message = path_v;
919
+ goto fail_errno;
920
+ }
921
+ }
922
+ status = sym_revalidated;
923
+ }
924
+ break;
925
+ };
926
+
927
+ if (!valid_cache) {
928
+ status = sym_stale;
929
+ }
930
+ }
931
+
932
+ if (valid_cache) {
933
+ /* Fetch the cache data and return it if we're able to load it successfully */
934
+ res = fetch_cached_data(
935
+ cache_fd, (ssize_t)cached_key.data_size, handler, args,
936
+ &output_data, &exception_tag, &errno_provenance
937
+ );
938
+ if (exception_tag != 0) goto raise;
939
+ else if (res == CACHE_UNCOMPILABLE) {
940
+ /* If fetch_cached_data returned `Uncompilable` we fallback to `input_to_output`
941
+ This happens if we have say, an unsafe YAML cache, but try to load it in safe mode */
942
+ if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
943
+ exception_message = path_v;
944
+ goto fail_errno;
945
+ }
946
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
947
+ if (exception_tag != 0) goto raise;
948
+ goto succeed;
949
+ } else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0;
950
+ else if (res == ERROR_WITH_ERRNO){
951
+ exception_message = rb_str_new_cstr(cache_path);
952
+ goto fail_errno;
953
+ }
954
+ else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
955
+ }
956
+ close(cache_fd);
957
+ cache_fd = -1;
958
+ /* Cache is stale, invalid, or missing. Regenerate and write it out. */
959
+
960
+ /* Read the contents of the source file into a buffer */
961
+ if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
962
+ exception_message = path_v;
963
+ goto fail_errno;
964
+ }
965
+
966
+ /* Try to compile the input_data using input_to_storage(input_data) */
967
+ exception_tag = bs_input_to_storage(handler, args, input_data, path_v, &storage_data);
968
+ if (exception_tag != 0) goto raise;
969
+ /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
970
+ * to cache anything; just return input_to_output(input_data) */
971
+ if (storage_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
972
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
973
+ if (exception_tag != 0) goto raise;
974
+ goto succeed;
975
+ }
976
+ /* If storage_data isn't a string, we can't cache it */
977
+ if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data;
978
+
979
+ /* Attempt to write the cache key and storage_data to the cache directory.
980
+ * We do however ignore any failures to persist the cache, as it's better
981
+ * to move along, than to interrupt the process.
982
+ */
983
+ bs_cache_key_digest(&current_key, input_data);
984
+ atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
985
+
986
+ /* Having written the cache, now convert storage_data to output_data */
987
+ exception_tag = bs_storage_to_output(handler, args, storage_data, &output_data);
988
+ if (exception_tag != 0) goto raise;
989
+
990
+ if (output_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
991
+ /* If storage_to_output returned `Uncompilable` we fallback to `input_to_output` */
992
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
993
+ if (exception_tag != 0) goto raise;
994
+ } else if (NIL_P(output_data)) {
995
+ /* If output_data is nil, delete the cache entry and generate the output
996
+ * using input_to_output */
997
+ if (unlink(cache_path) < 0) {
998
+ /* If the cache was already deleted, it might be that another process did it before us.
999
+ * No point raising an error */
1000
+ if (errno != ENOENT) {
1001
+ errno_provenance = "bs_fetch:unlink";
1002
+ exception_message = rb_str_new_cstr(cache_path);
1003
+ goto fail_errno;
1004
+ }
1005
+ }
1006
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
1007
+ if (exception_tag != 0) goto raise;
1008
+ }
1009
+
1010
+ goto succeed; /* output_data is now the correct return. */
1011
+
1012
+ #define CLEANUP \
1013
+ if (current_fd >= 0) close(current_fd); \
1014
+ if (cache_fd >= 0) close(cache_fd); \
1015
+ if (status != Qfalse) bs_instrumentation(status, path_v);
1016
+
1017
+ succeed:
1018
+ CLEANUP;
1019
+ return output_data;
1020
+ fail_errno:
1021
+ CLEANUP;
1022
+ if (errno_provenance) {
1023
+ exception_message = rb_str_concat(
1024
+ rb_str_new_cstr(errno_provenance),
1025
+ rb_str_concat(rb_str_new_cstr(": "), exception_message)
1026
+ );
1027
+ }
1028
+ exception = rb_syserr_new_str(errno, exception_message);
1029
+ rb_exc_raise(exception);
1030
+ __builtin_unreachable();
1031
+ raise:
1032
+ CLEANUP;
1033
+ rb_jump_tag(exception_tag);
1034
+ __builtin_unreachable();
1035
+ invalid_type_storage_data:
1036
+ CLEANUP;
1037
+ Check_Type(storage_data, T_STRING);
1038
+ __builtin_unreachable();
1039
+
1040
+ #undef CLEANUP
1041
+ }
1042
+
1043
+ static VALUE
1044
+ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
1045
+ {
1046
+ if (readonly) {
1047
+ return Qfalse;
1048
+ }
1049
+
1050
+ struct bs_cache_key cached_key, current_key;
1051
+ int cache_fd = -1, current_fd = -1;
1052
+ int res, valid_cache = 0, exception_tag = 0;
1053
+ const char * errno_provenance = NULL;
1054
+
1055
+ VALUE input_data = Qfalse; /* data read from source file, e.g. YAML or ruby source */
1056
+ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
1057
+
1058
+ /* Open the source file and generate a cache key for it */
1059
+ current_fd = open_current_file(path, &current_key, &errno_provenance);
1060
+ if (current_fd < 0) goto fail;
1061
+
1062
+ /* Open the cache key if it exists, and read its cache key in */
1063
+ cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
1064
+ if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) {
1065
+ /* This is ok: valid_cache remains false, we re-populate it. */
1066
+ } else if (cache_fd < 0) {
1067
+ goto fail;
1068
+ } else {
1069
+ /* True if the cache existed and no invalidating changes have occurred since
1070
+ * it was generated. */
1071
+ switch(cache_key_equal_fast_path(&current_key, &cached_key)) {
1072
+ case hit:
1073
+ valid_cache = true;
1074
+ break;
1075
+ case miss:
1076
+ valid_cache = false;
1077
+ break;
1078
+ case stale:
1079
+ valid_cache = false;
1080
+ if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
1081
+ goto fail;
1082
+ }
1083
+ valid_cache = cache_key_equal_slow_path(&current_key, &cached_key, input_data);
1084
+ if (valid_cache) {
1085
+ if (update_cache_key(&current_key, &cached_key, cache_fd, &errno_provenance)) {
1086
+ goto fail;
1087
+ }
1088
+ }
1089
+ break;
1090
+ };
1091
+ }
1092
+
1093
+ if (valid_cache) {
1094
+ goto succeed;
1095
+ }
1096
+
1097
+ close(cache_fd);
1098
+ cache_fd = -1;
1099
+ /* Cache is stale, invalid, or missing. Regenerate and write it out. */
1100
+
1101
+ /* Read the contents of the source file into a buffer */
1102
+ if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) goto fail;
1103
+
1104
+ /* Try to compile the input_data using input_to_storage(input_data) */
1105
+ exception_tag = bs_input_to_storage(handler, Qnil, input_data, path_v, &storage_data);
1106
+ if (exception_tag != 0) goto fail;
1107
+
1108
+ /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
1109
+ * to cache anything; just return false */
1110
+ if (storage_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
1111
+ goto fail;
1112
+ }
1113
+ /* If storage_data isn't a string, we can't cache it */
1114
+ if (!RB_TYPE_P(storage_data, T_STRING)) goto fail;
1115
+
1116
+ /* Write the cache key and storage_data to the cache directory */
1117
+ bs_cache_key_digest(&current_key, input_data);
1118
+ res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
1119
+ if (res < 0) goto fail;
1120
+
1121
+ goto succeed;
1122
+
1123
+ #define CLEANUP \
1124
+ if (current_fd >= 0) close(current_fd); \
1125
+ if (cache_fd >= 0) close(cache_fd);
1126
+
1127
+ succeed:
1128
+ CLEANUP;
1129
+ return Qtrue;
1130
+ fail:
1131
+ CLEANUP;
1132
+ return Qfalse;
1133
+ #undef CLEANUP
1134
+ }
1135
+
1136
+
1137
+ /*****************************************************************************/
1138
+ /********************* Handler Wrappers **************************************/
1139
+ /*****************************************************************************
1140
+ * Everything after this point in the file is just wrappers to deal with ruby's
1141
+ * clunky method of handling exceptions from ruby methods invoked from C:
1142
+ *
1143
+ * In order to call a ruby method from C, while protecting against crashing in
1144
+ * the event of an exception, we must call the method with rb_protect().
1145
+ *
1146
+ * rb_protect takes a C function and precisely one argument; however, we want
1147
+ * to pass multiple arguments, so we must create structs to wrap them up.
1148
+ *
1149
+ * These functions return an exception_tag, which, if non-zero, indicates an
1150
+ * exception that should be jumped to with rb_jump_tag after cleaning up
1151
+ * allocated resources.
1152
+ */
1153
+
1154
+ struct s2o_data {
1155
+ VALUE handler;
1156
+ VALUE args;
1157
+ VALUE storage_data;
1158
+ };
1159
+
1160
+ struct i2o_data {
1161
+ VALUE handler;
1162
+ VALUE args;
1163
+ VALUE input_data;
1164
+ VALUE pathval;
1165
+ };
1166
+
1167
+ struct i2s_data {
1168
+ VALUE handler;
1169
+ VALUE input_data;
1170
+ VALUE pathval;
1171
+ };
1172
+
1173
+ static VALUE
1174
+ try_storage_to_output(VALUE arg)
1175
+ {
1176
+ struct s2o_data * data = (struct s2o_data *)arg;
1177
+ return rb_funcall(data->handler, id_storage_to_output, 2, data->storage_data, data->args);
1178
+ }
1179
+
1180
+ static int
1181
+ bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data)
1182
+ {
1183
+ int state;
1184
+ struct s2o_data s2o_data = {
1185
+ .handler = handler,
1186
+ .args = args,
1187
+ .storage_data = storage_data,
1188
+ };
1189
+ *output_data = rb_protect(try_storage_to_output, (VALUE)&s2o_data, &state);
1190
+ return state;
1191
+ }
1192
+
1193
+ static void
1194
+ bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE path_v, VALUE * output_data, int * exception_tag)
1195
+ {
1196
+ struct i2o_data i2o_data = {
1197
+ .handler = handler,
1198
+ .args = args,
1199
+ .input_data = input_data,
1200
+ .pathval = path_v,
1201
+ };
1202
+ *output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag);
1203
+ }
1204
+
1205
+ static VALUE
1206
+ prot_input_to_output(VALUE arg)
1207
+ {
1208
+ struct i2o_data * data = (struct i2o_data *)arg;
1209
+ return rb_funcall(data->handler, id_input_to_output, 3, data->input_data, data->pathval, data->args);
1210
+ }
1211
+
1212
+ static VALUE
1213
+ try_input_to_storage(VALUE arg)
1214
+ {
1215
+ struct i2s_data * data = (struct i2s_data *)arg;
1216
+ return rb_funcall(data->handler, id_input_to_storage, 2, data->input_data, data->pathval);
1217
+ }
1218
+
1219
+ static int
1220
+ bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data)
1221
+ {
1222
+ if (readonly) {
1223
+ *storage_data = rb_cBootsnap_CompileCache_UNCOMPILABLE;
1224
+ return 0;
1225
+ } else {
1226
+ int state;
1227
+ struct i2s_data i2s_data = {
1228
+ .handler = handler,
1229
+ .input_data = input_data,
1230
+ .pathval = pathval,
1231
+ };
1232
+ *storage_data = rb_protect(try_input_to_storage, (VALUE)&i2s_data, &state);
1233
+ return state;
1234
+ }
1235
+ }