archive_r_ruby 0.1.3 → 0.1.4

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/{LICENSE → LICENSE.txt} +77 -77
  3. data/README.md +103 -103
  4. data/ext/archive_r/Makefile +48 -45
  5. data/ext/archive_r/archive_r-x64-mingw-ucrt.def +2 -0
  6. data/ext/archive_r/archive_r_ext.cc +1106 -1106
  7. data/ext/archive_r/archive_r_ext.o +0 -0
  8. data/ext/archive_r/extconf.rb +120 -120
  9. data/ext/archive_r/mkmf.log +23 -18
  10. data/ext/archive_r/vendor/archive_r/LICENSE.txt +77 -77
  11. data/ext/archive_r/vendor/archive_r/include/archive_r/data_stream.h +52 -52
  12. data/ext/archive_r/vendor/archive_r/include/archive_r/entry.h +166 -166
  13. data/ext/archive_r/vendor/archive_r/include/archive_r/entry_fault.h +34 -34
  14. data/ext/archive_r/vendor/archive_r/include/archive_r/entry_metadata.h +56 -56
  15. data/ext/archive_r/vendor/archive_r/include/archive_r/multi_volume_stream_base.h +46 -46
  16. data/ext/archive_r/vendor/archive_r/include/archive_r/path_hierarchy.h +109 -109
  17. data/ext/archive_r/vendor/archive_r/include/archive_r/path_hierarchy_utils.h +37 -37
  18. data/ext/archive_r/vendor/archive_r/include/archive_r/platform_compat.h +19 -19
  19. data/ext/archive_r/vendor/archive_r/include/archive_r/traverser.h +122 -122
  20. data/ext/archive_r/vendor/archive_r/src/archive_stack_cursor.cc +330 -330
  21. data/ext/archive_r/vendor/archive_r/src/archive_stack_cursor.h +97 -97
  22. data/ext/archive_r/vendor/archive_r/src/archive_stack_orchestrator.cc +162 -162
  23. data/ext/archive_r/vendor/archive_r/src/archive_stack_orchestrator.h +54 -54
  24. data/ext/archive_r/vendor/archive_r/src/archive_type.cc +552 -552
  25. data/ext/archive_r/vendor/archive_r/src/archive_type.h +77 -77
  26. data/ext/archive_r/vendor/archive_r/src/data_stream.cc +35 -35
  27. data/ext/archive_r/vendor/archive_r/src/entry.cc +253 -253
  28. data/ext/archive_r/vendor/archive_r/src/entry_fault.cc +26 -26
  29. data/ext/archive_r/vendor/archive_r/src/entry_fault_error.cc +54 -54
  30. data/ext/archive_r/vendor/archive_r/src/entry_fault_error.h +32 -32
  31. data/ext/archive_r/vendor/archive_r/src/entry_impl.h +57 -57
  32. data/ext/archive_r/vendor/archive_r/src/multi_volume_manager.cc +81 -81
  33. data/ext/archive_r/vendor/archive_r/src/multi_volume_manager.h +41 -41
  34. data/ext/archive_r/vendor/archive_r/src/multi_volume_stream_base.cc +199 -199
  35. data/ext/archive_r/vendor/archive_r/src/path_hierarchy.cc +151 -151
  36. data/ext/archive_r/vendor/archive_r/src/path_hierarchy_utils.cc +304 -304
  37. data/ext/archive_r/vendor/archive_r/src/simple_profiler.h +120 -120
  38. data/ext/archive_r/vendor/archive_r/src/system_file_stream.cc +295 -295
  39. data/ext/archive_r/vendor/archive_r/src/system_file_stream.h +46 -46
  40. data/ext/archive_r/vendor/archive_r/src/traverser.cc +314 -314
  41. data/lib/archive_r.rb +105 -105
  42. metadata +11 -8
  43. data/ext/archive_r/archive_r.bundle +0 -0
@@ -1,1106 +1,1106 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2025 archive_r Team
3
-
4
- #include "archive_r/data_stream.h"
5
- #include "archive_r/entry.h"
6
- #include "archive_r/entry_fault.h"
7
- #include "archive_r/multi_volume_stream_base.h"
8
- #include "archive_r/path_hierarchy_utils.h"
9
- #include "archive_r/traverser.h"
10
- #include <cctype>
11
- #include <cstdio>
12
- #include <cstring>
13
- #include <memory>
14
- #include <ruby.h>
15
- #include <stdexcept>
16
- #include <string>
17
- #include <utility>
18
- #include <variant>
19
- #include <vector>
20
- #include <limits>
21
- #include <optional>
22
-
23
- using namespace archive_r;
24
-
25
- // Ruby module and class references
26
- static VALUE mArchive_r;
27
- static VALUE cTraverser;
28
- static VALUE cEntry;
29
- static ID rb_id_read_method;
30
- static ID rb_id_seek_method;
31
- static ID rb_id_tell_method;
32
- static ID rb_id_eof_method;
33
- static ID rb_id_close_method;
34
- static ID rb_id_size_method;
35
- static ID rb_id_call_method;
36
- static ID rb_id_open_part_method;
37
- static ID rb_id_close_part_method;
38
- static ID rb_id_read_part_method;
39
- static ID rb_id_seek_part_method;
40
- static ID rb_id_part_size_method;
41
- static ID rb_id_open_part_io_method;
42
- static ID rb_id_close_part_io_method;
43
- struct RubyCallbackHolder;
44
- static std::shared_ptr<RubyCallbackHolder> g_stream_factory_callback;
45
-
46
- // Helper: Convert Ruby string to C++ string
47
- static VALUE cStream;
48
- static PathHierarchy rb_value_to_path_hierarchy(VALUE value);
49
- static std::string rb_string_to_cpp(VALUE rb_str) {
50
- Check_Type(rb_str, T_STRING);
51
- return std::string(RSTRING_PTR(rb_str), RSTRING_LEN(rb_str));
52
- }
53
-
54
- static void archive_r_cleanup(VALUE) {
55
- register_fault_callback(FaultCallback{});
56
- set_root_stream_factory(RootStreamFactory{});
57
- g_stream_factory_callback.reset();
58
- }
59
-
60
- // Helper: Convert C++ string to Ruby string
61
- static VALUE cpp_string_to_rb(const std::string &str) { return rb_str_new(str.c_str(), str.length()); }
62
-
63
- static VALUE path_entry_to_rb(const PathEntry &entry) {
64
- if (entry.is_single()) {
65
- return cpp_string_to_rb(entry.single_value());
66
- }
67
- if (entry.is_multi_volume()) {
68
- const auto &parts = entry.multi_volume_parts().values;
69
- VALUE array = rb_ary_new_capa(parts.size());
70
- for (const auto &part : parts) {
71
- rb_ary_push(array, cpp_string_to_rb(part));
72
- }
73
- return array;
74
- }
75
- VALUE array = rb_ary_new_capa(entry.nested_nodes().size());
76
- for (const auto &child : entry.nested_nodes()) {
77
- rb_ary_push(array, path_entry_to_rb(child));
78
- }
79
- return array;
80
- }
81
-
82
- static VALUE path_hierarchy_to_rb(const PathHierarchy &hierarchy) {
83
- VALUE array = rb_ary_new_capa(hierarchy.size());
84
- for (const auto &component : hierarchy) {
85
- rb_ary_push(array, path_entry_to_rb(component));
86
- }
87
- return array;
88
- }
89
-
90
- struct RubyCallbackHolder {
91
- explicit RubyCallbackHolder(VALUE proc)
92
- : proc_value(proc) {
93
- rb_gc_register_address(&proc_value);
94
- }
95
-
96
- ~RubyCallbackHolder() { rb_gc_unregister_address(&proc_value); }
97
-
98
- VALUE proc_value;
99
- };
100
-
101
- class RubyUserStream : public MultiVolumeStreamBase {
102
- public:
103
- RubyUserStream(VALUE ruby_stream, PathHierarchy hierarchy, std::optional<bool> seekable_override)
104
- : MultiVolumeStreamBase(std::move(hierarchy), determine_seek_support(ruby_stream, seekable_override))
105
- , _ruby_stream(ruby_stream)
106
- , _has_close(rb_respond_to(ruby_stream, rb_id_close_part_method))
107
- , _has_seek(rb_respond_to(ruby_stream, rb_id_seek_part_method))
108
- , _has_size(rb_respond_to(ruby_stream, rb_id_part_size_method))
109
- , _has_open_part_io(rb_respond_to(ruby_stream, rb_id_open_part_io_method))
110
- , _has_close_part_io(rb_respond_to(ruby_stream, rb_id_close_part_io_method))
111
- , _active_io(Qnil)
112
- , _active_io_seekable(false)
113
- , _active_io_tellable(false)
114
- , _active_io_has_close(false)
115
- , _active_io_has_size(false) {
116
- ensure_required_methods(ruby_stream);
117
- rb_gc_register_address(&_ruby_stream);
118
- }
119
-
120
- ~RubyUserStream() override {
121
- release_active_io();
122
- rb_gc_unregister_address(&_ruby_stream);
123
- }
124
-
125
- protected:
126
- void open_single_part(const PathHierarchy &single_part) override {
127
- if (_has_open_part_io) {
128
- VALUE arg = path_hierarchy_to_rb(single_part);
129
- VALUE io = rb_funcall(_ruby_stream, rb_id_open_part_io_method, 1, arg);
130
- if (NIL_P(io)) {
131
- rb_raise(rb_eRuntimeError, "open_part_io must return an IO-like object");
132
- }
133
- activate_io(io);
134
- return;
135
- }
136
-
137
- VALUE arg = path_hierarchy_to_rb(single_part);
138
- rb_funcall(_ruby_stream, rb_id_open_part_method, 1, arg);
139
- }
140
-
141
- void close_single_part() override {
142
- if (_has_open_part_io) {
143
- release_active_io();
144
- if (_has_close_part_io) {
145
- rb_funcall(_ruby_stream, rb_id_close_part_io_method, 0);
146
- }
147
- return;
148
- }
149
-
150
- if (_has_close) {
151
- rb_funcall(_ruby_stream, rb_id_close_part_method, 0);
152
- }
153
- }
154
-
155
- ssize_t read_from_single_part(void *buffer, size_t size) override {
156
- if (size == 0) {
157
- return 0;
158
- }
159
-
160
- if (_has_open_part_io) {
161
- if (_active_io == Qnil) {
162
- rb_raise(rb_eRuntimeError, "open_part_io must return an IO before reading");
163
- }
164
- VALUE result = rb_funcall(_active_io, rb_id_read_method, 1, SIZET2NUM(size));
165
- if (NIL_P(result)) {
166
- return 0;
167
- }
168
- Check_Type(result, T_STRING);
169
- const ssize_t length = static_cast<ssize_t>(RSTRING_LEN(result));
170
- if (length <= 0) {
171
- return 0;
172
- }
173
- std::memcpy(buffer, RSTRING_PTR(result), static_cast<size_t>(length));
174
- return length;
175
- }
176
-
177
- VALUE result = rb_funcall(_ruby_stream, rb_id_read_part_method, 1, SIZET2NUM(size));
178
- if (NIL_P(result)) {
179
- return 0;
180
- }
181
- Check_Type(result, T_STRING);
182
- const ssize_t length = static_cast<ssize_t>(RSTRING_LEN(result));
183
- if (length <= 0) {
184
- return 0;
185
- }
186
- std::memcpy(buffer, RSTRING_PTR(result), static_cast<size_t>(length));
187
- return length;
188
- }
189
-
190
- int64_t seek_within_single_part(int64_t offset, int whence) override {
191
- if (_has_open_part_io && _active_io != Qnil && _active_io_seekable) {
192
- VALUE result = rb_funcall(_active_io, rb_id_seek_method, 2, LL2NUM(offset), INT2NUM(whence));
193
- return NUM2LL(result);
194
- }
195
-
196
- if (!_has_seek) {
197
- return -1;
198
- }
199
- VALUE result = rb_funcall(_ruby_stream, rb_id_seek_part_method, 2, LL2NUM(offset), INT2NUM(whence));
200
- return NUM2LL(result);
201
- }
202
-
203
- int64_t size_of_single_part(const PathHierarchy &single_part) override {
204
- if (_has_open_part_io && _active_io != Qnil) {
205
- if (_active_io_has_size) {
206
- VALUE result = rb_funcall(_active_io, rb_id_size_method, 0);
207
- return NUM2LL(result);
208
- }
209
- if (_active_io_seekable && _active_io_tellable) {
210
- VALUE current = rb_funcall(_active_io, rb_id_tell_method, 0);
211
- rb_funcall(_active_io, rb_id_seek_method, 2, LL2NUM(0), INT2NUM(SEEK_END));
212
- VALUE end_pos = rb_funcall(_active_io, rb_id_tell_method, 0);
213
- rb_funcall(_active_io, rb_id_seek_method, 2, current, INT2NUM(SEEK_SET));
214
- return NUM2LL(end_pos);
215
- }
216
- }
217
-
218
- if (!_has_size) {
219
- return -1;
220
- }
221
- VALUE arg = path_hierarchy_to_rb(single_part);
222
- VALUE result = rb_funcall(_ruby_stream, rb_id_part_size_method, 1, arg);
223
- return NUM2LL(result);
224
- }
225
-
226
- private:
227
- static bool determine_seek_support(VALUE ruby_stream, const std::optional<bool> &override_flag) {
228
- if (override_flag.has_value()) {
229
- return *override_flag;
230
- }
231
- if (rb_respond_to(ruby_stream, rb_id_seek_part_method)) {
232
- return true;
233
- }
234
- if (rb_respond_to(ruby_stream, rb_id_open_part_io_method)) {
235
- return true;
236
- }
237
- return false;
238
- }
239
-
240
- static void ensure_required_methods(VALUE ruby_stream) {
241
- if (!rb_respond_to(ruby_stream, rb_id_open_part_method) && !rb_respond_to(ruby_stream, rb_id_open_part_io_method)) {
242
- rb_raise(rb_eNotImpError, "Stream subclasses must implement #open_part_io or #open_part");
243
- }
244
- if (!rb_respond_to(ruby_stream, rb_id_read_part_method) && !rb_respond_to(ruby_stream, rb_id_open_part_io_method)) {
245
- rb_raise(rb_eNotImpError, "Stream subclasses must implement #open_part_io or #read_part");
246
- }
247
- }
248
-
249
- void activate_io(VALUE io) {
250
- if (!rb_respond_to(io, rb_id_read_method)) {
251
- rb_raise(rb_eTypeError, "open_part_io must return an object responding to #read");
252
- }
253
- release_active_io();
254
- _active_io = io;
255
- rb_gc_register_address(&_active_io);
256
- _active_io_seekable = rb_respond_to(io, rb_id_seek_method);
257
- _active_io_tellable = rb_respond_to(io, rb_id_tell_method);
258
- _active_io_has_close = rb_respond_to(io, rb_id_close_method);
259
- _active_io_has_size = rb_respond_to(io, rb_id_size_method);
260
- }
261
-
262
- void release_active_io() {
263
- if (_active_io == Qnil) {
264
- return;
265
- }
266
- if (_active_io_has_close) {
267
- rb_funcall(_active_io, rb_id_close_method, 0);
268
- }
269
- rb_gc_unregister_address(&_active_io);
270
- _active_io = Qnil;
271
- _active_io_seekable = false;
272
- _active_io_tellable = false;
273
- _active_io_has_close = false;
274
- _active_io_has_size = false;
275
- }
276
-
277
- VALUE _ruby_stream;
278
- bool _has_close;
279
- bool _has_seek;
280
- bool _has_size;
281
- bool _has_open_part_io;
282
- bool _has_close_part_io;
283
- VALUE _active_io;
284
- bool _active_io_seekable;
285
- bool _active_io_tellable;
286
- bool _active_io_has_close;
287
- bool _active_io_has_size;
288
- };
289
-
290
- struct RubyStreamWrapper {
291
- std::shared_ptr<RubyUserStream> stream;
292
- };
293
-
294
- static void stream_wrapper_free(void *ptr) {
295
- auto *wrapper = static_cast<RubyStreamWrapper *>(ptr);
296
- delete wrapper;
297
- }
298
-
299
- static size_t stream_wrapper_memsize(const void *ptr) {
300
- return sizeof(RubyStreamWrapper);
301
- }
302
-
303
- static const rb_data_type_t stream_wrapper_type = {
304
- "ArchiveR::Stream",
305
- {
306
- nullptr,
307
- stream_wrapper_free,
308
- stream_wrapper_memsize,
309
- },
310
- nullptr,
311
- nullptr,
312
- RUBY_TYPED_FREE_IMMEDIATELY,
313
- };
314
-
315
- static RubyStreamWrapper *get_stream_wrapper(VALUE self) {
316
- RubyStreamWrapper *wrapper = nullptr;
317
- TypedData_Get_Struct(self, RubyStreamWrapper, &stream_wrapper_type, wrapper);
318
- if (!wrapper) {
319
- rb_raise(rb_eRuntimeError, "invalid Stream wrapper");
320
- }
321
- return wrapper;
322
- }
323
-
324
- static VALUE stream_allocate(VALUE klass) {
325
- auto *wrapper = new RubyStreamWrapper();
326
- wrapper->stream.reset();
327
- return TypedData_Wrap_Struct(klass, &stream_wrapper_type, wrapper);
328
- }
329
-
330
- static VALUE stream_initialize(int argc, VALUE *argv, VALUE self) {
331
- RubyStreamWrapper *wrapper = get_stream_wrapper(self);
332
- if (wrapper->stream) {
333
- rb_raise(rb_eRuntimeError, "Stream already initialized");
334
- }
335
- VALUE hierarchy_value = Qnil;
336
- VALUE opts = Qnil;
337
- rb_scan_args(argc, argv, "11", &hierarchy_value, &opts);
338
- PathHierarchy hierarchy = rb_value_to_path_hierarchy(hierarchy_value);
339
- std::optional<bool> seekable_override;
340
- if (!NIL_P(opts)) {
341
- Check_Type(opts, T_HASH);
342
- static ID id_seekable = rb_intern("seekable");
343
- VALUE seekable_value = rb_hash_aref(opts, ID2SYM(id_seekable));
344
- if (!NIL_P(seekable_value)) {
345
- seekable_override = RTEST(seekable_value);
346
- }
347
- }
348
- wrapper->stream = std::make_shared<RubyUserStream>(self, std::move(hierarchy), seekable_override);
349
- return self;
350
- }
351
-
352
- static VALUE entry_fault_to_rb(const EntryFault &fault) {
353
- VALUE hash = rb_hash_new();
354
- static ID id_message = rb_intern("message");
355
- static ID id_errno = rb_intern("errno");
356
- static ID id_hierarchy = rb_intern("hierarchy");
357
- static ID id_path = rb_intern("path");
358
-
359
- rb_hash_aset(hash, ID2SYM(id_message), cpp_string_to_rb(fault.message));
360
- rb_hash_aset(hash, ID2SYM(id_errno), INT2NUM(fault.errno_value));
361
- rb_hash_aset(hash, ID2SYM(id_hierarchy), path_hierarchy_to_rb(fault.hierarchy));
362
- std::string path_string = fault.hierarchy.empty() ? std::string() : hierarchy_display(fault.hierarchy);
363
- rb_hash_aset(hash, ID2SYM(id_path), cpp_string_to_rb(path_string));
364
- return hash;
365
- }
366
-
367
- static FaultCallback make_ruby_fault_callback(VALUE callable) {
368
- if (NIL_P(callable)) {
369
- return {};
370
- }
371
-
372
- static ID id_call = rb_intern("call");
373
- if (!rb_respond_to(callable, id_call)) {
374
- rb_raise(rb_eTypeError, "fault callback must respond to #call");
375
- }
376
-
377
- auto holder = std::make_shared<RubyCallbackHolder>(callable);
378
-
379
- return [holder](const EntryFault &fault) {
380
- struct InvokePayload {
381
- std::shared_ptr<RubyCallbackHolder> holder;
382
- VALUE fault_hash;
383
- } payload{ holder, entry_fault_to_rb(fault) };
384
-
385
- auto invoke = [](VALUE data) -> VALUE {
386
- auto *info = reinterpret_cast<InvokePayload *>(data);
387
- static ID id_call_inner = rb_intern("call");
388
- return rb_funcall(info->holder->proc_value, id_call_inner, 1, info->fault_hash);
389
- };
390
-
391
- int state = 0;
392
- rb_protect(invoke, reinterpret_cast<VALUE>(&payload), &state);
393
- if (state != 0) {
394
- rb_jump_tag(state);
395
- }
396
- return;
397
- };
398
- }
399
-
400
- // Helper: Convert Ruby hash options into TraverserOptions
401
- static void populate_traverser_options(VALUE opts, TraverserOptions &options) {
402
- if (NIL_P(opts)) {
403
- return;
404
- }
405
-
406
- Check_Type(opts, T_HASH);
407
-
408
- static ID id_passphrases = rb_intern("passphrases");
409
- static ID id_formats = rb_intern("formats");
410
- static ID id_metadata_keys = rb_intern("metadata_keys");
411
- static ID id_descend_archives = rb_intern("descend_archives");
412
-
413
- VALUE passphrases_val = rb_hash_aref(opts, ID2SYM(id_passphrases));
414
- if (!NIL_P(passphrases_val)) {
415
- Check_Type(passphrases_val, T_ARRAY);
416
- long len = RARRAY_LEN(passphrases_val);
417
- options.passphrases.reserve(len);
418
- for (long i = 0; i < len; ++i) {
419
- VALUE item = rb_ary_entry(passphrases_val, i);
420
- options.passphrases.push_back(rb_string_to_cpp(StringValue(item)));
421
- }
422
- }
423
-
424
- VALUE formats_val = rb_hash_aref(opts, ID2SYM(id_formats));
425
- if (!NIL_P(formats_val)) {
426
- Check_Type(formats_val, T_ARRAY);
427
- long len = RARRAY_LEN(formats_val);
428
- options.formats.reserve(len);
429
- for (long i = 0; i < len; ++i) {
430
- VALUE item = rb_ary_entry(formats_val, i);
431
- options.formats.push_back(rb_string_to_cpp(StringValue(item)));
432
- }
433
- }
434
-
435
- VALUE metadata_val = rb_hash_aref(opts, ID2SYM(id_metadata_keys));
436
- if (!NIL_P(metadata_val)) {
437
- Check_Type(metadata_val, T_ARRAY);
438
- long len = RARRAY_LEN(metadata_val);
439
- options.metadata_keys.reserve(len);
440
- for (long i = 0; i < len; ++i) {
441
- VALUE item = rb_ary_entry(metadata_val, i);
442
- options.metadata_keys.push_back(rb_string_to_cpp(StringValue(item)));
443
- }
444
- }
445
-
446
- VALUE descend_val = rb_hash_aref(opts, ID2SYM(id_descend_archives));
447
- if (!NIL_P(descend_val)) {
448
- options.descend_archives = RTEST(descend_val);
449
- }
450
- }
451
-
452
- static PathEntry rb_value_to_path_entry(VALUE value);
453
-
454
- static PathHierarchy rb_value_to_path_hierarchy(VALUE value) {
455
- if (RB_TYPE_P(value, T_STRING)) {
456
- return make_single_path(rb_string_to_cpp(value));
457
- }
458
-
459
- VALUE array = rb_check_array_type(value);
460
- if (NIL_P(array)) {
461
- rb_raise(rb_eTypeError, "path hierarchy must be a String or Array");
462
- }
463
-
464
- const long length = RARRAY_LEN(array);
465
- if (length == 0) {
466
- rb_raise(rb_eArgError, "path hierarchy cannot be empty");
467
- }
468
-
469
- PathHierarchy hierarchy;
470
- hierarchy.reserve(static_cast<size_t>(length));
471
- for (long i = 0; i < length; ++i) {
472
- VALUE component = rb_ary_entry(array, i);
473
- hierarchy.emplace_back(rb_value_to_path_entry(component));
474
- }
475
-
476
- return hierarchy;
477
- }
478
-
479
- static PathEntry rb_value_to_path_entry(VALUE value) {
480
- if (RB_TYPE_P(value, T_STRING)) {
481
- return PathEntry::single(rb_string_to_cpp(value));
482
- }
483
-
484
- VALUE array = rb_check_array_type(value);
485
- if (NIL_P(array)) {
486
- rb_raise(rb_eTypeError, "PathEntry must be String or Array");
487
- }
488
-
489
- const long length = RARRAY_LEN(array);
490
- if (length == 0) {
491
- rb_raise(rb_eArgError, "PathEntry array cannot be empty");
492
- }
493
-
494
- bool all_strings = true;
495
- for (long i = 0; i < length; ++i) {
496
- VALUE element = rb_ary_entry(array, i);
497
- if (!RB_TYPE_P(element, T_STRING)) {
498
- all_strings = false;
499
- break;
500
- }
501
- }
502
-
503
- if (all_strings) {
504
- std::vector<std::string> parts;
505
- parts.reserve(static_cast<size_t>(length));
506
- for (long i = 0; i < length; ++i) {
507
- parts.emplace_back(rb_string_to_cpp(rb_ary_entry(array, i)));
508
- }
509
- return PathEntry::multi_volume(std::move(parts));
510
- }
511
-
512
- PathEntry::NodeList nodes;
513
- nodes.reserve(static_cast<size_t>(length));
514
- for (long i = 0; i < length; ++i) {
515
- nodes.emplace_back(rb_value_to_path_entry(rb_ary_entry(array, i)));
516
- }
517
- return PathEntry::nested(std::move(nodes));
518
- }
519
-
520
- // Helper: Convert Ruby path argument into vector of PathHierarchy
521
- static std::vector<PathHierarchy> rb_paths_to_hierarchies(VALUE paths) {
522
- if (RB_TYPE_P(paths, T_STRING)) {
523
- std::vector<PathHierarchy> result;
524
- result.emplace_back(make_single_path(rb_string_to_cpp(paths)));
525
- return result;
526
- }
527
-
528
- VALUE array = rb_check_array_type(paths);
529
- if (NIL_P(array)) {
530
- rb_raise(rb_eTypeError, "paths must be a String or an Array");
531
- }
532
-
533
- const long length = RARRAY_LEN(array);
534
- if (length == 0) {
535
- rb_raise(rb_eArgError, "paths cannot be empty");
536
- }
537
-
538
- std::vector<PathHierarchy> result;
539
- result.reserve(static_cast<size_t>(length));
540
- for (long i = 0; i < length; ++i) {
541
- VALUE item = rb_ary_entry(array, i);
542
- result.emplace_back(rb_value_to_path_hierarchy(item));
543
- }
544
-
545
- return result;
546
- }
547
-
548
- static VALUE invoke_ruby_stream_factory(const std::shared_ptr<RubyCallbackHolder> &holder, const PathHierarchy &hierarchy) {
549
- struct FactoryPayload {
550
- std::shared_ptr<RubyCallbackHolder> holder;
551
- VALUE arg;
552
- } payload{ holder, path_hierarchy_to_rb(hierarchy) };
553
-
554
- auto invoke = [](VALUE data) -> VALUE {
555
- auto *info = reinterpret_cast<FactoryPayload *>(data);
556
- return rb_funcall(info->holder->proc_value, rb_id_call_method, 1, info->arg);
557
- };
558
-
559
- int state = 0;
560
- VALUE result = rb_protect(invoke, reinterpret_cast<VALUE>(&payload), &state);
561
- if (state != 0) {
562
- rb_jump_tag(state);
563
- }
564
- return result;
565
- }
566
-
567
- static std::shared_ptr<IDataStream> stream_from_ruby_value(VALUE value, const PathHierarchy &requested) {
568
- if (NIL_P(value)) {
569
- return nullptr;
570
- }
571
-
572
- if (rb_obj_is_kind_of(value, cStream)) {
573
- RubyStreamWrapper *wrapper = get_stream_wrapper(value);
574
- if (!wrapper->stream) {
575
- rb_raise(rb_eRuntimeError, "Stream instance is not initialized");
576
- }
577
- if (!hierarchies_equal(wrapper->stream->source_hierarchy(), requested)) {
578
- rb_raise(rb_eArgError, "Stream instance must be created with the same hierarchy passed to the factory");
579
- }
580
- return wrapper->stream;
581
- }
582
-
583
- rb_raise(rb_eTypeError, "stream factory must return nil or an Archive_r::Stream instance");
584
- }
585
-
586
- // Helper: Convert EntryMetadataValue to Ruby object
587
- static VALUE metadata_value_to_rb(const EntryMetadataValue &value) {
588
- struct Visitor {
589
- VALUE
590
- operator()(std::monostate) const { return Qnil; }
591
- VALUE
592
- operator()(bool v) const { return v ? Qtrue : Qfalse; }
593
- VALUE
594
- operator()(int64_t v) const { return LL2NUM(v); }
595
- VALUE
596
- operator()(uint64_t v) const { return ULL2NUM(v); }
597
- VALUE
598
- operator()(const std::string & v) const { return cpp_string_to_rb(v); }
599
- VALUE
600
- operator()(const std::vector<uint8_t> &v) const {
601
- if (v.empty()) {
602
- return rb_str_new(nullptr, 0);
603
- }
604
- return rb_str_new(reinterpret_cast<const char *>(v.data()), v.size());
605
- }
606
- VALUE
607
- operator()(const EntryMetadataTime & v) const {
608
- VALUE hash = rb_hash_new();
609
- rb_hash_aset(hash, ID2SYM(rb_intern("seconds")), LL2NUM(v.seconds));
610
- rb_hash_aset(hash, ID2SYM(rb_intern("nanoseconds")), INT2NUM(v.nanoseconds));
611
- return hash;
612
- }
613
- VALUE
614
- operator()(const EntryMetadataDeviceNumbers & v) const {
615
- VALUE hash = rb_hash_new();
616
- rb_hash_aset(hash, ID2SYM(rb_intern("major")), ULL2NUM(v.major));
617
- rb_hash_aset(hash, ID2SYM(rb_intern("minor")), ULL2NUM(v.minor));
618
- return hash;
619
- }
620
- VALUE
621
- operator()(const EntryMetadataFileFlags & v) const {
622
- VALUE hash = rb_hash_new();
623
- rb_hash_aset(hash, ID2SYM(rb_intern("set")), ULL2NUM(v.set));
624
- rb_hash_aset(hash, ID2SYM(rb_intern("clear")), ULL2NUM(v.clear));
625
- return hash;
626
- }
627
- VALUE
628
- operator()(const std::vector<EntryMetadataXattr> &vec) const {
629
- VALUE array = rb_ary_new_capa(vec.size());
630
- for (const auto &item : vec) {
631
- VALUE hash = rb_hash_new();
632
- rb_hash_aset(hash, ID2SYM(rb_intern("name")), cpp_string_to_rb(item.name));
633
- if (item.value.empty()) {
634
- rb_hash_aset(hash, ID2SYM(rb_intern("value")), rb_str_new(nullptr, 0));
635
- } else {
636
- rb_hash_aset(hash, ID2SYM(rb_intern("value")), rb_str_new(reinterpret_cast<const char *>(item.value.data()), item.value.size()));
637
- }
638
- rb_ary_push(array, hash);
639
- }
640
- return array;
641
- }
642
- VALUE
643
- operator()(const std::vector<EntryMetadataSparseChunk> &vec) const {
644
- VALUE array = rb_ary_new_capa(vec.size());
645
- for (const auto &item : vec) {
646
- VALUE hash = rb_hash_new();
647
- rb_hash_aset(hash, ID2SYM(rb_intern("offset")), LL2NUM(item.offset));
648
- rb_hash_aset(hash, ID2SYM(rb_intern("length")), LL2NUM(item.length));
649
- rb_ary_push(array, hash);
650
- }
651
- return array;
652
- }
653
- VALUE
654
- operator()(const std::vector<EntryMetadataDigest> &vec) const {
655
- VALUE array = rb_ary_new_capa(vec.size());
656
- for (const auto &item : vec) {
657
- VALUE hash = rb_hash_new();
658
- rb_hash_aset(hash, ID2SYM(rb_intern("algorithm")), cpp_string_to_rb(item.algorithm));
659
- if (item.value.empty()) {
660
- rb_hash_aset(hash, ID2SYM(rb_intern("value")), rb_str_new(nullptr, 0));
661
- } else {
662
- rb_hash_aset(hash, ID2SYM(rb_intern("value")), rb_str_new(reinterpret_cast<const char *>(item.value.data()), item.value.size()));
663
- }
664
- rb_ary_push(array, hash);
665
- }
666
- return array;
667
- }
668
- } visitor;
669
-
670
- return std::visit(visitor, value);
671
- }
672
-
673
- static VALUE archive_r_register_stream_factory(int argc, VALUE *argv, VALUE self) {
674
- VALUE callable = Qnil;
675
- rb_scan_args(argc, argv, "01", &callable);
676
-
677
- if (!NIL_P(callable) && rb_block_given_p()) {
678
- rb_raise(rb_eArgError, "provide callable argument or block, not both");
679
- }
680
-
681
- if (NIL_P(callable) && rb_block_given_p()) {
682
- callable = rb_block_proc();
683
- }
684
-
685
- if (NIL_P(callable)) {
686
- set_root_stream_factory(RootStreamFactory{});
687
- g_stream_factory_callback.reset();
688
- return Qnil;
689
- }
690
-
691
- if (!rb_respond_to(callable, rb_id_call_method)) {
692
- rb_raise(rb_eTypeError, "stream factory must respond to #call");
693
- }
694
-
695
- auto holder = std::make_shared<RubyCallbackHolder>(callable);
696
-
697
- RootStreamFactory factory = [holder](const PathHierarchy &hierarchy) -> std::shared_ptr<IDataStream> {
698
- VALUE result = invoke_ruby_stream_factory(holder, hierarchy);
699
- return stream_from_ruby_value(result, hierarchy);
700
- };
701
-
702
- set_root_stream_factory(factory);
703
- g_stream_factory_callback = holder;
704
- return Qnil;
705
- }
706
-
707
- static VALUE archive_r_on_fault(int argc, VALUE *argv, VALUE self) {
708
- VALUE callback = Qnil;
709
- rb_scan_args(argc, argv, "01", &callback);
710
-
711
- if (!NIL_P(callback) && rb_block_given_p()) {
712
- rb_raise(rb_eArgError, "provide callable argument or block, not both");
713
- }
714
-
715
- if (NIL_P(callback) && rb_block_given_p()) {
716
- callback = rb_block_proc();
717
- }
718
-
719
- FaultCallback cb = make_ruby_fault_callback(callback);
720
- register_fault_callback(std::move(cb));
721
- return self;
722
- }
723
-
724
- //=============================================================================
725
- // Entry class
726
- //=============================================================================
727
-
728
- // EntryWrapper: references an Entry owned by Traverser iterator
729
- struct EntryWrapper {
730
- Entry *entry_ref;
731
- std::unique_ptr<Entry> entry_copy;
732
-
733
- EntryWrapper(Entry *ref, std::unique_ptr<Entry> copy)
734
- : entry_ref(ref)
735
- , entry_copy(std::move(copy)) {}
736
- };
737
-
738
- // Free function for EntryWrapper (wrapper only, Entry owned elsewhere)
739
- static void entry_free(void *ptr) { delete static_cast<EntryWrapper *>(ptr); }
740
-
741
- // Wrap Entry pointer in EntryWrapper (does not copy Entry)
742
- static VALUE entry_wrap(Entry &entry) {
743
- std::unique_ptr<Entry> copy(new Entry(entry));
744
- EntryWrapper *wrapper = new EntryWrapper(&entry, std::move(copy));
745
- return Data_Wrap_Struct(cEntry, nullptr, entry_free, wrapper);
746
- }
747
-
748
- static VALUE entry_invalidate(VALUE entry_obj) {
749
- EntryWrapper *wrapper;
750
- Data_Get_Struct(entry_obj, EntryWrapper, wrapper);
751
- if (wrapper) {
752
- wrapper->entry_ref = nullptr;
753
- }
754
- return Qnil;
755
- }
756
-
757
- static EntryWrapper *entry_get_wrapper(VALUE self) {
758
- EntryWrapper *wrapper;
759
- Data_Get_Struct(self, EntryWrapper, wrapper);
760
- if (!wrapper) {
761
- rb_raise(rb_eRuntimeError, "Invalid Entry handle");
762
- }
763
- return wrapper;
764
- }
765
-
766
- // Helper to fetch Entry for read operations, falling back to preserved copy
767
- static Entry *entry_for_read(VALUE self) {
768
- EntryWrapper *wrapper = entry_get_wrapper(self);
769
- if (wrapper->entry_ref) {
770
- return wrapper->entry_ref;
771
- }
772
- if (wrapper->entry_copy) {
773
- return wrapper->entry_copy.get();
774
- }
775
- rb_raise(rb_eRuntimeError, "Entry data is no longer available");
776
- return nullptr;
777
- }
778
-
779
- // Helper to fetch live Entry pointer required for mutating operations
780
- static Entry *entry_for_live(VALUE self) {
781
- EntryWrapper *wrapper = entry_get_wrapper(self);
782
- if (!wrapper->entry_ref) {
783
- rb_raise(rb_eRuntimeError, "Entry is no longer valid");
784
- }
785
- return wrapper->entry_ref;
786
- }
787
-
788
- // Entry#path -> String
789
- static VALUE entry_path(VALUE self) {
790
- Entry *entry = entry_for_read(self);
791
- const std::string entry_path_str = entry->path();
792
- return cpp_string_to_rb(entry->path());
793
- }
794
-
795
- // Entry#path_hierarchy -> Array
796
- static VALUE entry_path_hierarchy(VALUE self) {
797
- Entry *entry = entry_for_read(self);
798
- return path_hierarchy_to_rb(entry->path_hierarchy());
799
- }
800
-
801
- // Entry#name -> String
802
- static VALUE entry_name(VALUE self) {
803
- Entry *entry = entry_for_read(self);
804
- return cpp_string_to_rb(entry->name());
805
- }
806
-
807
- // Entry#size -> Integer
808
- static VALUE entry_size(VALUE self) {
809
- Entry *entry = entry_for_read(self);
810
- return LONG2NUM(entry->size());
811
- }
812
-
813
- // Entry#file? -> Boolean
814
- static VALUE entry_is_file(VALUE self) {
815
- Entry *entry = entry_for_read(self);
816
- return entry->is_file() ? Qtrue : Qfalse;
817
- }
818
-
819
- // Entry#directory? -> Boolean
820
- static VALUE entry_is_directory(VALUE self) {
821
- Entry *entry = entry_for_read(self);
822
- return entry->is_directory() ? Qtrue : Qfalse;
823
- }
824
-
825
- // Entry#depth -> Integer
826
- static VALUE entry_depth(VALUE self) {
827
- Entry *entry = entry_for_read(self);
828
- return INT2NUM(entry->depth());
829
- }
830
-
831
- // Entry#set_descent(enabled) -> self
832
- static VALUE entry_set_descent(VALUE self, VALUE enabled) {
833
- Entry *entry = entry_for_live(self);
834
- entry->set_descent(RTEST(enabled));
835
- return self;
836
- }
837
-
838
- // Entry#set_multi_volume_group(base_name, order: :natural) -> nil
839
- static VALUE entry_set_multi_volume_group(int argc, VALUE *argv, VALUE self) {
840
- VALUE base_name_val;
841
- VALUE options_val = Qnil;
842
-
843
- rb_scan_args(argc, argv, "11", &base_name_val, &options_val);
844
-
845
- MultiVolumeGroupOptions options;
846
- if (!NIL_P(options_val)) {
847
- VALUE hash = rb_check_hash_type(options_val);
848
- if (NIL_P(hash)) {
849
- rb_raise(rb_eTypeError, "options must be a Hash");
850
- }
851
-
852
- static ID id_order = rb_intern("order");
853
- VALUE order_val = rb_hash_aref(hash, ID2SYM(id_order));
854
- if (!NIL_P(order_val)) {
855
- std::string order_str;
856
- if (SYMBOL_P(order_val)) {
857
- order_str = rb_id2name(SYM2ID(order_val));
858
- } else {
859
- order_str = rb_string_to_cpp(StringValue(order_val));
860
- }
861
- for (char &ch : order_str) {
862
- ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
863
- }
864
- if (order_str == "given") {
865
- options.ordering = PathEntry::Parts::Ordering::Given;
866
- } else if (order_str == "natural") {
867
- options.ordering = PathEntry::Parts::Ordering::Natural;
868
- } else {
869
- rb_raise(rb_eArgError, "order must be :natural or :given");
870
- }
871
- }
872
- }
873
-
874
- Entry *entry = entry_for_live(self);
875
- try {
876
- entry->set_multi_volume_group(rb_string_to_cpp(StringValue(base_name_val)), options);
877
- } catch (const std::exception &e) {
878
- rb_raise(rb_eRuntimeError, "Failed to set multi-volume group: %s", e.what());
879
- }
880
- return Qnil;
881
- }
882
-
883
- // Entry#read(length = nil) -> String
884
- static VALUE entry_read(int argc, VALUE *argv, VALUE self) {
885
- VALUE length_val = Qnil;
886
- rb_scan_args(argc, argv, "01", &length_val);
887
-
888
- bool bounded_read = false;
889
- size_t requested_size = 0;
890
- if (!NIL_P(length_val)) {
891
- long long length_long = NUM2LL(length_val);
892
- if (length_long == 0) {
893
- return rb_str_new("", 0);
894
- } else if (length_long > 0) {
895
- const auto max_allowed = std::numeric_limits<size_t>::max();
896
- if (static_cast<unsigned long long>(length_long) > max_allowed) {
897
- rb_raise(rb_eRangeError, "requested length exceeds platform limits");
898
- }
899
- requested_size = static_cast<size_t>(length_long);
900
- bounded_read = true;
901
- }
902
- // Negative values fall through to the streaming path (full read)
903
- }
904
-
905
- Entry *entry = entry_for_read(self);
906
- const std::string entry_path_str = entry->path();
907
-
908
- try {
909
- if (bounded_read) {
910
- std::vector<uint8_t> buffer(requested_size);
911
- const ssize_t bytes_read = entry->read(buffer.data(), buffer.size());
912
- if (bytes_read < 0) {
913
- rb_raise(rb_eRuntimeError, "Failed to read entry payload at %s", entry_path_str.c_str());
914
- }
915
- return rb_str_new(reinterpret_cast<const char *>(buffer.data()), static_cast<long>(bytes_read));
916
- }
917
-
918
- std::string aggregate;
919
- const uint64_t reported_size = entry->size();
920
- if (reported_size > 0 && reported_size <= static_cast<uint64_t>(std::numeric_limits<size_t>::max())) {
921
- aggregate.reserve(static_cast<size_t>(reported_size));
922
- }
923
-
924
- std::vector<uint8_t> chunk(64 * 1024);
925
- while (true) {
926
- const ssize_t bytes_read = entry->read(chunk.data(), chunk.size());
927
- if (bytes_read < 0) {
928
- rb_raise(rb_eRuntimeError, "Failed to read entry payload at %s", entry_path_str.c_str());
929
- }
930
- if (bytes_read == 0) {
931
- break;
932
- }
933
- aggregate.append(reinterpret_cast<const char *>(chunk.data()), static_cast<size_t>(bytes_read));
934
- }
935
-
936
- return rb_str_new(aggregate.data(), static_cast<long>(aggregate.size()));
937
- } catch (const std::exception &e) {
938
- rb_raise(rb_eRuntimeError, "Failed to read entry at %s: %s", entry_path_str.c_str(), e.what());
939
- return Qnil;
940
- }
941
- }
942
-
943
- // Entry#metadata -> Hash
944
- static VALUE entry_metadata(VALUE self) {
945
- Entry *entry = entry_for_read(self);
946
- VALUE hash = rb_hash_new();
947
- const EntryMetadataMap &metadata = entry->metadata();
948
- for (const auto &kv : metadata) {
949
- rb_hash_aset(hash, cpp_string_to_rb(kv.first), metadata_value_to_rb(kv.second));
950
- }
951
- return hash;
952
- }
953
-
954
- // Entry#metadata_value(key) -> Object or nil
955
- static VALUE entry_metadata_value(VALUE self, VALUE key) {
956
- Entry *entry = entry_for_read(self);
957
- std::string key_str = rb_string_to_cpp(StringValue(key));
958
- const EntryMetadataValue *value = entry->find_metadata(key_str);
959
- if (!value) {
960
- return Qnil;
961
- }
962
- return metadata_value_to_rb(*value);
963
- }
964
-
965
- //=============================================================================
966
- // Traverser class
967
- //=============================================================================
968
-
969
- // Free function for Traverser
970
- static void traverser_free(void *ptr) { delete static_cast<Traverser *>(ptr); }
971
-
972
- // Wrap Traverser pointer
973
- static VALUE traverser_wrap(Traverser *traverser) { return Data_Wrap_Struct(cTraverser, nullptr, traverser_free, traverser); }
974
-
975
- // Get Traverser pointer from Ruby object
976
- static Traverser *traverser_unwrap(VALUE self) {
977
- Traverser *traverser;
978
- Data_Get_Struct(self, Traverser, traverser);
979
- return traverser;
980
- }
981
-
982
- // Traverser allocation
983
- static VALUE traverser_allocate(VALUE klass) { return Data_Wrap_Struct(klass, nullptr, traverser_free, nullptr); }
984
-
985
- // Traverser.new(paths, passphrases: [], formats: [], metadata_keys: []) -> Traverser
986
- static VALUE traverser_initialize(int argc, VALUE *argv, VALUE self) {
987
- VALUE paths;
988
- VALUE opts = Qnil;
989
- rb_scan_args(argc, argv, "11", &paths, &opts);
990
-
991
- try {
992
- std::vector<PathHierarchy> path_list = rb_paths_to_hierarchies(paths);
993
- TraverserOptions options;
994
- populate_traverser_options(opts, options);
995
- Traverser *traverser = new Traverser(std::move(path_list), options);
996
- DATA_PTR(self) = traverser;
997
- return self;
998
- } catch (const std::exception &e) {
999
- rb_raise(rb_eRuntimeError, "Failed to open archive: %s", e.what());
1000
- return Qnil;
1001
- }
1002
- }
1003
-
1004
- // Traverser#each { |entry| ... } -> Enumerator
1005
- static VALUE yield_entry(VALUE entry_obj) {
1006
- rb_yield(entry_obj);
1007
- return Qnil;
1008
- }
1009
-
1010
- static VALUE traverser_each(VALUE self) {
1011
- Traverser *traverser = traverser_unwrap(self);
1012
-
1013
- // If no block given, return Enumerator
1014
- if (!rb_block_given_p()) {
1015
- return rb_funcall(self, rb_intern("to_enum"), 1, ID2SYM(rb_intern("each")));
1016
- }
1017
-
1018
- try {
1019
- for (auto it = traverser->begin(); it != traverser->end(); ++it) {
1020
- Entry &entry = *it;
1021
- VALUE rb_entry = entry_wrap(entry);
1022
- rb_ensure(yield_entry, rb_entry, entry_invalidate, rb_entry);
1023
- }
1024
- } catch (const std::exception &e) {
1025
- rb_raise(rb_eRuntimeError, "Error during traversal: %s", e.what());
1026
- }
1027
-
1028
- return self;
1029
- }
1030
-
1031
- // Helper for Traverser.open cleanup
1032
- static VALUE traverser_close_helper(VALUE arg) {
1033
- // Nothing to do - Traverser cleanup is automatic
1034
- return Qnil;
1035
- }
1036
-
1037
- // Traverser.open(path, opts = {}) { |traverser| ... } -> result of block
1038
- static VALUE traverser_s_open(int argc, VALUE *argv, VALUE klass) {
1039
- VALUE traverser = rb_class_new_instance(argc, argv, klass);
1040
-
1041
- if (rb_block_given_p()) {
1042
- return rb_ensure(rb_yield, traverser, traverser_close_helper, traverser);
1043
- }
1044
-
1045
- return traverser;
1046
- }
1047
-
1048
- //=============================================================================
1049
- // Module initialization
1050
- //=============================================================================
1051
-
1052
- extern "C" void Init_archive_r() {
1053
- // Define module Archive_r
1054
- mArchive_r = rb_define_module("Archive_r");
1055
- cStream = rb_define_class_under(mArchive_r, "Stream", rb_cObject);
1056
- rb_define_alloc_func(cStream, stream_allocate);
1057
- rb_define_method(cStream, "initialize", RUBY_METHOD_FUNC(stream_initialize), -1);
1058
-
1059
- rb_id_read_method = rb_intern("read");
1060
- rb_id_seek_method = rb_intern("seek");
1061
- rb_id_tell_method = rb_intern("tell");
1062
- rb_id_eof_method = rb_intern("eof?");
1063
- rb_id_close_method = rb_intern("close");
1064
- rb_id_size_method = rb_intern("size");
1065
- rb_id_call_method = rb_intern("call");
1066
- rb_id_open_part_method = rb_intern("open_part");
1067
- rb_id_close_part_method = rb_intern("close_part");
1068
- rb_id_read_part_method = rb_intern("read_part");
1069
- rb_id_seek_part_method = rb_intern("seek_part");
1070
- rb_id_part_size_method = rb_intern("part_size");
1071
- rb_id_open_part_io_method = rb_intern("open_part_io");
1072
- rb_id_close_part_io_method = rb_intern("close_part_io");
1073
-
1074
- // Define Entry class
1075
- cEntry = rb_define_class_under(mArchive_r, "Entry", rb_cObject);
1076
- rb_undef_alloc_func(cEntry);
1077
-
1078
- rb_define_method(cEntry, "path", RUBY_METHOD_FUNC(entry_path), 0);
1079
- rb_define_method(cEntry, "path_hierarchy", RUBY_METHOD_FUNC(entry_path_hierarchy), 0);
1080
- rb_define_method(cEntry, "name", RUBY_METHOD_FUNC(entry_name), 0);
1081
- rb_define_method(cEntry, "size", RUBY_METHOD_FUNC(entry_size), 0);
1082
- rb_define_method(cEntry, "file?", RUBY_METHOD_FUNC(entry_is_file), 0);
1083
- rb_define_method(cEntry, "directory?", RUBY_METHOD_FUNC(entry_is_directory), 0);
1084
- rb_define_method(cEntry, "depth", RUBY_METHOD_FUNC(entry_depth), 0);
1085
- rb_define_method(cEntry, "set_descent", RUBY_METHOD_FUNC(entry_set_descent), 1);
1086
- rb_define_method(cEntry, "set_multi_volume_group", RUBY_METHOD_FUNC(entry_set_multi_volume_group), -1);
1087
- rb_define_method(cEntry, "read", RUBY_METHOD_FUNC(entry_read), -1);
1088
- rb_define_method(cEntry, "metadata", RUBY_METHOD_FUNC(entry_metadata), 0);
1089
- rb_define_method(cEntry, "metadata_value", RUBY_METHOD_FUNC(entry_metadata_value), 1);
1090
-
1091
- // Define Traverser class
1092
- cTraverser = rb_define_class_under(mArchive_r, "Traverser", rb_cObject);
1093
-
1094
- rb_define_alloc_func(cTraverser, traverser_allocate);
1095
- rb_define_method(cTraverser, "initialize", RUBY_METHOD_FUNC(traverser_initialize), -1);
1096
- rb_define_method(cTraverser, "each", RUBY_METHOD_FUNC(traverser_each), 0);
1097
- rb_define_singleton_method(cTraverser, "open", RUBY_METHOD_FUNC(traverser_s_open), -1);
1098
-
1099
- // Make Traverser enumerable
1100
- rb_include_module(cTraverser, rb_mEnumerable);
1101
-
1102
- rb_define_module_function(mArchive_r, "on_fault", RUBY_METHOD_FUNC(archive_r_on_fault), -1);
1103
- rb_define_module_function(mArchive_r, "register_stream_factory", RUBY_METHOD_FUNC(archive_r_register_stream_factory), -1);
1104
-
1105
- rb_set_end_proc(archive_r_cleanup, Qnil);
1106
- }
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2025 archive_r Team
3
+
4
+ #include "archive_r/data_stream.h"
5
+ #include "archive_r/entry.h"
6
+ #include "archive_r/entry_fault.h"
7
+ #include "archive_r/multi_volume_stream_base.h"
8
+ #include "archive_r/path_hierarchy_utils.h"
9
+ #include "archive_r/traverser.h"
10
+ #include <cctype>
11
+ #include <cstdio>
12
+ #include <cstring>
13
+ #include <memory>
14
+ #include <ruby.h>
15
+ #include <stdexcept>
16
+ #include <string>
17
+ #include <utility>
18
+ #include <variant>
19
+ #include <vector>
20
+ #include <limits>
21
+ #include <optional>
22
+
23
+ using namespace archive_r;
24
+
25
+ // Ruby module and class references
26
+ static VALUE mArchive_r;
27
+ static VALUE cTraverser;
28
+ static VALUE cEntry;
29
+ static ID rb_id_read_method;
30
+ static ID rb_id_seek_method;
31
+ static ID rb_id_tell_method;
32
+ static ID rb_id_eof_method;
33
+ static ID rb_id_close_method;
34
+ static ID rb_id_size_method;
35
+ static ID rb_id_call_method;
36
+ static ID rb_id_open_part_method;
37
+ static ID rb_id_close_part_method;
38
+ static ID rb_id_read_part_method;
39
+ static ID rb_id_seek_part_method;
40
+ static ID rb_id_part_size_method;
41
+ static ID rb_id_open_part_io_method;
42
+ static ID rb_id_close_part_io_method;
43
+ struct RubyCallbackHolder;
44
+ static std::shared_ptr<RubyCallbackHolder> g_stream_factory_callback;
45
+
46
+ // Helper: Convert Ruby string to C++ string
47
+ static VALUE cStream;
48
+ static PathHierarchy rb_value_to_path_hierarchy(VALUE value);
49
+ static std::string rb_string_to_cpp(VALUE rb_str) {
50
+ Check_Type(rb_str, T_STRING);
51
+ return std::string(RSTRING_PTR(rb_str), RSTRING_LEN(rb_str));
52
+ }
53
+
54
+ static void archive_r_cleanup(VALUE) {
55
+ register_fault_callback(FaultCallback{});
56
+ set_root_stream_factory(RootStreamFactory{});
57
+ g_stream_factory_callback.reset();
58
+ }
59
+
60
+ // Helper: Convert C++ string to Ruby string
61
+ static VALUE cpp_string_to_rb(const std::string &str) { return rb_str_new(str.c_str(), str.length()); }
62
+
63
+ static VALUE path_entry_to_rb(const PathEntry &entry) {
64
+ if (entry.is_single()) {
65
+ return cpp_string_to_rb(entry.single_value());
66
+ }
67
+ if (entry.is_multi_volume()) {
68
+ const auto &parts = entry.multi_volume_parts().values;
69
+ VALUE array = rb_ary_new_capa(parts.size());
70
+ for (const auto &part : parts) {
71
+ rb_ary_push(array, cpp_string_to_rb(part));
72
+ }
73
+ return array;
74
+ }
75
+ VALUE array = rb_ary_new_capa(entry.nested_nodes().size());
76
+ for (const auto &child : entry.nested_nodes()) {
77
+ rb_ary_push(array, path_entry_to_rb(child));
78
+ }
79
+ return array;
80
+ }
81
+
82
+ static VALUE path_hierarchy_to_rb(const PathHierarchy &hierarchy) {
83
+ VALUE array = rb_ary_new_capa(hierarchy.size());
84
+ for (const auto &component : hierarchy) {
85
+ rb_ary_push(array, path_entry_to_rb(component));
86
+ }
87
+ return array;
88
+ }
89
+
90
+ struct RubyCallbackHolder {
91
+ explicit RubyCallbackHolder(VALUE proc)
92
+ : proc_value(proc) {
93
+ rb_gc_register_address(&proc_value);
94
+ }
95
+
96
+ ~RubyCallbackHolder() { rb_gc_unregister_address(&proc_value); }
97
+
98
+ VALUE proc_value;
99
+ };
100
+
101
+ class RubyUserStream : public MultiVolumeStreamBase {
102
+ public:
103
+ RubyUserStream(VALUE ruby_stream, PathHierarchy hierarchy, std::optional<bool> seekable_override)
104
+ : MultiVolumeStreamBase(std::move(hierarchy), determine_seek_support(ruby_stream, seekable_override))
105
+ , _ruby_stream(ruby_stream)
106
+ , _has_close(rb_respond_to(ruby_stream, rb_id_close_part_method))
107
+ , _has_seek(rb_respond_to(ruby_stream, rb_id_seek_part_method))
108
+ , _has_size(rb_respond_to(ruby_stream, rb_id_part_size_method))
109
+ , _has_open_part_io(rb_respond_to(ruby_stream, rb_id_open_part_io_method))
110
+ , _has_close_part_io(rb_respond_to(ruby_stream, rb_id_close_part_io_method))
111
+ , _active_io(Qnil)
112
+ , _active_io_seekable(false)
113
+ , _active_io_tellable(false)
114
+ , _active_io_has_close(false)
115
+ , _active_io_has_size(false) {
116
+ ensure_required_methods(ruby_stream);
117
+ rb_gc_register_address(&_ruby_stream);
118
+ }
119
+
120
+ ~RubyUserStream() override {
121
+ release_active_io();
122
+ rb_gc_unregister_address(&_ruby_stream);
123
+ }
124
+
125
+ protected:
126
+ void open_single_part(const PathHierarchy &single_part) override {
127
+ if (_has_open_part_io) {
128
+ VALUE arg = path_hierarchy_to_rb(single_part);
129
+ VALUE io = rb_funcall(_ruby_stream, rb_id_open_part_io_method, 1, arg);
130
+ if (NIL_P(io)) {
131
+ rb_raise(rb_eRuntimeError, "open_part_io must return an IO-like object");
132
+ }
133
+ activate_io(io);
134
+ return;
135
+ }
136
+
137
+ VALUE arg = path_hierarchy_to_rb(single_part);
138
+ rb_funcall(_ruby_stream, rb_id_open_part_method, 1, arg);
139
+ }
140
+
141
+ void close_single_part() override {
142
+ if (_has_open_part_io) {
143
+ release_active_io();
144
+ if (_has_close_part_io) {
145
+ rb_funcall(_ruby_stream, rb_id_close_part_io_method, 0);
146
+ }
147
+ return;
148
+ }
149
+
150
+ if (_has_close) {
151
+ rb_funcall(_ruby_stream, rb_id_close_part_method, 0);
152
+ }
153
+ }
154
+
155
+ ssize_t read_from_single_part(void *buffer, size_t size) override {
156
+ if (size == 0) {
157
+ return 0;
158
+ }
159
+
160
+ if (_has_open_part_io) {
161
+ if (_active_io == Qnil) {
162
+ rb_raise(rb_eRuntimeError, "open_part_io must return an IO before reading");
163
+ }
164
+ VALUE result = rb_funcall(_active_io, rb_id_read_method, 1, SIZET2NUM(size));
165
+ if (NIL_P(result)) {
166
+ return 0;
167
+ }
168
+ Check_Type(result, T_STRING);
169
+ const ssize_t length = static_cast<ssize_t>(RSTRING_LEN(result));
170
+ if (length <= 0) {
171
+ return 0;
172
+ }
173
+ std::memcpy(buffer, RSTRING_PTR(result), static_cast<size_t>(length));
174
+ return length;
175
+ }
176
+
177
+ VALUE result = rb_funcall(_ruby_stream, rb_id_read_part_method, 1, SIZET2NUM(size));
178
+ if (NIL_P(result)) {
179
+ return 0;
180
+ }
181
+ Check_Type(result, T_STRING);
182
+ const ssize_t length = static_cast<ssize_t>(RSTRING_LEN(result));
183
+ if (length <= 0) {
184
+ return 0;
185
+ }
186
+ std::memcpy(buffer, RSTRING_PTR(result), static_cast<size_t>(length));
187
+ return length;
188
+ }
189
+
190
+ int64_t seek_within_single_part(int64_t offset, int whence) override {
191
+ if (_has_open_part_io && _active_io != Qnil && _active_io_seekable) {
192
+ VALUE result = rb_funcall(_active_io, rb_id_seek_method, 2, LL2NUM(offset), INT2NUM(whence));
193
+ return NUM2LL(result);
194
+ }
195
+
196
+ if (!_has_seek) {
197
+ return -1;
198
+ }
199
+ VALUE result = rb_funcall(_ruby_stream, rb_id_seek_part_method, 2, LL2NUM(offset), INT2NUM(whence));
200
+ return NUM2LL(result);
201
+ }
202
+
203
+ int64_t size_of_single_part(const PathHierarchy &single_part) override {
204
+ if (_has_open_part_io && _active_io != Qnil) {
205
+ if (_active_io_has_size) {
206
+ VALUE result = rb_funcall(_active_io, rb_id_size_method, 0);
207
+ return NUM2LL(result);
208
+ }
209
+ if (_active_io_seekable && _active_io_tellable) {
210
+ VALUE current = rb_funcall(_active_io, rb_id_tell_method, 0);
211
+ rb_funcall(_active_io, rb_id_seek_method, 2, LL2NUM(0), INT2NUM(SEEK_END));
212
+ VALUE end_pos = rb_funcall(_active_io, rb_id_tell_method, 0);
213
+ rb_funcall(_active_io, rb_id_seek_method, 2, current, INT2NUM(SEEK_SET));
214
+ return NUM2LL(end_pos);
215
+ }
216
+ }
217
+
218
+ if (!_has_size) {
219
+ return -1;
220
+ }
221
+ VALUE arg = path_hierarchy_to_rb(single_part);
222
+ VALUE result = rb_funcall(_ruby_stream, rb_id_part_size_method, 1, arg);
223
+ return NUM2LL(result);
224
+ }
225
+
226
+ private:
227
+ static bool determine_seek_support(VALUE ruby_stream, const std::optional<bool> &override_flag) {
228
+ if (override_flag.has_value()) {
229
+ return *override_flag;
230
+ }
231
+ if (rb_respond_to(ruby_stream, rb_id_seek_part_method)) {
232
+ return true;
233
+ }
234
+ if (rb_respond_to(ruby_stream, rb_id_open_part_io_method)) {
235
+ return true;
236
+ }
237
+ return false;
238
+ }
239
+
240
+ static void ensure_required_methods(VALUE ruby_stream) {
241
+ if (!rb_respond_to(ruby_stream, rb_id_open_part_method) && !rb_respond_to(ruby_stream, rb_id_open_part_io_method)) {
242
+ rb_raise(rb_eNotImpError, "Stream subclasses must implement #open_part_io or #open_part");
243
+ }
244
+ if (!rb_respond_to(ruby_stream, rb_id_read_part_method) && !rb_respond_to(ruby_stream, rb_id_open_part_io_method)) {
245
+ rb_raise(rb_eNotImpError, "Stream subclasses must implement #open_part_io or #read_part");
246
+ }
247
+ }
248
+
249
+ void activate_io(VALUE io) {
250
+ if (!rb_respond_to(io, rb_id_read_method)) {
251
+ rb_raise(rb_eTypeError, "open_part_io must return an object responding to #read");
252
+ }
253
+ release_active_io();
254
+ _active_io = io;
255
+ rb_gc_register_address(&_active_io);
256
+ _active_io_seekable = rb_respond_to(io, rb_id_seek_method);
257
+ _active_io_tellable = rb_respond_to(io, rb_id_tell_method);
258
+ _active_io_has_close = rb_respond_to(io, rb_id_close_method);
259
+ _active_io_has_size = rb_respond_to(io, rb_id_size_method);
260
+ }
261
+
262
+ void release_active_io() {
263
+ if (_active_io == Qnil) {
264
+ return;
265
+ }
266
+ if (_active_io_has_close) {
267
+ rb_funcall(_active_io, rb_id_close_method, 0);
268
+ }
269
+ rb_gc_unregister_address(&_active_io);
270
+ _active_io = Qnil;
271
+ _active_io_seekable = false;
272
+ _active_io_tellable = false;
273
+ _active_io_has_close = false;
274
+ _active_io_has_size = false;
275
+ }
276
+
277
+ VALUE _ruby_stream;
278
+ bool _has_close;
279
+ bool _has_seek;
280
+ bool _has_size;
281
+ bool _has_open_part_io;
282
+ bool _has_close_part_io;
283
+ VALUE _active_io;
284
+ bool _active_io_seekable;
285
+ bool _active_io_tellable;
286
+ bool _active_io_has_close;
287
+ bool _active_io_has_size;
288
+ };
289
+
290
+ struct RubyStreamWrapper {
291
+ std::shared_ptr<RubyUserStream> stream;
292
+ };
293
+
294
+ static void stream_wrapper_free(void *ptr) {
295
+ auto *wrapper = static_cast<RubyStreamWrapper *>(ptr);
296
+ delete wrapper;
297
+ }
298
+
299
+ static size_t stream_wrapper_memsize(const void *ptr) {
300
+ return sizeof(RubyStreamWrapper);
301
+ }
302
+
303
+ static const rb_data_type_t stream_wrapper_type = {
304
+ "ArchiveR::Stream",
305
+ {
306
+ nullptr,
307
+ stream_wrapper_free,
308
+ stream_wrapper_memsize,
309
+ },
310
+ nullptr,
311
+ nullptr,
312
+ RUBY_TYPED_FREE_IMMEDIATELY,
313
+ };
314
+
315
+ static RubyStreamWrapper *get_stream_wrapper(VALUE self) {
316
+ RubyStreamWrapper *wrapper = nullptr;
317
+ TypedData_Get_Struct(self, RubyStreamWrapper, &stream_wrapper_type, wrapper);
318
+ if (!wrapper) {
319
+ rb_raise(rb_eRuntimeError, "invalid Stream wrapper");
320
+ }
321
+ return wrapper;
322
+ }
323
+
324
+ static VALUE stream_allocate(VALUE klass) {
325
+ auto *wrapper = new RubyStreamWrapper();
326
+ wrapper->stream.reset();
327
+ return TypedData_Wrap_Struct(klass, &stream_wrapper_type, wrapper);
328
+ }
329
+
330
+ static VALUE stream_initialize(int argc, VALUE *argv, VALUE self) {
331
+ RubyStreamWrapper *wrapper = get_stream_wrapper(self);
332
+ if (wrapper->stream) {
333
+ rb_raise(rb_eRuntimeError, "Stream already initialized");
334
+ }
335
+ VALUE hierarchy_value = Qnil;
336
+ VALUE opts = Qnil;
337
+ rb_scan_args(argc, argv, "11", &hierarchy_value, &opts);
338
+ PathHierarchy hierarchy = rb_value_to_path_hierarchy(hierarchy_value);
339
+ std::optional<bool> seekable_override;
340
+ if (!NIL_P(opts)) {
341
+ Check_Type(opts, T_HASH);
342
+ static ID id_seekable = rb_intern("seekable");
343
+ VALUE seekable_value = rb_hash_aref(opts, ID2SYM(id_seekable));
344
+ if (!NIL_P(seekable_value)) {
345
+ seekable_override = RTEST(seekable_value);
346
+ }
347
+ }
348
+ wrapper->stream = std::make_shared<RubyUserStream>(self, std::move(hierarchy), seekable_override);
349
+ return self;
350
+ }
351
+
352
+ static VALUE entry_fault_to_rb(const EntryFault &fault) {
353
+ VALUE hash = rb_hash_new();
354
+ static ID id_message = rb_intern("message");
355
+ static ID id_errno = rb_intern("errno");
356
+ static ID id_hierarchy = rb_intern("hierarchy");
357
+ static ID id_path = rb_intern("path");
358
+
359
+ rb_hash_aset(hash, ID2SYM(id_message), cpp_string_to_rb(fault.message));
360
+ rb_hash_aset(hash, ID2SYM(id_errno), INT2NUM(fault.errno_value));
361
+ rb_hash_aset(hash, ID2SYM(id_hierarchy), path_hierarchy_to_rb(fault.hierarchy));
362
+ std::string path_string = fault.hierarchy.empty() ? std::string() : hierarchy_display(fault.hierarchy);
363
+ rb_hash_aset(hash, ID2SYM(id_path), cpp_string_to_rb(path_string));
364
+ return hash;
365
+ }
366
+
367
+ static FaultCallback make_ruby_fault_callback(VALUE callable) {
368
+ if (NIL_P(callable)) {
369
+ return {};
370
+ }
371
+
372
+ static ID id_call = rb_intern("call");
373
+ if (!rb_respond_to(callable, id_call)) {
374
+ rb_raise(rb_eTypeError, "fault callback must respond to #call");
375
+ }
376
+
377
+ auto holder = std::make_shared<RubyCallbackHolder>(callable);
378
+
379
+ return [holder](const EntryFault &fault) {
380
+ struct InvokePayload {
381
+ std::shared_ptr<RubyCallbackHolder> holder;
382
+ VALUE fault_hash;
383
+ } payload{ holder, entry_fault_to_rb(fault) };
384
+
385
+ auto invoke = [](VALUE data) -> VALUE {
386
+ auto *info = reinterpret_cast<InvokePayload *>(data);
387
+ static ID id_call_inner = rb_intern("call");
388
+ return rb_funcall(info->holder->proc_value, id_call_inner, 1, info->fault_hash);
389
+ };
390
+
391
+ int state = 0;
392
+ rb_protect(invoke, reinterpret_cast<VALUE>(&payload), &state);
393
+ if (state != 0) {
394
+ rb_jump_tag(state);
395
+ }
396
+ return;
397
+ };
398
+ }
399
+
400
+ // Helper: Convert Ruby hash options into TraverserOptions
401
+ static void populate_traverser_options(VALUE opts, TraverserOptions &options) {
402
+ if (NIL_P(opts)) {
403
+ return;
404
+ }
405
+
406
+ Check_Type(opts, T_HASH);
407
+
408
+ static ID id_passphrases = rb_intern("passphrases");
409
+ static ID id_formats = rb_intern("formats");
410
+ static ID id_metadata_keys = rb_intern("metadata_keys");
411
+ static ID id_descend_archives = rb_intern("descend_archives");
412
+
413
+ VALUE passphrases_val = rb_hash_aref(opts, ID2SYM(id_passphrases));
414
+ if (!NIL_P(passphrases_val)) {
415
+ Check_Type(passphrases_val, T_ARRAY);
416
+ long len = RARRAY_LEN(passphrases_val);
417
+ options.passphrases.reserve(len);
418
+ for (long i = 0; i < len; ++i) {
419
+ VALUE item = rb_ary_entry(passphrases_val, i);
420
+ options.passphrases.push_back(rb_string_to_cpp(StringValue(item)));
421
+ }
422
+ }
423
+
424
+ VALUE formats_val = rb_hash_aref(opts, ID2SYM(id_formats));
425
+ if (!NIL_P(formats_val)) {
426
+ Check_Type(formats_val, T_ARRAY);
427
+ long len = RARRAY_LEN(formats_val);
428
+ options.formats.reserve(len);
429
+ for (long i = 0; i < len; ++i) {
430
+ VALUE item = rb_ary_entry(formats_val, i);
431
+ options.formats.push_back(rb_string_to_cpp(StringValue(item)));
432
+ }
433
+ }
434
+
435
+ VALUE metadata_val = rb_hash_aref(opts, ID2SYM(id_metadata_keys));
436
+ if (!NIL_P(metadata_val)) {
437
+ Check_Type(metadata_val, T_ARRAY);
438
+ long len = RARRAY_LEN(metadata_val);
439
+ options.metadata_keys.reserve(len);
440
+ for (long i = 0; i < len; ++i) {
441
+ VALUE item = rb_ary_entry(metadata_val, i);
442
+ options.metadata_keys.push_back(rb_string_to_cpp(StringValue(item)));
443
+ }
444
+ }
445
+
446
+ VALUE descend_val = rb_hash_aref(opts, ID2SYM(id_descend_archives));
447
+ if (!NIL_P(descend_val)) {
448
+ options.descend_archives = RTEST(descend_val);
449
+ }
450
+ }
451
+
452
+ static PathEntry rb_value_to_path_entry(VALUE value);
453
+
454
+ static PathHierarchy rb_value_to_path_hierarchy(VALUE value) {
455
+ if (RB_TYPE_P(value, T_STRING)) {
456
+ return make_single_path(rb_string_to_cpp(value));
457
+ }
458
+
459
+ VALUE array = rb_check_array_type(value);
460
+ if (NIL_P(array)) {
461
+ rb_raise(rb_eTypeError, "path hierarchy must be a String or Array");
462
+ }
463
+
464
+ const long length = RARRAY_LEN(array);
465
+ if (length == 0) {
466
+ rb_raise(rb_eArgError, "path hierarchy cannot be empty");
467
+ }
468
+
469
+ PathHierarchy hierarchy;
470
+ hierarchy.reserve(static_cast<size_t>(length));
471
+ for (long i = 0; i < length; ++i) {
472
+ VALUE component = rb_ary_entry(array, i);
473
+ hierarchy.emplace_back(rb_value_to_path_entry(component));
474
+ }
475
+
476
+ return hierarchy;
477
+ }
478
+
479
+ static PathEntry rb_value_to_path_entry(VALUE value) {
480
+ if (RB_TYPE_P(value, T_STRING)) {
481
+ return PathEntry::single(rb_string_to_cpp(value));
482
+ }
483
+
484
+ VALUE array = rb_check_array_type(value);
485
+ if (NIL_P(array)) {
486
+ rb_raise(rb_eTypeError, "PathEntry must be String or Array");
487
+ }
488
+
489
+ const long length = RARRAY_LEN(array);
490
+ if (length == 0) {
491
+ rb_raise(rb_eArgError, "PathEntry array cannot be empty");
492
+ }
493
+
494
+ bool all_strings = true;
495
+ for (long i = 0; i < length; ++i) {
496
+ VALUE element = rb_ary_entry(array, i);
497
+ if (!RB_TYPE_P(element, T_STRING)) {
498
+ all_strings = false;
499
+ break;
500
+ }
501
+ }
502
+
503
+ if (all_strings) {
504
+ std::vector<std::string> parts;
505
+ parts.reserve(static_cast<size_t>(length));
506
+ for (long i = 0; i < length; ++i) {
507
+ parts.emplace_back(rb_string_to_cpp(rb_ary_entry(array, i)));
508
+ }
509
+ return PathEntry::multi_volume(std::move(parts));
510
+ }
511
+
512
+ PathEntry::NodeList nodes;
513
+ nodes.reserve(static_cast<size_t>(length));
514
+ for (long i = 0; i < length; ++i) {
515
+ nodes.emplace_back(rb_value_to_path_entry(rb_ary_entry(array, i)));
516
+ }
517
+ return PathEntry::nested(std::move(nodes));
518
+ }
519
+
520
+ // Helper: Convert Ruby path argument into vector of PathHierarchy
521
+ static std::vector<PathHierarchy> rb_paths_to_hierarchies(VALUE paths) {
522
+ if (RB_TYPE_P(paths, T_STRING)) {
523
+ std::vector<PathHierarchy> result;
524
+ result.emplace_back(make_single_path(rb_string_to_cpp(paths)));
525
+ return result;
526
+ }
527
+
528
+ VALUE array = rb_check_array_type(paths);
529
+ if (NIL_P(array)) {
530
+ rb_raise(rb_eTypeError, "paths must be a String or an Array");
531
+ }
532
+
533
+ const long length = RARRAY_LEN(array);
534
+ if (length == 0) {
535
+ rb_raise(rb_eArgError, "paths cannot be empty");
536
+ }
537
+
538
+ std::vector<PathHierarchy> result;
539
+ result.reserve(static_cast<size_t>(length));
540
+ for (long i = 0; i < length; ++i) {
541
+ VALUE item = rb_ary_entry(array, i);
542
+ result.emplace_back(rb_value_to_path_hierarchy(item));
543
+ }
544
+
545
+ return result;
546
+ }
547
+
548
+ static VALUE invoke_ruby_stream_factory(const std::shared_ptr<RubyCallbackHolder> &holder, const PathHierarchy &hierarchy) {
549
+ struct FactoryPayload {
550
+ std::shared_ptr<RubyCallbackHolder> holder;
551
+ VALUE arg;
552
+ } payload{ holder, path_hierarchy_to_rb(hierarchy) };
553
+
554
+ auto invoke = [](VALUE data) -> VALUE {
555
+ auto *info = reinterpret_cast<FactoryPayload *>(data);
556
+ return rb_funcall(info->holder->proc_value, rb_id_call_method, 1, info->arg);
557
+ };
558
+
559
+ int state = 0;
560
+ VALUE result = rb_protect(invoke, reinterpret_cast<VALUE>(&payload), &state);
561
+ if (state != 0) {
562
+ rb_jump_tag(state);
563
+ }
564
+ return result;
565
+ }
566
+
567
+ static std::shared_ptr<IDataStream> stream_from_ruby_value(VALUE value, const PathHierarchy &requested) {
568
+ if (NIL_P(value)) {
569
+ return nullptr;
570
+ }
571
+
572
+ if (rb_obj_is_kind_of(value, cStream)) {
573
+ RubyStreamWrapper *wrapper = get_stream_wrapper(value);
574
+ if (!wrapper->stream) {
575
+ rb_raise(rb_eRuntimeError, "Stream instance is not initialized");
576
+ }
577
+ if (!hierarchies_equal(wrapper->stream->source_hierarchy(), requested)) {
578
+ rb_raise(rb_eArgError, "Stream instance must be created with the same hierarchy passed to the factory");
579
+ }
580
+ return wrapper->stream;
581
+ }
582
+
583
+ rb_raise(rb_eTypeError, "stream factory must return nil or an Archive_r::Stream instance");
584
+ }
585
+
586
+ // Helper: Convert EntryMetadataValue to Ruby object
587
+ static VALUE metadata_value_to_rb(const EntryMetadataValue &value) {
588
+ struct Visitor {
589
+ VALUE
590
+ operator()(std::monostate) const { return Qnil; }
591
+ VALUE
592
+ operator()(bool v) const { return v ? Qtrue : Qfalse; }
593
+ VALUE
594
+ operator()(int64_t v) const { return LL2NUM(v); }
595
+ VALUE
596
+ operator()(uint64_t v) const { return ULL2NUM(v); }
597
+ VALUE
598
+ operator()(const std::string & v) const { return cpp_string_to_rb(v); }
599
+ VALUE
600
+ operator()(const std::vector<uint8_t> &v) const {
601
+ if (v.empty()) {
602
+ return rb_str_new(nullptr, 0);
603
+ }
604
+ return rb_str_new(reinterpret_cast<const char *>(v.data()), v.size());
605
+ }
606
+ VALUE
607
+ operator()(const EntryMetadataTime & v) const {
608
+ VALUE hash = rb_hash_new();
609
+ rb_hash_aset(hash, ID2SYM(rb_intern("seconds")), LL2NUM(v.seconds));
610
+ rb_hash_aset(hash, ID2SYM(rb_intern("nanoseconds")), INT2NUM(v.nanoseconds));
611
+ return hash;
612
+ }
613
+ VALUE
614
+ operator()(const EntryMetadataDeviceNumbers & v) const {
615
+ VALUE hash = rb_hash_new();
616
+ rb_hash_aset(hash, ID2SYM(rb_intern("major")), ULL2NUM(v.major));
617
+ rb_hash_aset(hash, ID2SYM(rb_intern("minor")), ULL2NUM(v.minor));
618
+ return hash;
619
+ }
620
+ VALUE
621
+ operator()(const EntryMetadataFileFlags & v) const {
622
+ VALUE hash = rb_hash_new();
623
+ rb_hash_aset(hash, ID2SYM(rb_intern("set")), ULL2NUM(v.set));
624
+ rb_hash_aset(hash, ID2SYM(rb_intern("clear")), ULL2NUM(v.clear));
625
+ return hash;
626
+ }
627
+ VALUE
628
+ operator()(const std::vector<EntryMetadataXattr> &vec) const {
629
+ VALUE array = rb_ary_new_capa(vec.size());
630
+ for (const auto &item : vec) {
631
+ VALUE hash = rb_hash_new();
632
+ rb_hash_aset(hash, ID2SYM(rb_intern("name")), cpp_string_to_rb(item.name));
633
+ if (item.value.empty()) {
634
+ rb_hash_aset(hash, ID2SYM(rb_intern("value")), rb_str_new(nullptr, 0));
635
+ } else {
636
+ rb_hash_aset(hash, ID2SYM(rb_intern("value")), rb_str_new(reinterpret_cast<const char *>(item.value.data()), item.value.size()));
637
+ }
638
+ rb_ary_push(array, hash);
639
+ }
640
+ return array;
641
+ }
642
+ VALUE
643
+ operator()(const std::vector<EntryMetadataSparseChunk> &vec) const {
644
+ VALUE array = rb_ary_new_capa(vec.size());
645
+ for (const auto &item : vec) {
646
+ VALUE hash = rb_hash_new();
647
+ rb_hash_aset(hash, ID2SYM(rb_intern("offset")), LL2NUM(item.offset));
648
+ rb_hash_aset(hash, ID2SYM(rb_intern("length")), LL2NUM(item.length));
649
+ rb_ary_push(array, hash);
650
+ }
651
+ return array;
652
+ }
653
+ VALUE
654
+ operator()(const std::vector<EntryMetadataDigest> &vec) const {
655
+ VALUE array = rb_ary_new_capa(vec.size());
656
+ for (const auto &item : vec) {
657
+ VALUE hash = rb_hash_new();
658
+ rb_hash_aset(hash, ID2SYM(rb_intern("algorithm")), cpp_string_to_rb(item.algorithm));
659
+ if (item.value.empty()) {
660
+ rb_hash_aset(hash, ID2SYM(rb_intern("value")), rb_str_new(nullptr, 0));
661
+ } else {
662
+ rb_hash_aset(hash, ID2SYM(rb_intern("value")), rb_str_new(reinterpret_cast<const char *>(item.value.data()), item.value.size()));
663
+ }
664
+ rb_ary_push(array, hash);
665
+ }
666
+ return array;
667
+ }
668
+ } visitor;
669
+
670
+ return std::visit(visitor, value);
671
+ }
672
+
673
+ static VALUE archive_r_register_stream_factory(int argc, VALUE *argv, VALUE self) {
674
+ VALUE callable = Qnil;
675
+ rb_scan_args(argc, argv, "01", &callable);
676
+
677
+ if (!NIL_P(callable) && rb_block_given_p()) {
678
+ rb_raise(rb_eArgError, "provide callable argument or block, not both");
679
+ }
680
+
681
+ if (NIL_P(callable) && rb_block_given_p()) {
682
+ callable = rb_block_proc();
683
+ }
684
+
685
+ if (NIL_P(callable)) {
686
+ set_root_stream_factory(RootStreamFactory{});
687
+ g_stream_factory_callback.reset();
688
+ return Qnil;
689
+ }
690
+
691
+ if (!rb_respond_to(callable, rb_id_call_method)) {
692
+ rb_raise(rb_eTypeError, "stream factory must respond to #call");
693
+ }
694
+
695
+ auto holder = std::make_shared<RubyCallbackHolder>(callable);
696
+
697
+ RootStreamFactory factory = [holder](const PathHierarchy &hierarchy) -> std::shared_ptr<IDataStream> {
698
+ VALUE result = invoke_ruby_stream_factory(holder, hierarchy);
699
+ return stream_from_ruby_value(result, hierarchy);
700
+ };
701
+
702
+ set_root_stream_factory(factory);
703
+ g_stream_factory_callback = holder;
704
+ return Qnil;
705
+ }
706
+
707
+ static VALUE archive_r_on_fault(int argc, VALUE *argv, VALUE self) {
708
+ VALUE callback = Qnil;
709
+ rb_scan_args(argc, argv, "01", &callback);
710
+
711
+ if (!NIL_P(callback) && rb_block_given_p()) {
712
+ rb_raise(rb_eArgError, "provide callable argument or block, not both");
713
+ }
714
+
715
+ if (NIL_P(callback) && rb_block_given_p()) {
716
+ callback = rb_block_proc();
717
+ }
718
+
719
+ FaultCallback cb = make_ruby_fault_callback(callback);
720
+ register_fault_callback(std::move(cb));
721
+ return self;
722
+ }
723
+
724
+ //=============================================================================
725
+ // Entry class
726
+ //=============================================================================
727
+
728
+ // EntryWrapper: references an Entry owned by Traverser iterator
729
+ struct EntryWrapper {
730
+ Entry *entry_ref;
731
+ std::unique_ptr<Entry> entry_copy;
732
+
733
+ EntryWrapper(Entry *ref, std::unique_ptr<Entry> copy)
734
+ : entry_ref(ref)
735
+ , entry_copy(std::move(copy)) {}
736
+ };
737
+
738
+ // Free function for EntryWrapper (wrapper only, Entry owned elsewhere)
739
+ static void entry_free(void *ptr) { delete static_cast<EntryWrapper *>(ptr); }
740
+
741
+ // Wrap Entry pointer in EntryWrapper (does not copy Entry)
742
+ static VALUE entry_wrap(Entry &entry) {
743
+ std::unique_ptr<Entry> copy(new Entry(entry));
744
+ EntryWrapper *wrapper = new EntryWrapper(&entry, std::move(copy));
745
+ return Data_Wrap_Struct(cEntry, nullptr, entry_free, wrapper);
746
+ }
747
+
748
+ static VALUE entry_invalidate(VALUE entry_obj) {
749
+ EntryWrapper *wrapper;
750
+ Data_Get_Struct(entry_obj, EntryWrapper, wrapper);
751
+ if (wrapper) {
752
+ wrapper->entry_ref = nullptr;
753
+ }
754
+ return Qnil;
755
+ }
756
+
757
+ static EntryWrapper *entry_get_wrapper(VALUE self) {
758
+ EntryWrapper *wrapper;
759
+ Data_Get_Struct(self, EntryWrapper, wrapper);
760
+ if (!wrapper) {
761
+ rb_raise(rb_eRuntimeError, "Invalid Entry handle");
762
+ }
763
+ return wrapper;
764
+ }
765
+
766
+ // Helper to fetch Entry for read operations, falling back to preserved copy
767
+ static Entry *entry_for_read(VALUE self) {
768
+ EntryWrapper *wrapper = entry_get_wrapper(self);
769
+ if (wrapper->entry_ref) {
770
+ return wrapper->entry_ref;
771
+ }
772
+ if (wrapper->entry_copy) {
773
+ return wrapper->entry_copy.get();
774
+ }
775
+ rb_raise(rb_eRuntimeError, "Entry data is no longer available");
776
+ return nullptr;
777
+ }
778
+
779
+ // Helper to fetch live Entry pointer required for mutating operations
780
+ static Entry *entry_for_live(VALUE self) {
781
+ EntryWrapper *wrapper = entry_get_wrapper(self);
782
+ if (!wrapper->entry_ref) {
783
+ rb_raise(rb_eRuntimeError, "Entry is no longer valid");
784
+ }
785
+ return wrapper->entry_ref;
786
+ }
787
+
788
+ // Entry#path -> String
789
+ static VALUE entry_path(VALUE self) {
790
+ Entry *entry = entry_for_read(self);
791
+ const std::string entry_path_str = entry->path();
792
+ return cpp_string_to_rb(entry->path());
793
+ }
794
+
795
+ // Entry#path_hierarchy -> Array
796
+ static VALUE entry_path_hierarchy(VALUE self) {
797
+ Entry *entry = entry_for_read(self);
798
+ return path_hierarchy_to_rb(entry->path_hierarchy());
799
+ }
800
+
801
+ // Entry#name -> String
802
+ static VALUE entry_name(VALUE self) {
803
+ Entry *entry = entry_for_read(self);
804
+ return cpp_string_to_rb(entry->name());
805
+ }
806
+
807
+ // Entry#size -> Integer
808
+ static VALUE entry_size(VALUE self) {
809
+ Entry *entry = entry_for_read(self);
810
+ return LONG2NUM(entry->size());
811
+ }
812
+
813
+ // Entry#file? -> Boolean
814
+ static VALUE entry_is_file(VALUE self) {
815
+ Entry *entry = entry_for_read(self);
816
+ return entry->is_file() ? Qtrue : Qfalse;
817
+ }
818
+
819
+ // Entry#directory? -> Boolean
820
+ static VALUE entry_is_directory(VALUE self) {
821
+ Entry *entry = entry_for_read(self);
822
+ return entry->is_directory() ? Qtrue : Qfalse;
823
+ }
824
+
825
+ // Entry#depth -> Integer
826
+ static VALUE entry_depth(VALUE self) {
827
+ Entry *entry = entry_for_read(self);
828
+ return INT2NUM(entry->depth());
829
+ }
830
+
831
+ // Entry#set_descent(enabled) -> self
832
+ static VALUE entry_set_descent(VALUE self, VALUE enabled) {
833
+ Entry *entry = entry_for_live(self);
834
+ entry->set_descent(RTEST(enabled));
835
+ return self;
836
+ }
837
+
838
+ // Entry#set_multi_volume_group(base_name, order: :natural) -> nil
839
+ static VALUE entry_set_multi_volume_group(int argc, VALUE *argv, VALUE self) {
840
+ VALUE base_name_val;
841
+ VALUE options_val = Qnil;
842
+
843
+ rb_scan_args(argc, argv, "11", &base_name_val, &options_val);
844
+
845
+ MultiVolumeGroupOptions options;
846
+ if (!NIL_P(options_val)) {
847
+ VALUE hash = rb_check_hash_type(options_val);
848
+ if (NIL_P(hash)) {
849
+ rb_raise(rb_eTypeError, "options must be a Hash");
850
+ }
851
+
852
+ static ID id_order = rb_intern("order");
853
+ VALUE order_val = rb_hash_aref(hash, ID2SYM(id_order));
854
+ if (!NIL_P(order_val)) {
855
+ std::string order_str;
856
+ if (SYMBOL_P(order_val)) {
857
+ order_str = rb_id2name(SYM2ID(order_val));
858
+ } else {
859
+ order_str = rb_string_to_cpp(StringValue(order_val));
860
+ }
861
+ for (char &ch : order_str) {
862
+ ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
863
+ }
864
+ if (order_str == "given") {
865
+ options.ordering = PathEntry::Parts::Ordering::Given;
866
+ } else if (order_str == "natural") {
867
+ options.ordering = PathEntry::Parts::Ordering::Natural;
868
+ } else {
869
+ rb_raise(rb_eArgError, "order must be :natural or :given");
870
+ }
871
+ }
872
+ }
873
+
874
+ Entry *entry = entry_for_live(self);
875
+ try {
876
+ entry->set_multi_volume_group(rb_string_to_cpp(StringValue(base_name_val)), options);
877
+ } catch (const std::exception &e) {
878
+ rb_raise(rb_eRuntimeError, "Failed to set multi-volume group: %s", e.what());
879
+ }
880
+ return Qnil;
881
+ }
882
+
883
+ // Entry#read(length = nil) -> String
884
+ static VALUE entry_read(int argc, VALUE *argv, VALUE self) {
885
+ VALUE length_val = Qnil;
886
+ rb_scan_args(argc, argv, "01", &length_val);
887
+
888
+ bool bounded_read = false;
889
+ size_t requested_size = 0;
890
+ if (!NIL_P(length_val)) {
891
+ long long length_long = NUM2LL(length_val);
892
+ if (length_long == 0) {
893
+ return rb_str_new("", 0);
894
+ } else if (length_long > 0) {
895
+ const auto max_allowed = std::numeric_limits<size_t>::max();
896
+ if (static_cast<unsigned long long>(length_long) > max_allowed) {
897
+ rb_raise(rb_eRangeError, "requested length exceeds platform limits");
898
+ }
899
+ requested_size = static_cast<size_t>(length_long);
900
+ bounded_read = true;
901
+ }
902
+ // Negative values fall through to the streaming path (full read)
903
+ }
904
+
905
+ Entry *entry = entry_for_read(self);
906
+ const std::string entry_path_str = entry->path();
907
+
908
+ try {
909
+ if (bounded_read) {
910
+ std::vector<uint8_t> buffer(requested_size);
911
+ const ssize_t bytes_read = entry->read(buffer.data(), buffer.size());
912
+ if (bytes_read < 0) {
913
+ rb_raise(rb_eRuntimeError, "Failed to read entry payload at %s", entry_path_str.c_str());
914
+ }
915
+ return rb_str_new(reinterpret_cast<const char *>(buffer.data()), static_cast<long>(bytes_read));
916
+ }
917
+
918
+ std::string aggregate;
919
+ const uint64_t reported_size = entry->size();
920
+ if (reported_size > 0 && reported_size <= static_cast<uint64_t>(std::numeric_limits<size_t>::max())) {
921
+ aggregate.reserve(static_cast<size_t>(reported_size));
922
+ }
923
+
924
+ std::vector<uint8_t> chunk(64 * 1024);
925
+ while (true) {
926
+ const ssize_t bytes_read = entry->read(chunk.data(), chunk.size());
927
+ if (bytes_read < 0) {
928
+ rb_raise(rb_eRuntimeError, "Failed to read entry payload at %s", entry_path_str.c_str());
929
+ }
930
+ if (bytes_read == 0) {
931
+ break;
932
+ }
933
+ aggregate.append(reinterpret_cast<const char *>(chunk.data()), static_cast<size_t>(bytes_read));
934
+ }
935
+
936
+ return rb_str_new(aggregate.data(), static_cast<long>(aggregate.size()));
937
+ } catch (const std::exception &e) {
938
+ rb_raise(rb_eRuntimeError, "Failed to read entry at %s: %s", entry_path_str.c_str(), e.what());
939
+ return Qnil;
940
+ }
941
+ }
942
+
943
+ // Entry#metadata -> Hash
944
+ static VALUE entry_metadata(VALUE self) {
945
+ Entry *entry = entry_for_read(self);
946
+ VALUE hash = rb_hash_new();
947
+ const EntryMetadataMap &metadata = entry->metadata();
948
+ for (const auto &kv : metadata) {
949
+ rb_hash_aset(hash, cpp_string_to_rb(kv.first), metadata_value_to_rb(kv.second));
950
+ }
951
+ return hash;
952
+ }
953
+
954
+ // Entry#metadata_value(key) -> Object or nil
955
+ static VALUE entry_metadata_value(VALUE self, VALUE key) {
956
+ Entry *entry = entry_for_read(self);
957
+ std::string key_str = rb_string_to_cpp(StringValue(key));
958
+ const EntryMetadataValue *value = entry->find_metadata(key_str);
959
+ if (!value) {
960
+ return Qnil;
961
+ }
962
+ return metadata_value_to_rb(*value);
963
+ }
964
+
965
+ //=============================================================================
966
+ // Traverser class
967
+ //=============================================================================
968
+
969
+ // Free function for Traverser
970
+ static void traverser_free(void *ptr) { delete static_cast<Traverser *>(ptr); }
971
+
972
+ // Wrap Traverser pointer
973
+ static VALUE traverser_wrap(Traverser *traverser) { return Data_Wrap_Struct(cTraverser, nullptr, traverser_free, traverser); }
974
+
975
+ // Get Traverser pointer from Ruby object
976
+ static Traverser *traverser_unwrap(VALUE self) {
977
+ Traverser *traverser;
978
+ Data_Get_Struct(self, Traverser, traverser);
979
+ return traverser;
980
+ }
981
+
982
+ // Traverser allocation
983
+ static VALUE traverser_allocate(VALUE klass) { return Data_Wrap_Struct(klass, nullptr, traverser_free, nullptr); }
984
+
985
+ // Traverser.new(paths, passphrases: [], formats: [], metadata_keys: []) -> Traverser
986
+ static VALUE traverser_initialize(int argc, VALUE *argv, VALUE self) {
987
+ VALUE paths;
988
+ VALUE opts = Qnil;
989
+ rb_scan_args(argc, argv, "11", &paths, &opts);
990
+
991
+ try {
992
+ std::vector<PathHierarchy> path_list = rb_paths_to_hierarchies(paths);
993
+ TraverserOptions options;
994
+ populate_traverser_options(opts, options);
995
+ Traverser *traverser = new Traverser(std::move(path_list), options);
996
+ DATA_PTR(self) = traverser;
997
+ return self;
998
+ } catch (const std::exception &e) {
999
+ rb_raise(rb_eRuntimeError, "Failed to open archive: %s", e.what());
1000
+ return Qnil;
1001
+ }
1002
+ }
1003
+
1004
+ // Traverser#each { |entry| ... } -> Enumerator
1005
+ static VALUE yield_entry(VALUE entry_obj) {
1006
+ rb_yield(entry_obj);
1007
+ return Qnil;
1008
+ }
1009
+
1010
+ static VALUE traverser_each(VALUE self) {
1011
+ Traverser *traverser = traverser_unwrap(self);
1012
+
1013
+ // If no block given, return Enumerator
1014
+ if (!rb_block_given_p()) {
1015
+ return rb_funcall(self, rb_intern("to_enum"), 1, ID2SYM(rb_intern("each")));
1016
+ }
1017
+
1018
+ try {
1019
+ for (auto it = traverser->begin(); it != traverser->end(); ++it) {
1020
+ Entry &entry = *it;
1021
+ VALUE rb_entry = entry_wrap(entry);
1022
+ rb_ensure(yield_entry, rb_entry, entry_invalidate, rb_entry);
1023
+ }
1024
+ } catch (const std::exception &e) {
1025
+ rb_raise(rb_eRuntimeError, "Error during traversal: %s", e.what());
1026
+ }
1027
+
1028
+ return self;
1029
+ }
1030
+
1031
+ // Helper for Traverser.open cleanup
1032
+ static VALUE traverser_close_helper(VALUE arg) {
1033
+ // Nothing to do - Traverser cleanup is automatic
1034
+ return Qnil;
1035
+ }
1036
+
1037
+ // Traverser.open(path, opts = {}) { |traverser| ... } -> result of block
1038
+ static VALUE traverser_s_open(int argc, VALUE *argv, VALUE klass) {
1039
+ VALUE traverser = rb_class_new_instance(argc, argv, klass);
1040
+
1041
+ if (rb_block_given_p()) {
1042
+ return rb_ensure(rb_yield, traverser, traverser_close_helper, traverser);
1043
+ }
1044
+
1045
+ return traverser;
1046
+ }
1047
+
1048
+ //=============================================================================
1049
+ // Module initialization
1050
+ //=============================================================================
1051
+
1052
+ extern "C" void Init_archive_r() {
1053
+ // Define module Archive_r
1054
+ mArchive_r = rb_define_module("Archive_r");
1055
+ cStream = rb_define_class_under(mArchive_r, "Stream", rb_cObject);
1056
+ rb_define_alloc_func(cStream, stream_allocate);
1057
+ rb_define_method(cStream, "initialize", RUBY_METHOD_FUNC(stream_initialize), -1);
1058
+
1059
+ rb_id_read_method = rb_intern("read");
1060
+ rb_id_seek_method = rb_intern("seek");
1061
+ rb_id_tell_method = rb_intern("tell");
1062
+ rb_id_eof_method = rb_intern("eof?");
1063
+ rb_id_close_method = rb_intern("close");
1064
+ rb_id_size_method = rb_intern("size");
1065
+ rb_id_call_method = rb_intern("call");
1066
+ rb_id_open_part_method = rb_intern("open_part");
1067
+ rb_id_close_part_method = rb_intern("close_part");
1068
+ rb_id_read_part_method = rb_intern("read_part");
1069
+ rb_id_seek_part_method = rb_intern("seek_part");
1070
+ rb_id_part_size_method = rb_intern("part_size");
1071
+ rb_id_open_part_io_method = rb_intern("open_part_io");
1072
+ rb_id_close_part_io_method = rb_intern("close_part_io");
1073
+
1074
+ // Define Entry class
1075
+ cEntry = rb_define_class_under(mArchive_r, "Entry", rb_cObject);
1076
+ rb_undef_alloc_func(cEntry);
1077
+
1078
+ rb_define_method(cEntry, "path", RUBY_METHOD_FUNC(entry_path), 0);
1079
+ rb_define_method(cEntry, "path_hierarchy", RUBY_METHOD_FUNC(entry_path_hierarchy), 0);
1080
+ rb_define_method(cEntry, "name", RUBY_METHOD_FUNC(entry_name), 0);
1081
+ rb_define_method(cEntry, "size", RUBY_METHOD_FUNC(entry_size), 0);
1082
+ rb_define_method(cEntry, "file?", RUBY_METHOD_FUNC(entry_is_file), 0);
1083
+ rb_define_method(cEntry, "directory?", RUBY_METHOD_FUNC(entry_is_directory), 0);
1084
+ rb_define_method(cEntry, "depth", RUBY_METHOD_FUNC(entry_depth), 0);
1085
+ rb_define_method(cEntry, "set_descent", RUBY_METHOD_FUNC(entry_set_descent), 1);
1086
+ rb_define_method(cEntry, "set_multi_volume_group", RUBY_METHOD_FUNC(entry_set_multi_volume_group), -1);
1087
+ rb_define_method(cEntry, "read", RUBY_METHOD_FUNC(entry_read), -1);
1088
+ rb_define_method(cEntry, "metadata", RUBY_METHOD_FUNC(entry_metadata), 0);
1089
+ rb_define_method(cEntry, "metadata_value", RUBY_METHOD_FUNC(entry_metadata_value), 1);
1090
+
1091
+ // Define Traverser class
1092
+ cTraverser = rb_define_class_under(mArchive_r, "Traverser", rb_cObject);
1093
+
1094
+ rb_define_alloc_func(cTraverser, traverser_allocate);
1095
+ rb_define_method(cTraverser, "initialize", RUBY_METHOD_FUNC(traverser_initialize), -1);
1096
+ rb_define_method(cTraverser, "each", RUBY_METHOD_FUNC(traverser_each), 0);
1097
+ rb_define_singleton_method(cTraverser, "open", RUBY_METHOD_FUNC(traverser_s_open), -1);
1098
+
1099
+ // Make Traverser enumerable
1100
+ rb_include_module(cTraverser, rb_mEnumerable);
1101
+
1102
+ rb_define_module_function(mArchive_r, "on_fault", RUBY_METHOD_FUNC(archive_r_on_fault), -1);
1103
+ rb_define_module_function(mArchive_r, "register_stream_factory", RUBY_METHOD_FUNC(archive_r_register_stream_factory), -1);
1104
+
1105
+ rb_set_end_proc(archive_r_cleanup, Qnil);
1106
+ }