rufio 0.33.0 → 0.34.0

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.
@@ -1,167 +1,378 @@
1
1
  const std = @import("std");
2
2
  const c = @cImport({
3
- @cInclude("ruby.h");
4
3
  @cInclude("dirent.h");
5
4
  @cInclude("sys/stat.h");
6
5
  @cInclude("string.h");
7
- @cInclude("time.h");
6
+ @cInclude("stdlib.h");
7
+ @cInclude("pthread.h");
8
8
  });
9
9
 
10
- /// ディレクトリエントリ情報
10
+ /// スキャン状態
11
+ pub const ScanState = enum(u8) {
12
+ idle = 0,
13
+ scanning = 1,
14
+ done = 2,
15
+ cancelled = 3,
16
+ failed = 4,
17
+ };
18
+
19
+ /// ディレクトリエントリ情報(内部保持用)
11
20
  const DirEntry = struct {
12
- name: []const u8,
21
+ name: []u8,
13
22
  is_dir: bool,
14
23
  size: u64,
15
24
  mtime: i64,
16
25
  executable: bool,
17
26
  hidden: bool,
27
+
28
+ fn deinit(self: *DirEntry) void {
29
+ c.free(self.name.ptr);
30
+ }
18
31
  };
19
32
 
20
- /// ディレクトリをスキャンしてRuby配列を返す
21
- fn scanDirectory(self: c.VALUE, path_value: c.VALUE) callconv(.c) c.VALUE {
22
- _ = self;
23
-
24
- // パス文字列を取得
25
- var mutable_path = path_value;
26
- const path_str = c.rb_string_value_cstr(@ptrCast(&mutable_path));
27
- const path_len = c.strlen(path_str);
28
- const path = path_str[0..path_len];
29
-
30
- // ディレクトリを開く
31
- const dir = c.opendir(path_str) orelse {
32
- c.rb_raise(c.rb_eRuntimeError, "Failed to open directory");
33
- return c.Qnil;
34
- };
35
- defer _ = c.closedir(dir);
36
-
37
- // Ruby配列を作成
38
- const ary = c.rb_ary_new();
39
-
40
- // ディレクトリエントリを読み取り
41
- while (c.readdir(dir)) |entry| {
42
- const name_ptr = @as([*:0]const u8, @ptrCast(&entry.*.d_name));
43
- const name_len = c.strlen(name_ptr);
44
- const name = name_ptr[0..name_len];
45
-
46
- // "." と ".." をスキップ
47
- if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) {
48
- continue;
33
+ /// 非同期スキャナーコア(状態を保持)
34
+ const AsyncScanner = struct {
35
+ entries: []DirEntry,
36
+ capacity: usize,
37
+ count: usize,
38
+ state: ScanState,
39
+ progress: usize,
40
+ total_estimate: usize,
41
+ mutex: c.pthread_mutex_t,
42
+ path_copy: ?[*:0]u8,
43
+ max_entries: usize,
44
+ thread: c.pthread_t,
45
+ thread_started: bool,
46
+
47
+ fn init() !*AsyncScanner {
48
+ const self = @as(?*AsyncScanner, @ptrCast(@alignCast(c.malloc(@sizeOf(AsyncScanner))))) orelse return error.OutOfMemory;
49
+
50
+ self.*.capacity = 64;
51
+ const entries_size = self.*.capacity * @sizeOf(DirEntry);
52
+ const entries_ptr = c.malloc(entries_size) orelse {
53
+ c.free(self);
54
+ return error.OutOfMemory;
55
+ };
56
+ self.*.entries = @as([*]DirEntry, @ptrCast(@alignCast(entries_ptr)))[0..self.*.capacity];
57
+ self.*.count = 0;
58
+ self.*.state = .idle;
59
+ self.*.progress = 0;
60
+ self.*.total_estimate = 0;
61
+ self.*.path_copy = null;
62
+ self.*.max_entries = 0;
63
+ self.*.thread_started = false;
64
+
65
+ // mutex初期化
66
+ _ = c.pthread_mutex_init(&self.*.mutex, null);
67
+
68
+ return self;
69
+ }
70
+
71
+ fn deinit(self: *AsyncScanner) void {
72
+ // スレッドが実行中なら待機
73
+ if (self.thread_started) {
74
+ _ = c.pthread_join(self.thread, null);
49
75
  }
50
76
 
51
- // フルパスを構築
52
- var path_buf: [std.fs.max_path_bytes]u8 = undefined;
53
- const full_path = std.fmt.bufPrintZ(&path_buf, "{s}/{s}", .{ path, name }) catch {
54
- continue;
55
- };
77
+ // 各エントリの名前を解放
78
+ for (self.entries[0..self.count]) |*entry| {
79
+ entry.deinit();
80
+ }
56
81
 
57
- // ファイル情報を取得
58
- var stat_buf: c.struct_stat = undefined;
59
- if (c.lstat(full_path.ptr, &stat_buf) != 0) {
60
- continue;
82
+ // パスのコピーを解放
83
+ if (self.path_copy) |path| {
84
+ c.free(path);
61
85
  }
62
86
 
63
- // ディレクトリかどうか
64
- const is_dir = (stat_buf.st_mode & c.S_IFMT) == c.S_IFDIR;
87
+ // エントリ配列を解放
88
+ c.free(self.entries.ptr);
65
89
 
66
- // 実行可能かどうか
67
- const executable = (stat_buf.st_mode & c.S_IXUSR) != 0;
90
+ // mutex破棄
91
+ _ = c.pthread_mutex_destroy(&self.mutex);
68
92
 
69
- // 隠しファイルかどうか
70
- const hidden = name[0] == '.';
93
+ // 自身を解放
94
+ c.free(self);
95
+ }
71
96
 
72
- // Rubyハッシュを作成
73
- const hash = c.rb_hash_new();
97
+ fn clear(self: *AsyncScanner) void {
98
+ // 既存のエントリをクリア
99
+ for (self.entries[0..self.count]) |*entry| {
100
+ entry.deinit();
101
+ }
102
+ self.count = 0;
103
+ self.progress = 0;
104
+ self.total_estimate = 0;
105
+ }
74
106
 
75
- // ハッシュにキーと値を設定
76
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("name")), c.rb_str_new(name.ptr, @intCast(name.len)));
77
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("is_dir")), if (is_dir) c.Qtrue else c.Qfalse);
78
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("size")), c.ULL2NUM(@as(c_ulonglong, @intCast(stat_buf.st_size))));
79
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("mtime")), c.LL2NUM(@as(c_longlong, @intCast(stat_buf.st_mtimespec.tv_sec))));
80
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("executable")), if (executable) c.Qtrue else c.Qfalse);
81
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("hidden")), if (hidden) c.Qtrue else c.Qfalse);
107
+ fn ensureCapacity(self: *AsyncScanner, needed: usize) !void {
108
+ if (needed <= self.capacity) return;
82
109
 
83
- // 配列に追加
84
- _ = c.rb_ary_push(ary, hash);
110
+ const new_capacity = @max(needed, self.capacity * 2);
111
+ const new_size = new_capacity * @sizeOf(DirEntry);
112
+ const new_ptr = c.realloc(self.entries.ptr, new_size) orelse return error.OutOfMemory;
113
+ self.entries = @as([*]DirEntry, @ptrCast(@alignCast(new_ptr)))[0..new_capacity];
114
+ self.capacity = new_capacity;
85
115
  }
86
116
 
87
- return ary;
88
- }
117
+ fn isCancelled(self: *AsyncScanner) bool {
118
+ _ = c.pthread_mutex_lock(&self.mutex);
119
+ defer _ = c.pthread_mutex_unlock(&self.mutex);
120
+ return self.state == .cancelled;
121
+ }
89
122
 
90
- /// 高速スキャン(エントリ数制限付き)
91
- fn scanDirectoryFast(self: c.VALUE, path_value: c.VALUE, max_entries_value: c.VALUE) callconv(.c) c.VALUE {
92
- _ = self;
123
+ fn setState(self: *AsyncScanner, new_state: ScanState) void {
124
+ _ = c.pthread_mutex_lock(&self.mutex);
125
+ defer _ = c.pthread_mutex_unlock(&self.mutex);
126
+ self.state = new_state;
127
+ }
128
+
129
+ fn scan(self: *AsyncScanner, path: [*:0]const u8, max_entries: usize) !void {
130
+ // クリア
131
+ self.clear();
132
+
133
+ const dir = c.opendir(path) orelse return error.CannotOpenDir;
134
+ defer _ = c.closedir(dir);
135
+
136
+ const path_len = c.strlen(path);
137
+ const path_slice = path[0..path_len];
138
+
139
+ while (c.readdir(dir)) |entry| {
140
+ // キャンセルチェック
141
+ if (self.isCancelled()) {
142
+ return error.Cancelled;
143
+ }
144
+
145
+ // エントリ数制限チェック
146
+ if (max_entries > 0 and self.count >= max_entries) break;
147
+
148
+ const name_ptr = @as([*:0]const u8, @ptrCast(&entry.*.d_name));
149
+ const name_len = c.strlen(name_ptr);
150
+ const name_slice = name_ptr[0..name_len];
151
+
152
+ // "." と ".." をスキップ
153
+ if (std.mem.eql(u8, name_slice, ".") or std.mem.eql(u8, name_slice, "..")) {
154
+ continue;
155
+ }
156
+
157
+ // 容量確保
158
+ try self.ensureCapacity(self.count + 1);
159
+
160
+ // フルパスを構築
161
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
162
+ const full_path = std.fmt.bufPrintZ(&path_buf, "{s}/{s}", .{ path_slice, name_slice }) catch continue;
163
+
164
+ // ファイル情報を取得
165
+ var stat_buf: c.struct_stat = undefined;
166
+ if (c.lstat(full_path.ptr, &stat_buf) != 0) {
167
+ continue;
168
+ }
169
+
170
+ // 名前をコピー
171
+ const name_copy_ptr = c.malloc(name_len + 1) orelse return error.OutOfMemory;
172
+ const name_copy_bytes = @as([*]u8, @ptrCast(@alignCast(name_copy_ptr)));
173
+ const name_copy = name_copy_bytes[0..name_len];
174
+ @memcpy(name_copy, name_slice);
175
+ name_copy_bytes[name_len] = 0;
176
+
177
+ // エントリを追加
178
+ self.entries[self.count] = DirEntry{
179
+ .name = name_copy,
180
+ .is_dir = (stat_buf.st_mode & c.S_IFMT) == c.S_IFDIR,
181
+ .size = @intCast(stat_buf.st_size),
182
+ .mtime = @intCast(stat_buf.st_mtimespec.tv_sec),
183
+ .executable = (stat_buf.st_mode & c.S_IXUSR) != 0,
184
+ .hidden = name_slice[0] == '.',
185
+ };
186
+ self.count += 1;
187
+
188
+ // 進捗更新
189
+ _ = c.pthread_mutex_lock(&self.mutex);
190
+ self.progress = self.count;
191
+ _ = c.pthread_mutex_unlock(&self.mutex);
192
+ }
193
+ }
93
194
 
94
- const max_entries = @as(usize, @intCast(c.NUM2INT(max_entries_value)));
95
- var mutable_path = path_value;
96
- const path_str = c.rb_string_value_cstr(@ptrCast(&mutable_path));
97
- const path_len = c.strlen(path_str);
98
- const path = path_str[0..path_len];
195
+ fn scanAsync(self: *AsyncScanner, path: [*:0]const u8, max_entries: usize) !void {
196
+ _ = c.pthread_mutex_lock(&self.mutex);
197
+ defer _ = c.pthread_mutex_unlock(&self.mutex);
99
198
 
100
- const dir = c.opendir(path_str) orelse {
101
- c.rb_raise(c.rb_eRuntimeError, "Failed to open directory");
102
- return c.Qnil;
103
- };
104
- defer _ = c.closedir(dir);
199
+ if (self.state == .scanning) {
200
+ return error.AlreadyScanning;
201
+ }
105
202
 
106
- const ary = c.rb_ary_new();
107
- var count: usize = 0;
203
+ // パスをコピー
204
+ const path_len = c.strlen(path);
205
+ const path_copy_ptr = c.malloc(path_len + 1) orelse return error.OutOfMemory;
206
+ const path_copy_bytes = @as([*:0]u8, @ptrCast(@alignCast(path_copy_ptr)));
207
+ @memcpy(path_copy_bytes[0..path_len], path[0..path_len]);
208
+ path_copy_bytes[path_len] = 0;
108
209
 
109
- while (c.readdir(dir)) |entry| {
110
- if (count >= max_entries) break;
210
+ // 古いパスを解放
211
+ if (self.path_copy) |old_path| {
212
+ c.free(old_path);
213
+ }
111
214
 
112
- const name_ptr = @as([*:0]const u8, @ptrCast(&entry.*.d_name));
113
- const name_len = c.strlen(name_ptr);
114
- const name = name_ptr[0..name_len];
215
+ self.path_copy = path_copy_bytes;
216
+ self.max_entries = max_entries;
217
+ self.state = .scanning;
115
218
 
116
- if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) {
117
- continue;
219
+ // スレッド開始
220
+ const result = c.pthread_create(&self.thread, null, scanWorkerEntry, self);
221
+ if (result != 0) {
222
+ self.state = .failed;
223
+ return error.ThreadCreateFailed;
118
224
  }
225
+ self.thread_started = true;
226
+ }
119
227
 
120
- var path_buf: [std.fs.max_path_bytes]u8 = undefined;
121
- const full_path = std.fmt.bufPrintZ(&path_buf, "{s}/{s}", .{ path, name }) catch {
122
- continue;
228
+ fn scanWorker(self: *AsyncScanner) void {
229
+ const path = self.path_copy orelse {
230
+ self.setState(.failed);
231
+ return;
123
232
  };
124
233
 
125
- var stat_buf: c.struct_stat = undefined;
126
- if (c.lstat(full_path.ptr, &stat_buf) != 0) {
127
- continue;
128
- }
234
+ self.scan(path, self.max_entries) catch |err| {
235
+ // キャンセルエラーの場合は状態を変更しない(既にcancelledになっている)
236
+ if (err == error.Cancelled) {
237
+ return;
238
+ }
239
+ self.setState(.failed);
240
+ return;
241
+ };
242
+
243
+ self.setState(.done);
244
+ }
245
+ };
246
+
247
+ // スレッドエントリポイント
248
+ fn scanWorkerEntry(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
249
+ const scanner = @as(*AsyncScanner, @ptrCast(@alignCast(arg)));
250
+ scanner.scanWorker();
251
+ return null;
252
+ }
253
+
254
+ // ============================================================
255
+ // ABI Boundary: Ruby ABI非依存・ハンドルベース
256
+ // ============================================================
129
257
 
130
- const is_dir = (stat_buf.st_mode & c.S_IFMT) == c.S_IFDIR;
131
- const executable = (stat_buf.st_mode & c.S_IXUSR) != 0;
132
- const hidden = name[0] == '.';
258
+ /// 非同期スキャナーを作成(ハンドル返却)
259
+ export fn core_async_create() u64 {
260
+ const scanner = AsyncScanner.init() catch return 0;
261
+ return @intFromPtr(scanner);
262
+ }
263
+
264
+ /// 非同期スキャン開始
265
+ export fn core_async_scan(handle: u64, path: [*:0]const u8) i32 {
266
+ if (handle == 0) return -1;
267
+
268
+ const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
269
+ scanner.scanAsync(path, 0) catch return -1;
270
+ return 0;
271
+ }
272
+
273
+ /// 高速スキャン(エントリ数制限付き)
274
+ export fn core_async_scan_fast(handle: u64, path: [*:0]const u8, max_entries: usize) i32 {
275
+ if (handle == 0) return -1;
276
+
277
+ const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
278
+ scanner.scanAsync(path, max_entries) catch return -1;
279
+ return 0;
280
+ }
133
281
 
134
- const hash = c.rb_hash_new();
135
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("name")), c.rb_str_new(name.ptr, @intCast(name.len)));
136
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("is_dir")), if (is_dir) c.Qtrue else c.Qfalse);
137
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("size")), c.ULL2NUM(@as(c_ulonglong, @intCast(stat_buf.st_size))));
138
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("mtime")), c.LL2NUM(@as(c_longlong, @intCast(stat_buf.st_mtimespec.tv_sec))));
139
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("executable")), if (executable) c.Qtrue else c.Qfalse);
140
- _ = c.rb_hash_aset(hash, c.ID2SYM(c.rb_intern("hidden")), if (hidden) c.Qtrue else c.Qfalse);
282
+ /// 状態取得
283
+ export fn core_async_get_state(handle: u64) u8 {
284
+ if (handle == 0) return @intFromEnum(ScanState.failed);
141
285
 
142
- _ = c.rb_ary_push(ary, hash);
143
- count += 1;
286
+ const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
287
+ _ = c.pthread_mutex_lock(&scanner.mutex);
288
+ defer _ = c.pthread_mutex_unlock(&scanner.mutex);
289
+ return @intFromEnum(scanner.state);
290
+ }
291
+
292
+ /// 進捗取得
293
+ export fn core_async_get_progress(handle: u64, current: *usize, total: *usize) void {
294
+ if (handle == 0) {
295
+ current.* = 0;
296
+ total.* = 0;
297
+ return;
144
298
  }
145
299
 
146
- return ary;
300
+ const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
301
+ _ = c.pthread_mutex_lock(&scanner.mutex);
302
+ defer _ = c.pthread_mutex_unlock(&scanner.mutex);
303
+
304
+ current.* = scanner.progress;
305
+ total.* = scanner.total_estimate;
147
306
  }
148
307
 
149
- /// バージョン情報
150
- fn getVersion(_: c.VALUE) callconv(.c) c.VALUE {
151
- const version = "1.0.0-zig";
152
- return c.rb_str_new(version.ptr, @intCast(version.len));
308
+ /// キャンセル
309
+ export fn core_async_cancel(handle: u64) void {
310
+ if (handle == 0) return;
311
+
312
+ const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
313
+ _ = c.pthread_mutex_lock(&scanner.mutex);
314
+ defer _ = c.pthread_mutex_unlock(&scanner.mutex);
315
+
316
+ if (scanner.state == .scanning) {
317
+ scanner.state = .cancelled;
318
+ }
153
319
  }
154
320
 
155
- /// Ruby拡張の初期化
156
- export fn Init_rufio_zig() void {
157
- // Rufioモジュールを取得または作成
158
- const rufio_module = c.rb_define_module("Rufio");
321
+ /// エントリ数を取得
322
+ export fn core_async_get_count(handle: u64) usize {
323
+ if (handle == 0) return 0;
159
324
 
160
- // NativeScannerZigクラスを定義
161
- const scanner_class = c.rb_define_class_under(rufio_module, "NativeScannerZig", c.rb_cObject);
325
+ const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
326
+ _ = c.pthread_mutex_lock(&scanner.mutex);
327
+ defer _ = c.pthread_mutex_unlock(&scanner.mutex);
328
+ return scanner.count;
329
+ }
330
+
331
+ /// 指定インデックスのエントリ名を取得
332
+ export fn core_async_get_name(handle: u64, index: usize, buf: [*]u8, buf_size: usize) usize {
333
+ if (handle == 0) return 0;
334
+
335
+ const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
336
+ _ = c.pthread_mutex_lock(&scanner.mutex);
337
+ defer _ = c.pthread_mutex_unlock(&scanner.mutex);
338
+
339
+ if (index >= scanner.count) return 0;
340
+
341
+ const entry = &scanner.entries[index];
342
+ const copy_len = @min(entry.name.len, buf_size - 1);
343
+ @memcpy(buf[0..copy_len], entry.name[0..copy_len]);
344
+ buf[copy_len] = 0;
345
+ return copy_len;
346
+ }
347
+
348
+ /// 指定インデックスのエントリ属性を取得
349
+ export fn core_async_get_attrs(handle: u64, index: usize, is_dir: *u8, size: *u64, mtime: *i64, executable: *u8, hidden: *u8) i32 {
350
+ if (handle == 0) return -1;
351
+
352
+ const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
353
+ _ = c.pthread_mutex_lock(&scanner.mutex);
354
+ defer _ = c.pthread_mutex_unlock(&scanner.mutex);
162
355
 
163
- // クラスメソッドを定義
164
- c.rb_define_singleton_method(scanner_class, "scan_directory", @ptrCast(&scanDirectory), 1);
165
- c.rb_define_singleton_method(scanner_class, "scan_directory_fast", @ptrCast(&scanDirectoryFast), 2);
166
- c.rb_define_singleton_method(scanner_class, "version", @ptrCast(&getVersion), 0);
356
+ if (index >= scanner.count) return -1;
357
+
358
+ const entry = &scanner.entries[index];
359
+ is_dir.* = if (entry.is_dir) 1 else 0;
360
+ size.* = entry.size;
361
+ mtime.* = entry.mtime;
362
+ executable.* = if (entry.executable) 1 else 0;
363
+ hidden.* = if (entry.hidden) 1 else 0;
364
+ return 0;
365
+ }
366
+
367
+ /// スキャナーを破棄
368
+ export fn core_async_destroy(handle: u64) void {
369
+ if (handle == 0) return;
370
+
371
+ const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
372
+ scanner.deinit();
373
+ }
374
+
375
+ /// バージョン情報
376
+ export fn core_async_version() [*:0]const u8 {
377
+ return "4.0.0-async";
167
378
  }