rufio 1.1.0 → 1.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +74 -3
- data/README_ja.md +71 -2
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/info/help.md +2 -0
- data/info/keybindings.md +3 -1
- data/info/welcome.md +1 -1
- data/lib/rufio/application.rb +12 -1
- data/lib/rufio/archive_controller.rb +211 -0
- data/lib/rufio/background_command_executor.rb +9 -0
- data/lib/rufio/background_status.rb +49 -0
- data/lib/rufio/bookmark_controller.rb +38 -121
- data/lib/rufio/bookmark_manager.rb +77 -331
- data/lib/rufio/command_completion.rb +6 -1
- data/lib/rufio/command_history.rb +5 -2
- data/lib/rufio/command_mode.rb +40 -134
- data/lib/rufio/command_mode_ui.rb +4 -42
- data/lib/rufio/config.rb +88 -136
- data/lib/rufio/config_loader.rb +3 -11
- data/lib/rufio/dialog_renderer.rb +64 -152
- data/lib/rufio/dialog_support.rb +69 -0
- data/lib/rufio/document_text_extractor.rb +167 -0
- data/lib/rufio/file_operation_controller.rb +49 -222
- data/lib/rufio/file_preview.rb +46 -2
- data/lib/rufio/global_search.rb +121 -9
- data/lib/rufio/help_menu.rb +8 -2
- data/lib/rufio/interpreter_resolver.rb +6 -11
- data/lib/rufio/job_manager.rb +2 -5
- data/lib/rufio/key_input.rb +77 -0
- data/lib/rufio/keybind_handler.rb +81 -115
- data/lib/rufio/local_script_scanner.rb +6 -18
- data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native_content_search.rb +153 -0
- data/lib/rufio/native_scanner.rb +13 -68
- data/lib/rufio/native_scanner_zig.rb +106 -56
- data/lib/rufio/native_zip.rb +121 -0
- data/lib/rufio/navigation_controller.rb +18 -20
- data/lib/rufio/screen.rb +2 -12
- data/lib/rufio/script_executor.rb +6 -73
- data/lib/rufio/script_file.rb +44 -0
- data/lib/rufio/script_path_manager.rb +9 -43
- data/lib/rufio/script_runner.rb +13 -50
- data/lib/rufio/search_controller.rb +1 -1
- data/lib/rufio/shell_command_completion.rb +1 -5
- data/lib/rufio/shell_init.rb +90 -0
- data/lib/rufio/task_status.rb +5 -1
- data/lib/rufio/terminal_ui.rb +55 -294
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +63 -40
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio/zip_archive.rb +314 -0
- data/lib/rufio/zip_extractor.rb +346 -0
- data/lib/rufio/zip_writer.rb +268 -0
- data/lib/rufio/zoxide_integration.rb +5 -43
- data/lib/rufio.rb +5 -0
- data/lib_zig/rufio_native/Makefile +54 -17
- data/lib_zig/rufio_native/bench.sh +36 -0
- data/lib_zig/rufio_native/build.zig +32 -35
- data/lib_zig/rufio_native/src/cli.zig +99 -0
- data/lib_zig/rufio_native/src/main.zig +187 -83
- data/lib_zig/rufio_native/src/os.zig +889 -0
- data/lib_zig/rufio_native/src/search.zig +919 -0
- data/lib_zig/rufio_native/src/search_test.zig +88 -0
- data/lib_zig/rufio_native/src/zip.zig +482 -0
- metadata +125 -3
- data/lib/rufio/native/rufio_zig.bundle +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const search = @import("search.zig");
|
|
3
|
+
const Matcher = search.Matcher;
|
|
4
|
+
|
|
5
|
+
fn naiveFind(hay: []const u8, needle: []const u8) ?usize {
|
|
6
|
+
if (needle.len > hay.len) return null;
|
|
7
|
+
var i: usize = 0;
|
|
8
|
+
while (i + needle.len <= hay.len) : (i += 1) {
|
|
9
|
+
var ok = true;
|
|
10
|
+
for (needle, 0..) |n, j| {
|
|
11
|
+
if (std.ascii.toLower(hay[i + j]) != std.ascii.toLower(n)) {
|
|
12
|
+
ok = false;
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (ok) return i;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test "finds ascii case-insensitively" {
|
|
22
|
+
const m = Matcher.init("world");
|
|
23
|
+
try std.testing.expectEqual(@as(?usize, 6), m.find("Hello WORLD"));
|
|
24
|
+
try std.testing.expectEqual(@as(?usize, 0), m.find("wOrLd"));
|
|
25
|
+
try std.testing.expectEqual(@as(?usize, null), m.find("worl"));
|
|
26
|
+
try std.testing.expectEqual(@as(?usize, null), m.find(""));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test "does not fold non-letters" {
|
|
30
|
+
const m = Matcher.init("[a]");
|
|
31
|
+
try std.testing.expectEqual(@as(?usize, null), m.find("{A}"));
|
|
32
|
+
try std.testing.expectEqual(@as(?usize, 3), m.find("{A}[A]"));
|
|
33
|
+
const at = Matcher.init("@");
|
|
34
|
+
try std.testing.expectEqual(@as(?usize, null), at.find("`````"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test "finds utf-8 needles" {
|
|
38
|
+
const m = Matcher.init("日本語");
|
|
39
|
+
try std.testing.expectEqual(@as(?usize, 3), m.find("あ日本語の検索"));
|
|
40
|
+
try std.testing.expectEqual(@as(?usize, null), m.find("日本の検索"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test "single byte needle" {
|
|
44
|
+
const m = Matcher.init("Q");
|
|
45
|
+
var buf: [100]u8 = undefined;
|
|
46
|
+
@memset(&buf, 'x');
|
|
47
|
+
try std.testing.expectEqual(@as(?usize, null), m.find(&buf));
|
|
48
|
+
buf[77] = 'q';
|
|
49
|
+
try std.testing.expectEqual(@as(?usize, 77), m.find(&buf));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test "matches naive search on random input" {
|
|
53
|
+
var prng = std.Random.DefaultPrng.init(0x5eed);
|
|
54
|
+
const rand = prng.random();
|
|
55
|
+
const alphabet = "abAB_ \n.xyz";
|
|
56
|
+
var hay: [700]u8 = undefined;
|
|
57
|
+
var needle: [9]u8 = undefined;
|
|
58
|
+
var iter: usize = 0;
|
|
59
|
+
while (iter < 20000) : (iter += 1) {
|
|
60
|
+
const hay_len = rand.uintLessThan(usize, hay.len);
|
|
61
|
+
const needle_len = 1 + rand.uintLessThan(usize, needle.len);
|
|
62
|
+
for (hay[0..hay_len]) |*b| b.* = alphabet[rand.uintLessThan(usize, alphabet.len)];
|
|
63
|
+
for (needle[0..needle_len]) |*b| b.* = alphabet[rand.uintLessThan(usize, 4)];
|
|
64
|
+
const m = Matcher.init(needle[0..needle_len]);
|
|
65
|
+
try std.testing.expectEqual(naiveFind(hay[0..hay_len], needle[0..needle_len]), m.find(hay[0..hay_len]));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test "counts newlines" {
|
|
70
|
+
var buf: [300]u8 = undefined;
|
|
71
|
+
@memset(&buf, 'a');
|
|
72
|
+
buf[0] = '\n';
|
|
73
|
+
buf[31] = '\n';
|
|
74
|
+
buf[32] = '\n';
|
|
75
|
+
buf[299] = '\n';
|
|
76
|
+
try std.testing.expectEqual(@as(usize, 4), search.countNewlines(&buf));
|
|
77
|
+
try std.testing.expectEqual(@as(usize, 2), search.countNewlines(buf[1..33]));
|
|
78
|
+
try std.testing.expectEqual(@as(usize, 0), search.countNewlines(buf[33..299]));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test "extension helpers follow Ruby semantics" {
|
|
82
|
+
try std.testing.expect(search.hasSuffixIgnoreCase("archive.TAR.GZ", ".tar.gz"));
|
|
83
|
+
try std.testing.expect(!search.hasSuffixIgnoreCase("catalog", ".log"));
|
|
84
|
+
try std.testing.expectEqualStrings(".PDF", search.extname("report.PDF"));
|
|
85
|
+
try std.testing.expectEqualStrings("", search.extname(".pdf"));
|
|
86
|
+
try std.testing.expectEqualStrings("", search.extname("Makefile"));
|
|
87
|
+
try std.testing.expectEqualStrings(".gz", search.extname("a.tar.gz"));
|
|
88
|
+
}
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
//! ZIP のデータ処理エンジン(展開・圧縮)。
|
|
2
|
+
//!
|
|
3
|
+
//! 安全性の検証(パス・シンボリックリンク・上限・重複範囲)と、ディレクトリ作成・rename・
|
|
4
|
+
//! 権限と日時の設定は Ruby 側(ZipExtractor / ZipWriter)が行う。ここではエントリ単位の
|
|
5
|
+
//! データ処理だけをスレッドで並列に行う。
|
|
6
|
+
//!
|
|
7
|
+
//! - 展開: ZIP 内の指定範囲を読み、Stored はそのまま、Deflate は伸長し、宣言サイズを超えた時点で
|
|
8
|
+
//! 打ち切り、CRC-32 を検証して一時ファイル(新規作成のみ)に書く
|
|
9
|
+
//! - 圧縮: ファイルを raw DEFLATE で一時ファイルに書き、CRC-32・圧縮後サイズ・元サイズを返す
|
|
10
|
+
const std = @import("std");
|
|
11
|
+
const os = @import("os.zig");
|
|
12
|
+
const flate = std.compress.flate;
|
|
13
|
+
const Reader = std.Io.Reader;
|
|
14
|
+
const Writer = std.Io.Writer;
|
|
15
|
+
const Crc32 = std.hash.Crc32;
|
|
16
|
+
|
|
17
|
+
const alloc = std.heap.c_allocator;
|
|
18
|
+
|
|
19
|
+
pub const MAX_THREADS = 16;
|
|
20
|
+
const IO_BUFFER = 64 * 1024;
|
|
21
|
+
/// flate.Compress は 200KB 超あり、初期化時に一時的にスタックへ載るため余裕を持たせる
|
|
22
|
+
const WORKER_STACK = 4 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
pub const State = enum(u8) {
|
|
25
|
+
idle = 0,
|
|
26
|
+
running = 1,
|
|
27
|
+
done = 2,
|
|
28
|
+
cancelled = 3,
|
|
29
|
+
failed = 4,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/// タスクごとの結果(Ruby 側 NativeZip::STATUS_* と一致させる)
|
|
33
|
+
pub const Status = enum(u8) {
|
|
34
|
+
ok = 0,
|
|
35
|
+
crc_mismatch = 1,
|
|
36
|
+
size_mismatch = 2,
|
|
37
|
+
corrupt = 3,
|
|
38
|
+
io_error = 4,
|
|
39
|
+
cancelled = 5,
|
|
40
|
+
too_large = 6,
|
|
41
|
+
pending = 255,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
pub const Kind = enum { extract, deflate };
|
|
45
|
+
|
|
46
|
+
const Task = struct {
|
|
47
|
+
// 展開
|
|
48
|
+
data_offset: u64 = 0,
|
|
49
|
+
compressed_size: u64 = 0,
|
|
50
|
+
size: u64 = 0,
|
|
51
|
+
crc: u32 = 0,
|
|
52
|
+
method: u8 = 0,
|
|
53
|
+
// 圧縮
|
|
54
|
+
source: ?[:0]u8 = null,
|
|
55
|
+
// 共通
|
|
56
|
+
output: ?[:0]u8 = null,
|
|
57
|
+
status: Status = .pending,
|
|
58
|
+
// 圧縮の結果
|
|
59
|
+
out_crc: u32 = 0,
|
|
60
|
+
out_compressed: u64 = 0,
|
|
61
|
+
out_size: u64 = 0,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/// 1 スレッド分の作業領域
|
|
65
|
+
const Worker = struct {
|
|
66
|
+
scratch: os.Scratch = .{},
|
|
67
|
+
in_buf: []u8 = &.{},
|
|
68
|
+
out_buf: []u8 = &.{},
|
|
69
|
+
window: []u8 = &.{},
|
|
70
|
+
|
|
71
|
+
fn init(self: *Worker) !void {
|
|
72
|
+
try self.scratch.init();
|
|
73
|
+
errdefer self.deinit();
|
|
74
|
+
self.in_buf = try alloc.alloc(u8, IO_BUFFER);
|
|
75
|
+
self.out_buf = try alloc.alloc(u8, IO_BUFFER);
|
|
76
|
+
self.window = try alloc.alloc(u8, flate.max_window_len);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn deinit(self: *Worker) void {
|
|
80
|
+
self.scratch.deinit();
|
|
81
|
+
if (self.in_buf.len > 0) alloc.free(self.in_buf);
|
|
82
|
+
if (self.out_buf.len > 0) alloc.free(self.out_buf);
|
|
83
|
+
if (self.window.len > 0) alloc.free(self.window);
|
|
84
|
+
self.* = .{};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/// ZIP 内の [offset, offset+remaining) を読む Reader
|
|
89
|
+
const RegionReader = struct {
|
|
90
|
+
reader: Reader,
|
|
91
|
+
file: os.File,
|
|
92
|
+
offset: u64,
|
|
93
|
+
remaining: u64,
|
|
94
|
+
failed: bool = false,
|
|
95
|
+
|
|
96
|
+
const vtable: Reader.VTable = .{ .stream = stream, .readVec = readVec };
|
|
97
|
+
|
|
98
|
+
fn init(file: os.File, offset: u64, length: u64, buffer: []u8) RegionReader {
|
|
99
|
+
return .{
|
|
100
|
+
.reader = .{ .vtable = &vtable, .buffer = buffer, .seek = 0, .end = 0 },
|
|
101
|
+
.file = file,
|
|
102
|
+
.offset = offset,
|
|
103
|
+
.remaining = length,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// 自分のバッファに直接読み込む(Reader の実装として許されている方式)
|
|
108
|
+
fn fill(r: *Reader) Reader.Error!void {
|
|
109
|
+
const self: *RegionReader = @fieldParentPtr("reader", r);
|
|
110
|
+
if (self.remaining == 0) return error.EndOfStream;
|
|
111
|
+
if (r.seek > 0) {
|
|
112
|
+
const unread = r.end - r.seek;
|
|
113
|
+
std.mem.copyForwards(u8, r.buffer[0..unread], r.buffer[r.seek..r.end]);
|
|
114
|
+
r.seek = 0;
|
|
115
|
+
r.end = unread;
|
|
116
|
+
}
|
|
117
|
+
const space = r.buffer[r.end..];
|
|
118
|
+
if (space.len == 0) return;
|
|
119
|
+
const want: usize = @intCast(@min(space.len, self.remaining));
|
|
120
|
+
const n = self.file.pread(space[0..want], self.offset) orelse {
|
|
121
|
+
self.failed = true;
|
|
122
|
+
return error.ReadFailed;
|
|
123
|
+
};
|
|
124
|
+
if (n == 0) return error.EndOfStream;
|
|
125
|
+
r.end += n;
|
|
126
|
+
self.offset += n;
|
|
127
|
+
self.remaining -= n;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn stream(r: *Reader, w: *Writer, limit: std.Io.Limit) Reader.StreamError!usize {
|
|
131
|
+
_ = w;
|
|
132
|
+
_ = limit;
|
|
133
|
+
try fill(r);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn readVec(r: *Reader, data: [][]u8) Reader.Error!usize {
|
|
138
|
+
_ = data;
|
|
139
|
+
try fill(r);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/// 書き込み先ファイルへの Writer(CRC-32・サイズ計測、上限超過・キャンセルで打ち切り)
|
|
145
|
+
const FileSink = struct {
|
|
146
|
+
writer: Writer,
|
|
147
|
+
file: os.File,
|
|
148
|
+
job: *Job,
|
|
149
|
+
limit: ?u64,
|
|
150
|
+
hash: bool,
|
|
151
|
+
crc: Crc32 = .init(),
|
|
152
|
+
written: u64 = 0,
|
|
153
|
+
overflow: bool = false,
|
|
154
|
+
cancelled: bool = false,
|
|
155
|
+
io_failed: bool = false,
|
|
156
|
+
|
|
157
|
+
const vtable: Writer.VTable = .{ .drain = drain };
|
|
158
|
+
|
|
159
|
+
fn init(file: os.File, job: *Job, buffer: []u8, limit: ?u64, hash: bool) FileSink {
|
|
160
|
+
return .{
|
|
161
|
+
.writer = .{ .vtable = &vtable, .buffer = buffer },
|
|
162
|
+
.file = file,
|
|
163
|
+
.job = job,
|
|
164
|
+
.limit = limit,
|
|
165
|
+
.hash = hash,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn sink(self: *FileSink, bytes: []const u8) Writer.Error!void {
|
|
170
|
+
if (bytes.len == 0) return;
|
|
171
|
+
if (self.job.cancelled.load(.acquire)) {
|
|
172
|
+
self.cancelled = true;
|
|
173
|
+
return error.WriteFailed;
|
|
174
|
+
}
|
|
175
|
+
if (self.limit) |max| {
|
|
176
|
+
if (self.written + bytes.len > max) {
|
|
177
|
+
self.overflow = true;
|
|
178
|
+
return error.WriteFailed;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (self.hash) self.crc.update(bytes);
|
|
182
|
+
if (!self.file.writeAll(bytes)) {
|
|
183
|
+
self.io_failed = true;
|
|
184
|
+
return error.WriteFailed;
|
|
185
|
+
}
|
|
186
|
+
self.written += bytes.len;
|
|
187
|
+
if (self.hash) _ = self.job.done_bytes.fetchAdd(bytes.len, .monotonic);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
|
|
191
|
+
const self: *FileSink = @fieldParentPtr("writer", w);
|
|
192
|
+
try self.sink(w.buffer[0..w.end]);
|
|
193
|
+
w.end = 0;
|
|
194
|
+
var n: usize = 0;
|
|
195
|
+
for (data[0 .. data.len - 1]) |bytes| {
|
|
196
|
+
try self.sink(bytes);
|
|
197
|
+
n += bytes.len;
|
|
198
|
+
}
|
|
199
|
+
const last = data[data.len - 1];
|
|
200
|
+
for (0..splat) |_| {
|
|
201
|
+
try self.sink(last);
|
|
202
|
+
n += last.len;
|
|
203
|
+
}
|
|
204
|
+
return n;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn failureStatus(self: *const FileSink) Status {
|
|
208
|
+
if (self.cancelled) return .cancelled;
|
|
209
|
+
if (self.overflow) return .too_large;
|
|
210
|
+
return .io_error;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
pub const Job = struct {
|
|
215
|
+
kind: Kind,
|
|
216
|
+
zip_path: ?[:0]u8 = null,
|
|
217
|
+
tasks: []Task,
|
|
218
|
+
level: flate.Compress.Options = .default,
|
|
219
|
+
next: std.atomic.Value(usize) = .init(0),
|
|
220
|
+
done_bytes: std.atomic.Value(u64) = .init(0),
|
|
221
|
+
total_bytes: u64 = 0,
|
|
222
|
+
cancelled: std.atomic.Value(bool) = .init(false),
|
|
223
|
+
state: std.atomic.Value(u8) = .init(@intFromEnum(State.idle)),
|
|
224
|
+
running: std.atomic.Value(usize) = .init(0),
|
|
225
|
+
threads: [MAX_THREADS]std.Thread = undefined,
|
|
226
|
+
thread_count: usize = 0,
|
|
227
|
+
|
|
228
|
+
/// 展開ジョブ。tasks は (data_offset u64, compressed_size u64, size u64, crc u32, method u8, path_len u32, path) の並び
|
|
229
|
+
pub fn createExtract(zip_path: []const u8, records: []const u8, threads: u32) !*Job {
|
|
230
|
+
var count: usize = 0;
|
|
231
|
+
var pos: usize = 0;
|
|
232
|
+
while (pos < records.len) : (count += 1) {
|
|
233
|
+
if (pos + 33 > records.len) return error.InvalidTasks;
|
|
234
|
+
const len = std.mem.readInt(u32, records[pos + 29 ..][0..4], .little);
|
|
235
|
+
pos += 33 + len;
|
|
236
|
+
if (pos > records.len) return error.InvalidTasks;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const self = try create(.extract, count);
|
|
240
|
+
errdefer self.destroyTasks();
|
|
241
|
+
self.zip_path = try alloc.dupeSentinel(u8, zip_path, 0);
|
|
242
|
+
pos = 0;
|
|
243
|
+
for (self.tasks) |*task| {
|
|
244
|
+
task.data_offset = std.mem.readInt(u64, records[pos..][0..8], .little);
|
|
245
|
+
task.compressed_size = std.mem.readInt(u64, records[pos + 8 ..][0..8], .little);
|
|
246
|
+
task.size = std.mem.readInt(u64, records[pos + 16 ..][0..8], .little);
|
|
247
|
+
task.crc = std.mem.readInt(u32, records[pos + 24 ..][0..4], .little);
|
|
248
|
+
task.method = records[pos + 28];
|
|
249
|
+
const len = std.mem.readInt(u32, records[pos + 29 ..][0..4], .little);
|
|
250
|
+
task.output = try alloc.dupeSentinel(u8, records[pos + 33 ..][0..len], 0);
|
|
251
|
+
pos += 33 + len;
|
|
252
|
+
self.total_bytes += task.size;
|
|
253
|
+
}
|
|
254
|
+
self.start(threads);
|
|
255
|
+
return self;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// 圧縮ジョブ。tasks は (source_len u32, source, output_len u32, output) の並び
|
|
259
|
+
pub fn createDeflate(records: []const u8, level: i32, threads: u32) !*Job {
|
|
260
|
+
var count: usize = 0;
|
|
261
|
+
var pos: usize = 0;
|
|
262
|
+
while (pos < records.len) : (count += 1) {
|
|
263
|
+
for (0..2) |_| {
|
|
264
|
+
if (pos + 4 > records.len) return error.InvalidTasks;
|
|
265
|
+
pos += 4 + std.mem.readInt(u32, records[pos..][0..4], .little);
|
|
266
|
+
if (pos > records.len) return error.InvalidTasks;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const self = try create(.deflate, count);
|
|
271
|
+
errdefer self.destroyTasks();
|
|
272
|
+
self.level = levelOptions(level);
|
|
273
|
+
pos = 0;
|
|
274
|
+
for (self.tasks) |*task| {
|
|
275
|
+
var len = std.mem.readInt(u32, records[pos..][0..4], .little);
|
|
276
|
+
task.source = try alloc.dupeSentinel(u8, records[pos + 4 ..][0..len], 0);
|
|
277
|
+
pos += 4 + len;
|
|
278
|
+
len = std.mem.readInt(u32, records[pos..][0..4], .little);
|
|
279
|
+
task.output = try alloc.dupeSentinel(u8, records[pos + 4 ..][0..len], 0);
|
|
280
|
+
pos += 4 + len;
|
|
281
|
+
}
|
|
282
|
+
self.start(threads);
|
|
283
|
+
return self;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fn create(kind: Kind, count: usize) !*Job {
|
|
287
|
+
const self = try alloc.create(Job);
|
|
288
|
+
errdefer alloc.destroy(self);
|
|
289
|
+
const tasks = try alloc.alloc(Task, count);
|
|
290
|
+
for (tasks) |*task| task.* = .{};
|
|
291
|
+
self.* = .{ .kind = kind, .tasks = tasks };
|
|
292
|
+
return self;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fn start(self: *Job, threads: u32) void {
|
|
296
|
+
self.state.store(@intFromEnum(State.running), .release);
|
|
297
|
+
if (self.tasks.len == 0) {
|
|
298
|
+
self.state.store(@intFromEnum(State.done), .release);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const wanted: usize = if (threads == 0) os.defaultThreads() else threads;
|
|
302
|
+
const count = @max(1, @min(@min(wanted, MAX_THREADS), self.tasks.len));
|
|
303
|
+
self.running.store(count, .release);
|
|
304
|
+
var spawned: usize = 0;
|
|
305
|
+
while (spawned < count) : (spawned += 1) {
|
|
306
|
+
self.threads[spawned] = std.Thread.spawn(.{ .stack_size = WORKER_STACK }, workerMain, .{self}) catch break;
|
|
307
|
+
}
|
|
308
|
+
self.thread_count = spawned;
|
|
309
|
+
if (spawned < count) {
|
|
310
|
+
// 起動できなかった分は終了済みとして数える
|
|
311
|
+
if (self.running.fetchSub(count - spawned, .acq_rel) == count - spawned) self.finish();
|
|
312
|
+
if (spawned == 0) self.state.store(@intFromEnum(State.failed), .release);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
fn finish(self: *Job) void {
|
|
317
|
+
const final: State = if (self.cancelled.load(.acquire)) .cancelled else .done;
|
|
318
|
+
self.state.store(@intFromEnum(final), .release);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
pub fn getState(self: *Job) State {
|
|
322
|
+
return @enumFromInt(self.state.load(.acquire));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
pub fn cancel(self: *Job) void {
|
|
326
|
+
self.cancelled.store(true, .release);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// 圧縮では、読み込み済みバイト数の合計が分かるのは開始後なので total は随時更新される
|
|
330
|
+
pub fn progress(self: *Job, done: *u64, total: *u64) void {
|
|
331
|
+
done.* = self.done_bytes.load(.monotonic);
|
|
332
|
+
total.* = if (self.kind == .extract) self.total_bytes else @max(self.total_bytes, done.*);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
pub fn status(self: *Job, index: usize) Status {
|
|
336
|
+
if (index >= self.tasks.len) return .io_error;
|
|
337
|
+
return self.tasks[index].status;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
pub fn deflateResult(self: *Job, index: usize, crc: *u32, compressed: *u64, size: *u64) Status {
|
|
341
|
+
if (index >= self.tasks.len) return .io_error;
|
|
342
|
+
const task = &self.tasks[index];
|
|
343
|
+
crc.* = task.out_crc;
|
|
344
|
+
compressed.* = task.out_compressed;
|
|
345
|
+
size.* = task.out_size;
|
|
346
|
+
return task.status;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// 実行中ならキャンセルしてスレッドの終了を待ってから解放する
|
|
350
|
+
pub fn destroy(self: *Job) void {
|
|
351
|
+
self.cancel();
|
|
352
|
+
for (self.threads[0..self.thread_count]) |thread| thread.join();
|
|
353
|
+
self.destroyTasks();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fn destroyTasks(self: *Job) void {
|
|
357
|
+
for (self.tasks) |task| {
|
|
358
|
+
if (task.source) |path| alloc.free(path);
|
|
359
|
+
if (task.output) |path| alloc.free(path);
|
|
360
|
+
}
|
|
361
|
+
alloc.free(self.tasks);
|
|
362
|
+
if (self.zip_path) |path| alloc.free(path);
|
|
363
|
+
alloc.destroy(self);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
fn levelOptions(level: i32) flate.Compress.Options {
|
|
368
|
+
return switch (level) {
|
|
369
|
+
1 => .level_1,
|
|
370
|
+
2 => .level_2,
|
|
371
|
+
3 => .level_3,
|
|
372
|
+
4 => .level_4,
|
|
373
|
+
5 => .level_5,
|
|
374
|
+
7 => .level_7,
|
|
375
|
+
8 => .level_8,
|
|
376
|
+
9 => .level_9,
|
|
377
|
+
else => .default,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
fn workerMain(job: *Job) void {
|
|
382
|
+
defer {
|
|
383
|
+
if (job.running.fetchSub(1, .acq_rel) == 1) job.finish();
|
|
384
|
+
}
|
|
385
|
+
var worker: Worker = .{};
|
|
386
|
+
worker.init() catch {
|
|
387
|
+
markRemaining(job, .io_error);
|
|
388
|
+
return;
|
|
389
|
+
};
|
|
390
|
+
defer worker.deinit();
|
|
391
|
+
|
|
392
|
+
var zip: ?os.File = null;
|
|
393
|
+
defer if (zip) |file| file.close();
|
|
394
|
+
|
|
395
|
+
while (true) {
|
|
396
|
+
const index = job.next.fetchAdd(1, .acq_rel);
|
|
397
|
+
if (index >= job.tasks.len) return;
|
|
398
|
+
const task = &job.tasks[index];
|
|
399
|
+
if (job.cancelled.load(.acquire)) {
|
|
400
|
+
task.status = .cancelled;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
switch (job.kind) {
|
|
404
|
+
.extract => {
|
|
405
|
+
if (zip == null) zip = os.openRead(job.zip_path.?, &worker.scratch);
|
|
406
|
+
task.status = if (zip) |file| extractTask(job, task, file, &worker) else .io_error;
|
|
407
|
+
},
|
|
408
|
+
.deflate => task.status = deflateTask(job, task, &worker),
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fn markRemaining(job: *Job, status: Status) void {
|
|
414
|
+
while (true) {
|
|
415
|
+
const index = job.next.fetchAdd(1, .acq_rel);
|
|
416
|
+
if (index >= job.tasks.len) return;
|
|
417
|
+
job.tasks[index].status = status;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
fn extractTask(job: *Job, task: *Task, zip: os.File, worker: *Worker) Status {
|
|
422
|
+
if (task.method != 0 and task.method != 8) return .corrupt;
|
|
423
|
+
if (task.method == 0 and task.compressed_size != task.size) return .size_mismatch;
|
|
424
|
+
|
|
425
|
+
const out = os.createExclusive(task.output.?, &worker.scratch) orelse return .io_error;
|
|
426
|
+
defer out.close();
|
|
427
|
+
|
|
428
|
+
var sink = FileSink.init(out, job, worker.out_buf, task.size, true);
|
|
429
|
+
var region = RegionReader.init(zip, task.data_offset, task.compressed_size, worker.in_buf);
|
|
430
|
+
|
|
431
|
+
if (task.method == 0) {
|
|
432
|
+
_ = region.reader.streamRemaining(&sink.writer) catch |err| switch (err) {
|
|
433
|
+
error.WriteFailed => return sink.failureStatus(),
|
|
434
|
+
error.ReadFailed => return if (region.failed) .io_error else .corrupt,
|
|
435
|
+
};
|
|
436
|
+
} else {
|
|
437
|
+
const decompress = alloc.create(flate.Decompress) catch return .io_error;
|
|
438
|
+
defer alloc.destroy(decompress);
|
|
439
|
+
decompress.* = .init(®ion.reader, .raw, worker.window);
|
|
440
|
+
_ = decompress.reader.streamRemaining(&sink.writer) catch |err| switch (err) {
|
|
441
|
+
error.WriteFailed => return sink.failureStatus(),
|
|
442
|
+
error.ReadFailed => return if (region.failed) .io_error else .corrupt,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
sink.writer.flush() catch return sink.failureStatus();
|
|
446
|
+
|
|
447
|
+
if (sink.written != task.size) return .size_mismatch;
|
|
448
|
+
if (sink.crc.final() != task.crc) return .crc_mismatch;
|
|
449
|
+
return .ok;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
fn deflateTask(job: *Job, task: *Task, worker: *Worker) Status {
|
|
453
|
+
const in = os.openRead(task.source.?, &worker.scratch) orelse return .io_error;
|
|
454
|
+
defer in.close();
|
|
455
|
+
const out = os.createExclusive(task.output.?, &worker.scratch) orelse return .io_error;
|
|
456
|
+
defer out.close();
|
|
457
|
+
|
|
458
|
+
var sink = FileSink.init(out, job, worker.out_buf, null, false);
|
|
459
|
+
const compress = alloc.create(flate.Compress) catch return .io_error;
|
|
460
|
+
defer alloc.destroy(compress);
|
|
461
|
+
compress.* = flate.Compress.init(&sink.writer, worker.window, .raw, job.level) catch return sink.failureStatus();
|
|
462
|
+
|
|
463
|
+
var crc: Crc32 = .init();
|
|
464
|
+
var size: u64 = 0;
|
|
465
|
+
while (true) {
|
|
466
|
+
if (job.cancelled.load(.acquire)) return .cancelled;
|
|
467
|
+
const n = in.pread(worker.in_buf, size) orelse return .io_error;
|
|
468
|
+
if (n == 0) break;
|
|
469
|
+
const chunk = worker.in_buf[0..n];
|
|
470
|
+
crc.update(chunk);
|
|
471
|
+
size += n;
|
|
472
|
+
_ = job.done_bytes.fetchAdd(n, .monotonic);
|
|
473
|
+
compress.writer.writeAll(chunk) catch return sink.failureStatus();
|
|
474
|
+
}
|
|
475
|
+
compress.finish() catch return sink.failureStatus();
|
|
476
|
+
sink.writer.flush() catch return sink.failureStatus();
|
|
477
|
+
|
|
478
|
+
task.out_crc = crc.final();
|
|
479
|
+
task.out_compressed = sink.written;
|
|
480
|
+
task.out_size = size;
|
|
481
|
+
return .ok;
|
|
482
|
+
}
|