ruzip 0.2.1 → 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 758585b6a1cde1090fc03b6745059dd7558c7816ec9e297ffe8e671ea531e364
4
- data.tar.gz: 6d4f5b3aa6c10be28bce83bb18452180a3450da050b71a99f28a5871e28b580e
3
+ metadata.gz: a1c9e169da26aa71bb1aee7beb83cc3bd294280c915eb9fe326cce1de07cf4b4
4
+ data.tar.gz: 155882217b16ff068dcc669c3a1f0eb6b30a6d9ec6dcd70ce81f85b8c5de73d1
5
5
  SHA512:
6
- metadata.gz: 60ba2d2dd7c91b7ec5d17d4da55b02bf420e933f0adc5fd25954011313566a4a1bf70cdcb815404f67e806710e526b769c1eae4ed696ae38ce46a413726f46df
7
- data.tar.gz: 6a5b2bccfee9c2a467ad7c065287a6ae34761dc5e2e0fe6ee0bfbf2e421c17933f5d9c6a1adc51895cd83c6c445278288a02530b6e1755ec80658000289a0cbe
6
+ metadata.gz: e2f24eccfb60df789063186779cb9f785024e6dd42bbb05f38f85e33c9b0447a2afcac17fec92f842d93acc0fa7903663bd492fc0e78da6c0a12cbbf7e7b72f0
7
+ data.tar.gz: a1c5603e029df78f7b88683d6b3139c8755cb874a0ad5b8a6deff3642754946a15014800afdb8167c3ad9d66c166985845a9fb119bf3f17caa7562a360f38316
data/.gitignore CHANGED
@@ -16,3 +16,5 @@ Gemfile.lock
16
16
  /test/fixtures/accessible_epub_3.epub
17
17
  html/
18
18
  ext/release/
19
+ .gem_rbs_collection/
20
+ rbs_collection.lock.yaml
data/.gitlab-ci.yml CHANGED
@@ -1,15 +1,15 @@
1
- default:
2
- image: ruby:4.0
3
-
1
+ test:
2
+ image: ruby:$RUBY_VERSION
3
+ variables:
4
+ RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true'
4
5
  before_script:
5
6
  - apt-get update && apt-get install -y clang
6
7
  - curl https://sh.rustup.rs -sSf | sh -s -- -y
7
8
  - . "$HOME/.cargo/env"
8
9
  - gem update --system
9
10
  - bundle install
10
-
11
- test:
12
- variables:
13
- RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true'
14
11
  script:
15
12
  - bundle exec rake test
13
+ parallel:
14
+ matrix:
15
+ - RUBY_VERSION: ["3.2", "3.3", "3.4", "4.0"]
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.3.0] - 2026-10-01
2
+
3
+ * `RuZip::File#name` returns UTF-8 or ASCII-8BIT string
4
+ * `Ruzip::Writer.new` accepts mode argument
5
+ * `RuZip::Writer#start_file` accepts keyword arguments: `compression_method`, `compression_level`, `last_modified_time`
6
+ * `RuZip::Writer#raw_copy_file` is added
7
+ * `RuZip::Writer#append_file` is added
8
+
1
9
  ## [0.2.1] - 2026-09-18
2
10
 
3
11
  * Update crates
data/README.md CHANGED
@@ -30,6 +30,13 @@ If bundler is not being used to manage dependencies, install the gem by executin
30
30
 
31
31
  archive.by_name("path/in/zip/archive") # => RuZip::File
32
32
 
33
+ writer = RuZip::Writer.new("path/to/output.zip")
34
+ ["content1", "content2", "content3"].each_with_index do |content, i|
35
+ writer.start_file "inner/path/to/#{i}.txt"
36
+ writer.write content
37
+ end
38
+ writer.finish
39
+
33
40
  ## Development
34
41
 
35
42
  After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bundle exec rake console` for an interactive prompt that will allow you to experiment.
data/Steepfile ADDED
@@ -0,0 +1,37 @@
1
+ D = Steep::Diagnostic
2
+ #
3
+ target :lib do
4
+ signature "sig"
5
+ ignore_signature "sig/test"
6
+
7
+ check "lib" # Directory name
8
+ # check "path/to/source.rb" # File name
9
+ # check "app/models/**/*.rb" # Glob
10
+ # ignore "lib/templates/*.rb"
11
+
12
+ # library "pathname" # Standard libraries
13
+ # library "strong_json" # Gems
14
+
15
+ # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
16
+ # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
17
+ # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
18
+ # configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
19
+ # configure_code_diagnostics do |hash| # You can setup everything yourself
20
+ # hash[D::Ruby::NoMethod] = :information
21
+ # end
22
+ end
23
+
24
+ target :test do
25
+ unreferenced! # Skip type checking the `lib` code when types in `test` target is changed
26
+ signature "sig/test" # Put RBS files for tests under `sig/test`
27
+ check "test" # Type check Ruby scripts under `test`
28
+
29
+ # configure_code_diagnostics(D::Ruby.lenient) # Weak type checking for test code
30
+ configure_code_diagnostics do |hash|
31
+ hash[D::Ruby::MethodDefinitionMissing] = :warning
32
+ hash[D::Ruby::UnknownConstant] = :information
33
+ end
34
+
35
+ library "tmpdir", "tempfile", "fileutils" # Standard libraries
36
+ library "rubyzip"
37
+ end
data/ext/Cargo.lock CHANGED
@@ -73,9 +73,9 @@ dependencies = [
73
73
 
74
74
  [[package]]
75
75
  name = "cc"
76
- version = "1.4.6"
76
+ version = "1.5.1"
77
77
  source = "registry+https://github.com/rust-lang/crates.io-index"
78
- checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4"
78
+ checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
79
79
  dependencies = [
80
80
  "find-msvc-tools",
81
81
  "jobserver",
@@ -218,9 +218,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
218
218
 
219
219
  [[package]]
220
220
  name = "find-msvc-tools"
221
- version = "0.1.12"
221
+ version = "0.1.14"
222
222
  source = "registry+https://github.com/rust-lang/crates.io-index"
223
- checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
223
+ checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
224
224
 
225
225
  [[package]]
226
226
  name = "flate2"
@@ -338,9 +338,9 @@ dependencies = [
338
338
 
339
339
  [[package]]
340
340
  name = "js-sys"
341
- version = "0.3.105"
341
+ version = "0.3.106"
342
342
  source = "registry+https://github.com/rust-lang/crates.io-index"
343
- checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
343
+ checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
344
344
  dependencies = [
345
345
  "cfg-if",
346
346
  "futures-util",
@@ -578,7 +578,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
578
578
 
579
579
  [[package]]
580
580
  name = "ruzip"
581
- version = "0.2.1"
581
+ version = "0.3.0"
582
582
  dependencies = [
583
583
  "magnus",
584
584
  "rb-sys",
@@ -725,9 +725,9 @@ checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
725
725
 
726
726
  [[package]]
727
727
  name = "wasm-bindgen"
728
- version = "0.2.128"
728
+ version = "0.2.129"
729
729
  source = "registry+https://github.com/rust-lang/crates.io-index"
730
- checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
730
+ checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
731
731
  dependencies = [
732
732
  "cfg-if",
733
733
  "once_cell",
@@ -738,9 +738,9 @@ dependencies = [
738
738
 
739
739
  [[package]]
740
740
  name = "wasm-bindgen-macro"
741
- version = "0.2.128"
741
+ version = "0.2.129"
742
742
  source = "registry+https://github.com/rust-lang/crates.io-index"
743
- checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
743
+ checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
744
744
  dependencies = [
745
745
  "quote",
746
746
  "wasm-bindgen-macro-support",
@@ -748,9 +748,9 @@ dependencies = [
748
748
 
749
749
  [[package]]
750
750
  name = "wasm-bindgen-macro-support"
751
- version = "0.2.128"
751
+ version = "0.2.129"
752
752
  source = "registry+https://github.com/rust-lang/crates.io-index"
753
- checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
753
+ checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
754
754
  dependencies = [
755
755
  "bumpalo",
756
756
  "proc-macro2",
@@ -761,9 +761,9 @@ dependencies = [
761
761
 
762
762
  [[package]]
763
763
  name = "wasm-bindgen-shared"
764
- version = "0.2.128"
764
+ version = "0.2.129"
765
765
  source = "registry+https://github.com/rust-lang/crates.io-index"
766
- checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
766
+ checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
767
767
  dependencies = [
768
768
  "unicode-ident",
769
769
  ]
data/ext/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ruzip"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  edition = "2024"
5
5
  authors = ["Kitaiti Makoto <KitaitiMakoto@gmail.com>"]
6
6
  publish = false
data/ext/src/lib.rs CHANGED
@@ -1,8 +1,13 @@
1
- use magnus::{Error, Integer, RString, Ruby, Value, function, method, prelude::*};
2
- use std::cell::RefCell;
1
+ use magnus::{
2
+ Error, Integer, RHash, RString, Ruby, Symbol, Time, Value, function, method,
3
+ prelude::*,
4
+ scan_args::{KwArgs, get_kwargs, scan_args},
5
+ typed_data::Obj,
6
+ };
7
+ use std::cell::{RefCell, RefMut};
3
8
  use std::fs;
4
9
  use std::io::{Read, Write};
5
- use std::os::fd::FromRawFd;
10
+ use std::str;
6
11
  use std::sync::{Arc, Mutex};
7
12
  use zip::{ZipArchive, ZipWriter};
8
13
 
@@ -15,12 +20,27 @@ fn map_err(err: impl std::error::Error, ruby: &Ruby) -> Error {
15
20
  #[magnus::wrap(class = "RuZip::Archive", free_immediately, size)]
16
21
  struct Archive(Arc<Mutex<ZipArchive<fs::File>>>);
17
22
 
23
+ #[cfg(windows)]
24
+ fn file_from_io(io: Value, ruby: &Ruby) -> Result<fs::File> {
25
+ todo!();
26
+ }
27
+
28
+ #[cfg(not(windows))]
29
+ fn file_from_io(io: Value, ruby: &Ruby) -> Result<fs::File> {
30
+ use std::os::fd::BorrowedFd;
31
+
32
+ let fileno: Integer = io.funcall_public("fileno", ())?;
33
+ let raw_fd = fileno.to_i32()?;
34
+ let fd = unsafe { BorrowedFd::borrow_raw(raw_fd) }
35
+ .try_clone_to_owned()
36
+ .map_err(|e| map_err(e, ruby))?;
37
+ Ok(fs::File::from(fd))
38
+ }
39
+
18
40
  impl Archive {
19
41
  fn new(ruby: &Ruby, r_io: Value) -> Result<Self> {
20
42
  let io = if r_io.is_kind_of(ruby.class_io()) {
21
- let fileno: Integer = r_io.funcall_public("fileno", ())?;
22
- let raw_fd = fileno.to_i32()?;
23
- unsafe { fs::File::from_raw_fd(raw_fd) }
43
+ file_from_io(r_io, ruby)?
24
44
  } else if r_io.respond_to("to_path", false)? {
25
45
  fs::File::open(
26
46
  r_io.funcall_public::<&str, (), RString>("to_path", ())?
@@ -58,8 +78,8 @@ impl Archive {
58
78
  }
59
79
  }
60
80
 
61
- fn by_name(ruby: &Ruby, rb_self: &Self, name: String) -> Result<Option<File>> {
62
- let name_bytes = name.as_bytes();
81
+ fn by_name(ruby: &Ruby, rb_self: &Self, name: RString) -> Result<Option<File>> {
82
+ let name_bytes = unsafe { name.as_slice() };
63
83
  let mut archive = rb_self.0.lock().map_err(|e| map_err(e, ruby))?;
64
84
  // TODO: Cache entries
65
85
  for i in 0..archive.len() {
@@ -76,18 +96,14 @@ impl Archive {
76
96
  struct File(Arc<Mutex<ZipArchive<fs::File>>>, usize);
77
97
 
78
98
  impl File {
79
- fn name(ruby: &Ruby, rb_self: &Self) -> Result<String> {
80
- String::from_utf8(
81
- rb_self
82
- .0
83
- .lock()
84
- .map_err(|e| map_err(e, ruby))?
85
- .by_index(rb_self.1)
86
- .map_err(|e| map_err(e, ruby))?
87
- .name_raw()
88
- .into(),
89
- )
90
- .map_err(|e| map_err(e, ruby))
99
+ fn name(ruby: &Ruby, rb_self: &Self) -> Result<RString> {
100
+ let mut archive = rb_self.0.lock().map_err(|e| map_err(e, ruby))?;
101
+ let file = archive.by_index(rb_self.1).map_err(|e| map_err(e, ruby))?;
102
+ let bytes = file.name_raw();
103
+ Ok(match str::from_utf8(bytes) {
104
+ Ok(s) => ruby.str_new(s),
105
+ Err(_) => ruby.str_from_slice(bytes),
106
+ })
91
107
  }
92
108
 
93
109
  fn size(ruby: &Ruby, rb_self: &Self) -> Result<u64> {
@@ -135,10 +151,122 @@ impl File {
135
151
  #[magnus::wrap(class = "RuZip::Writer", free_immediately, size)]
136
152
  struct Writer(RefCell<Option<ZipWriter<fs::File>>>);
137
153
 
154
+ enum WriterMode {
155
+ Write,
156
+ Append,
157
+ }
158
+
159
+ impl TryFrom<(&str, &Ruby)> for WriterMode {
160
+ type Error = Error;
161
+
162
+ fn try_from(value: (&str, &Ruby)) -> Result<Self> {
163
+ let (mode, ruby) = value;
164
+ Ok(match mode {
165
+ "w" => WriterMode::Write,
166
+ "a" => WriterMode::Append,
167
+ _ => {
168
+ return Err(Error::new(
169
+ ruby.exception_runtime_error(),
170
+ format!("Unsupported mode: {mode}"),
171
+ ));
172
+ }
173
+ })
174
+ }
175
+ }
176
+
177
+ fn compression_method_from_symbol(sym: Symbol, ruby: &Ruby) -> Result<zip::CompressionMethod> {
178
+ use zip::CompressionMethod::*;
179
+ Ok(match sym.name()?.as_ref() {
180
+ "stored" => Stored,
181
+ "deflated" => Deflated,
182
+ "deflated64" => Deflate64,
183
+ "bzip2" => Bzip2,
184
+ "aes" => Aes,
185
+ "zstd" => Zstd,
186
+ "lzma" => Lzma,
187
+ "xz" => Xz,
188
+ "ppmd" => Ppmd,
189
+ _ => {
190
+ return Err(Error::new(
191
+ ruby.exception_arg_error(),
192
+ format!("Unsupported compression method: {}", sym),
193
+ ));
194
+ }
195
+ })
196
+ }
197
+
198
+ fn date_time_from_time(time: Time, ruby: &Ruby) -> Result<zip::DateTime> {
199
+ zip::DateTime::from_date_and_time(
200
+ time.funcall("year", ())?,
201
+ time.funcall("month", ())?,
202
+ time.funcall("day", ())?,
203
+ time.funcall("hour", ())?,
204
+ time.funcall("min", ())?,
205
+ time.funcall("sec", ())?,
206
+ )
207
+ .map_err(|e| map_err(e, ruby))
208
+ }
209
+
210
+ // TODO: Move to Grey Knights
211
+ macro_rules! opop_t {
212
+ ($($type:ty),+) => { ($(Option<Option<$type>>),+) }
213
+ }
214
+ macro_rules! opt_t {
215
+ ($($type:ty),+) => { ($(Option<$type>),+) };
216
+ }
217
+
218
+ type StartFileKwArgsOptionalInput = opop_t!(Symbol, i64, Time);
219
+ type StartFileKwArgsOptionalOutput = opt_t!(Symbol, i64, Time);
220
+ type StartFileKwArgs = KwArgs<(), StartFileKwArgsOptionalOutput, ()>;
221
+ static START_FILE_KW_ARGS_NAME_OPTIONAL: &[&str] = &[
222
+ "compression_method",
223
+ "compression_level",
224
+ "last_modified_time",
225
+ ];
226
+
227
+ fn flatten_optional_keywords(
228
+ optional: StartFileKwArgsOptionalInput,
229
+ ) -> StartFileKwArgsOptionalOutput {
230
+ (
231
+ optional.0.flatten(),
232
+ optional.1.flatten(),
233
+ optional.2.flatten(),
234
+ )
235
+ }
236
+
237
+ fn get_start_file_kwargs(kw: RHash) -> Result<StartFileKwArgs> {
238
+ let kwargs = get_kwargs::<_, (), StartFileKwArgsOptionalInput, ()>(
239
+ kw,
240
+ &[],
241
+ START_FILE_KW_ARGS_NAME_OPTIONAL,
242
+ )?;
243
+ Ok(KwArgs {
244
+ required: kwargs.required,
245
+ optional: flatten_optional_keywords(kwargs.optional),
246
+ splat: kwargs.splat,
247
+ })
248
+ }
249
+
138
250
  impl Writer {
139
- fn new(ruby: &Ruby, path: String) -> Result<Self> {
140
- let file = fs::File::create(path).map_err(|e| map_err(e, ruby))?;
141
- let writer = ZipWriter::new(file);
251
+ // TODO: Accept arg the same to Archive.new
252
+ // TODO: Accept block
253
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self> {
254
+ let args = scan_args::<(String,), (Option<String>,), (), (), (), ()>(args)?;
255
+ let path = args.required.0;
256
+ let mode = args.optional.0;
257
+ let mut options = fs::OpenOptions::new();
258
+ let writer_mode = (mode.as_deref().unwrap_or("w"), ruby).try_into()?;
259
+ let file = match writer_mode {
260
+ WriterMode::Write => options.create(true),
261
+ WriterMode::Append => options.read(true),
262
+ }
263
+ .write(true)
264
+ .open(path)
265
+ .map_err(|e| map_err(e, ruby))?;
266
+ let writer = match writer_mode {
267
+ WriterMode::Write => ZipWriter::new(file),
268
+ WriterMode::Append => ZipWriter::new_append(file).map_err(|e| map_err(e, ruby))?,
269
+ };
142
270
  Ok(Self(RefCell::new(Some(writer))))
143
271
  }
144
272
 
@@ -157,25 +285,115 @@ impl Writer {
157
285
  }
158
286
  }
159
287
 
160
- fn start_file(ruby: &Ruby, rb_self: &Self, name: String) -> Result<()> {
161
- let mut writer = rb_self.0.borrow_mut();
162
- let writer = writer
163
- .as_mut()
164
- .ok_or_else(|| Error::new(ruby.exception_runtime_error(), "Already finished"))?;
165
- writer
166
- .start_file(name, zip::write::SimpleFileOptions::default())
288
+ fn start_file(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<()> {
289
+ let args = scan_args::<(String,), (), (), (), _, ()>(args)?;
290
+ let name = args.required.0;
291
+ let (compression_method, compression_level, last_modified_time) =
292
+ get_start_file_kwargs(args.keywords)?.optional;
293
+
294
+ let mut option =
295
+ zip::write::SimpleFileOptions::default().compression_level(compression_level);
296
+ if let Some(method) = compression_method {
297
+ option = option.compression_method(compression_method_from_symbol(method, ruby)?);
298
+ }
299
+ if let Some(mod_time) = last_modified_time {
300
+ option = option.last_modified_time(date_time_from_time(mod_time, ruby)?);
301
+ }
302
+
303
+ Self::writer(ruby, rb_self)?
304
+ .start_file(name, option)
167
305
  .map_err(|e| map_err(e, ruby))?;
168
306
  Ok(())
169
307
  }
170
308
 
171
- fn write(ruby: &Ruby, rb_self: &Self, buf: String) -> Result<()> {
172
- let mut writer = rb_self.0.borrow_mut();
173
- let writer = writer.as_mut()
174
- .ok_or_else(|| Error::new(ruby.exception_runtime_error(), "Already finished"))?;
175
- writer.write_all(buf.as_bytes())
309
+ fn write(ruby: &Ruby, rb_self: &Self, buf: RString) -> Result<()> {
310
+ Self::writer(ruby, rb_self)?
311
+ .write_all(unsafe { buf.as_slice() })
176
312
  .map_err(|e| map_err(e, ruby))?;
177
313
  Ok(())
178
314
  }
315
+
316
+ fn raw_copy_file(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<()> {
317
+ let args = scan_args::<(Obj<File>,), (), (), (), _, ()>(args)?;
318
+ let file = &*args.required.0;
319
+ let kwargs = get_kwargs::<
320
+ _,
321
+ (),
322
+ (
323
+ Option<Option<String>>,
324
+ Option<Option<Time>>,
325
+ Option<Option<u32>>,
326
+ ),
327
+ (),
328
+ >(
329
+ args.keywords,
330
+ &[],
331
+ &["name", "last_modified_time", "unix_mode"],
332
+ )?;
333
+ let name = kwargs.optional.0.flatten();
334
+ let last_modified_time = kwargs.optional.1.flatten();
335
+ let unix_mode = kwargs.optional.2.flatten();
336
+ if last_modified_time.is_none() && unix_mode.is_some() {
337
+ return Err(Error::new(
338
+ ruby.exception_arg_error(),
339
+ "last_modified_time is nil but unix_mode is passed",
340
+ ));
341
+ }
342
+
343
+ let mut writer = Self::writer(ruby, rb_self)?;
344
+ let mut archive = file.0.lock().map_err(|e| map_err(e, ruby))?;
345
+ let zip_file = archive.by_index(file.1).map_err(|e| map_err(e, ruby))?;
346
+ match (name, last_modified_time) {
347
+ (Some(_), Some(_)) => {
348
+ return Err(Error::new(
349
+ ruby.exception_arg_error(),
350
+ "name and last_modified_time cannot be passed at the same time",
351
+ ));
352
+ }
353
+ (Some(n), None) => writer.raw_copy_file_rename(zip_file, n),
354
+ (None, Some(t)) => {
355
+ let mtime = date_time_from_time(t, ruby)?;
356
+ writer.raw_copy_file_touch(zip_file, mtime, unix_mode)
357
+ }
358
+ (None, None) => writer.raw_copy_file(zip_file),
359
+ }
360
+ .map_err(|e| map_err(e, ruby))?;
361
+ Ok(())
362
+ }
363
+
364
+ fn append_file(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<()> {
365
+ let args = scan_args::<(String, RString), (), (), (), _, ()>(args)?;
366
+ let (name, buf) = args.required;
367
+ let (compression_method, compression_level, last_modified_time) =
368
+ get_start_file_kwargs(args.keywords)?.optional;
369
+
370
+ let mut option =
371
+ zip::write::SimpleFileOptions::default().compression_level(compression_level);
372
+ if let Some(method) = compression_method {
373
+ option = option.compression_method(compression_method_from_symbol(method, ruby)?);
374
+ }
375
+ if let Some(mod_time) = last_modified_time {
376
+ option = option.last_modified_time(date_time_from_time(mod_time, ruby)?);
377
+ }
378
+
379
+ let mut writer = Self::writer(ruby, rb_self)?;
380
+ writer
381
+ .start_file(name, option)
382
+ .map_err(|e| map_err(e, ruby))?;
383
+ writer
384
+ .write_all(unsafe { buf.as_slice() })
385
+ .map_err(|e| map_err(e, ruby))?;
386
+ Ok(())
387
+ }
388
+
389
+ fn writer<'a>(ruby: &Ruby, rb_self: &'a Self) -> Result<RefMut<'a, ZipWriter<fs::File>>> {
390
+ let writer = rb_self
391
+ .0
392
+ .try_borrow_mut()
393
+ .map_err(|_| Error::new(ruby.exception_runtime_error(), "Already finished"))?;
394
+ RefMut::filter_map(writer, Option::as_mut)
395
+ .map_err(|_| Error::new(ruby.exception_runtime_error(), "Already finished"))
396
+ }
179
397
  }
180
398
 
181
399
  #[magnus::init]
@@ -195,10 +413,12 @@ fn init(ruby: &Ruby) -> Result<()> {
195
413
  file_class.define_method("read", method!(File::read, 0))?;
196
414
 
197
415
  let writer_class = module.define_class("Writer", ruby.class_object())?;
198
- writer_class.define_singleton_method("new", function!(Writer::new, 1))?;
416
+ writer_class.define_singleton_method("new", function!(Writer::new, -1))?;
199
417
  writer_class.define_method("finish", method!(Writer::finish, 0))?;
200
- writer_class.define_method("start_file", method!(Writer::start_file, 1))?;
418
+ writer_class.define_method("start_file", method!(Writer::start_file, -1))?;
201
419
  writer_class.define_method("write", method!(Writer::write, 1))?;
420
+ writer_class.define_method("raw_copy_file", method!(Writer::raw_copy_file, -1))?;
421
+ writer_class.define_method("append_file", method!(Writer::append_file, -1))?;
202
422
 
203
423
  Ok(())
204
424
  }
@@ -0,0 +1,20 @@
1
+ # Download sources
2
+ sources:
3
+ - type: git
4
+ name: ruby/gem_rbs_collection
5
+ remote: https://github.com/ruby/gem_rbs_collection.git
6
+ revision: main
7
+ repo_dir: gems
8
+
9
+ # You can specify local directories as sources also.
10
+ # - type: local
11
+ # path: path/to/your/local/repository
12
+
13
+ # A directory to install the downloaded RBSs
14
+ path: .gem_rbs_collection
15
+
16
+ gems:
17
+ # If you want to avoid installing rbs files for gems, you can specify them here.
18
+ # - name: GEM_NAME
19
+ # ignore: true
20
+ - name: rubyzip
data/ruzip.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Library to support the reading and writing of zip files. A wrapper of Rust's zip crate."
13
13
  spec.homepage = "https://gitlab.com/KitaitiMakoto/ruzip"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.1.0"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
16
  spec.required_rubygems_version = ">= 3.3.11"
17
17
 
18
18
  spec.metadata["homepage_uri"] = spec.homepage
@@ -37,6 +37,7 @@ Gem::Specification.new do |spec|
37
37
  spec.add_development_dependency "rdoc"
38
38
  spec.add_development_dependency "rbs"
39
39
  spec.add_development_dependency "archive-zip"
40
+ spec.add_development_dependency "rubyzip"
40
41
  spec.add_development_dependency "ruby-lsp"
41
42
  spec.add_development_dependency "steep"
42
43
 
data/sig/ruzip.rbs CHANGED
@@ -1,12 +1,13 @@
1
1
  module RuZip
2
2
  class Archive
3
3
  def self.new: (IO | path) -> instance
4
- def len: -> Integer
4
+ def length: -> Integer
5
5
  def by_index: (Integer) -> File
6
6
  def by_name: (String) -> File
7
7
  end
8
8
 
9
9
  class File
10
+ # UTF-8 when valid, ASCII-8BIT otherwise
10
11
  def name: -> String
11
12
 
12
13
  # The size of decompressed file
@@ -18,9 +19,22 @@ module RuZip
18
19
  end
19
20
 
20
21
  class Writer
21
- def self.new: (String) -> instance
22
+ def self.new: (String path, ?String mode) -> instance
22
23
  def finish: -> Archive
23
- def start_file: (String) -> void
24
- def write: (String) -> void
24
+ def start_file: (
25
+ String name,
26
+ ?compression_method: Symbol?,
27
+ ?compression_level: Integer?,
28
+ ?last_modified_date: Time?
29
+ ) -> void
30
+ def write: (String buf) -> void
31
+ def raw_copy_file: (File file, ?name: String?, ?last_modified_time: Time?, ?unix_mode: Integer?) -> void
32
+ def append_file: (
33
+ String name,
34
+ String buf,
35
+ ?compression_method: Symbol?,
36
+ ?compression_level: Integer?,
37
+ ?last_modified_date: Time?
38
+ ) -> void
25
39
  end
26
40
  end
@@ -0,0 +1,24 @@
1
+ module Archive
2
+ class Zip
3
+ include Enumerable[Entry]
4
+
5
+ def self.open: (path, ?(String | Symbol) mode) { (instance) -> untyped } -> untyped
6
+ def each: { (Entry) -> untyped } -> untyped
7
+ | -> Enumerator[Entry]
8
+ def add_entry: (Entry) -> self
9
+ alias << add_entry
10
+
11
+ module Entry
12
+ def zip_path: -> String
13
+ def mtime: -> Time
14
+
15
+ class File
16
+ include Entry
17
+
18
+ def initialize: (String) -> void
19
+ def file_data: -> IO::Like
20
+ def file_data=: (IO::LikeHelpers::AbstractIO) -> void
21
+ end
22
+ end
23
+ end
24
+ end
data/sig/test/gem.rbs ADDED
@@ -0,0 +1,12 @@
1
+ module Gem
2
+ class Specification
3
+ def self.load: (String) -> instance
4
+ def file_name: -> String
5
+ def full_name: -> String
6
+ end
7
+
8
+ class Platform
9
+ def self.local: -> instance
10
+ def to_s: -> String
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ class IO
2
+ class Like
3
+ def read: (?Integer? length, ?string buffer) -> String?
4
+ end
5
+
6
+ module LikeHelpers
7
+ class AbstractIO
8
+ end
9
+
10
+ class DelegatedIO < AbstractIO
11
+ def initialize: (_Reader) -> void
12
+ end
13
+
14
+ class IOWrapper < DelegatedIO
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ module Test
2
+ module Unit
3
+ class TestCase
4
+ include Assertions
5
+ include TestCasePendingSupport
6
+
7
+ def self.test: (String) { (instance) -> void } -> void
8
+ def setup: { () -> void } -> void
9
+ def self.setup: { () -> void } -> void
10
+ end
11
+
12
+ module Assertions
13
+ def assert: (boolish) -> void
14
+ | { () -> void } -> void
15
+ def assert_equal: (untyped expected, untyped actual) -> void
16
+ def assert_nil: (untyped) -> void
17
+ def assert_kind_of: (Module, untyped) -> void
18
+ def assert_instance_of: (Class, untyped) -> void
19
+ def assert_path_exist: (path) -> void
20
+ def assert_raise: (?Class) { () -> void } -> void
21
+ def assert_raise_kind_of: (Class) { () -> void } -> void
22
+ end
23
+
24
+ module TestCasePendingSupport
25
+ def pend: -> void
26
+ end
27
+ end
28
+ end
data/sig/test/test.rbs ADDED
@@ -0,0 +1,26 @@
1
+ module Test::Unit
2
+ class TestCase
3
+ extend Assertions
4
+
5
+ def self.fixture_path: (path) -> String
6
+ def self.pend: -> void
7
+ def fixture_path: (path) -> String
8
+ end
9
+ end
10
+
11
+ class TestArchive < Test::Unit::TestCase
12
+ self.@fixture: String
13
+ @fixture: String
14
+ end
15
+
16
+ class TestFile < Test::Unit::TestCase
17
+ self.@archive: RuZip::Archive
18
+ end
19
+
20
+ class TestWriter < Test::Unit::TestCase
21
+ end
22
+
23
+ class TestPackage < Test::Unit::TestCase
24
+ def test_build: -> void
25
+ def test_install: -> void
26
+ end
data/test/helper.rb CHANGED
@@ -3,6 +3,9 @@ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
3
3
  require "ruzip"
4
4
  require "test-unit"
5
5
  require "test/unit/notify"
6
+ require "tmpdir"
7
+ require "archive/zip"
8
+ require "rubyzip"
6
9
 
7
10
  class Test::Unit::TestCase
8
11
  def fixture_path(component)
data/test/test_archive.rb CHANGED
@@ -21,6 +21,7 @@ class TestArchive < Test::Unit::TestCase
21
21
  end
22
22
 
23
23
  test "new with closed file" do
24
+ # @type var io: File?
24
25
  io = nil
25
26
  File.open @fixture do |file|
26
27
  io = file
data/test/test_file.rb CHANGED
@@ -21,4 +21,43 @@ XML
21
21
  test "retrieve non-existent" do
22
22
  assert_nil @archive.by_name("non-existent")
23
23
  end
24
+
25
+ test "retrieve multi byte name" do
26
+ Dir.mktmpdir do |dir|
27
+ path = "test.zip"
28
+ inner_path = "日本語.txt"
29
+ content = "今日は"
30
+ Archive::Zip.open path, :w do |archive|
31
+ entry = Archive::Zip::Entry::File.new(inner_path)
32
+ entry.file_data = IO::LikeHelpers::IOWrapper.new(StringIO.new(content))
33
+ archive << entry
34
+ end
35
+
36
+ file = RuZip::Archive.new(path).by_name(inner_path)
37
+ assert_kind_of RuZip::File, file
38
+ assert_equal "日本語.txt", file.name
39
+ end
40
+ end
41
+
42
+ test "retrieve file with non-UTF-8 name" do
43
+ Dir.mktmpdir do |dir|
44
+ path = "test.zip"
45
+ inner_path = "日本語.txt"
46
+ inner_path_sjis = inner_path.encode("Shift_JIS")
47
+ content = "今日は"
48
+ Archive::Zip.open path, :w do |archive|
49
+ entry = Archive::Zip::Entry::File.new(inner_path_sjis)
50
+ entry.file_data = IO::LikeHelpers::IOWrapper.new(StringIO.new(content))
51
+ archive << entry
52
+ end
53
+
54
+ archive = RuZip::Archive.new(path)
55
+
56
+ assert_nil archive.by_name("日本語.txt")
57
+
58
+ file = archive.by_name(inner_path_sjis)
59
+ assert_kind_of RuZip::File, file
60
+ assert_equal inner_path_sjis, file.name.force_encoding("Shift_JIS")
61
+ end
62
+ end
24
63
  end
data/test/test_writer.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  require_relative "helper"
2
- require "tmpdir"
3
- require "archive/zip"
4
2
 
5
3
  class TestWriter < Test::Unit::TestCase
6
4
  test "new with path" do
@@ -35,10 +33,194 @@ class TestWriter < Test::Unit::TestCase
35
33
  writer.finish
36
34
 
37
35
  Archive::Zip.open path do |archive|
38
- file = archive.each.first
36
+ file = archive.each.first #: Archive::Zip::Entry::File
39
37
  assert_equal inner_path, file.zip_path
40
38
  assert_equal content, file.file_data.read(content.length)
41
39
  end
42
40
  end
43
41
  end
42
+
43
+ test "add content multi times" do
44
+ Dir.mktmpdir do |dir|
45
+ path = File.join(dir, "output.zip")
46
+ writer = RuZip::Writer.new(path)
47
+ inner_path = "hello.txt"
48
+ writer.start_file(inner_path)
49
+ n = 3
50
+ n.times do |i|
51
+ writer.write i.to_s
52
+ end
53
+ writer.finish
54
+
55
+ Archive::Zip.open path do |archive|
56
+ file = archive.each.first #: Archive::Zip::Entry::File
57
+ assert_equal inner_path, file.zip_path
58
+ assert_equal "012", file.file_data.read(n)
59
+ end
60
+ end
61
+ end
62
+
63
+ test "add content with compression option" do
64
+ Dir.mktmpdir do |dir|
65
+ path = File.join(dir, "output.zip")
66
+ writer = RuZip::Writer.new(path)
67
+ content = "Hello, World!" * 100
68
+ inner_path = "hello.txt"
69
+ inner_path_stored = "hello-xz.txt"
70
+ writer.start_file(inner_path)
71
+ writer.write content
72
+ writer.start_file(inner_path_stored, compression_method: :stored)
73
+ writer.write content
74
+ writer.finish
75
+
76
+ Zip::File.open path do |archive|
77
+ entries = archive.each.to_a
78
+ hello = entries[0]
79
+ hello_stored= entries[1]
80
+ assert do
81
+ hello.compressed_size < hello_stored.compressed_size
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ test "new with append mode add file" do
88
+ Dir.mktmpdir do |dir|
89
+ path = File.join(dir, "accessible_epub_3.epub")
90
+ FileUtils.copy fixture_path("accessible_epub_3.epub"), path
91
+
92
+ writer = RuZip::Writer.new(path, "a")
93
+ inner_path = "hello.txt"
94
+ writer.start_file(inner_path)
95
+ content = "Hello, World!"
96
+ writer.write content
97
+ writer.finish
98
+
99
+ Zip::File.open path do |archive|
100
+ entries = archive.each.to_a #: [Zip::Entry]
101
+ assert_equal 39, entries.length
102
+
103
+ file = entries.last
104
+ assert_equal inner_path, file.name
105
+ assert_equal content, file.get_input_stream.read
106
+ end
107
+ end
108
+ end
109
+
110
+ test "append_file" do
111
+ Dir.mktmpdir do |dir|
112
+ path = File.join(dir, "output.zip")
113
+ writer = RuZip::Writer.new(path)
114
+ inner_path = "hello.txt"
115
+ content = "Hello, World!"
116
+ writer.append_file(inner_path, content)
117
+ writer.finish
118
+
119
+ Archive::Zip.open path do |archive|
120
+ file = archive.each.first #: Archive::Zip::Entry::File
121
+ assert_equal inner_path, file.zip_path
122
+ assert_equal content, file.file_data.read(content.length)
123
+ end
124
+ end
125
+ end
126
+
127
+ test "raw_copy_file" do
128
+ Dir.mktmpdir do |dir|
129
+ input = File.join(dir, "input.zip")
130
+ output = File.join(dir, "output.zip")
131
+ inner_path = "hello.txt"
132
+ content = "Hello, World!"
133
+ Archive::Zip.open input, :w do |archive|
134
+ entry = Archive::Zip::Entry::File.new(inner_path)
135
+ entry.file_data = IO::LikeHelpers::IOWrapper.new(StringIO.new(content))
136
+ archive << entry
137
+ end
138
+
139
+ file = RuZip::Archive.new(input).by_index(0)
140
+ writer = RuZip::Writer.new(output)
141
+ writer.raw_copy_file file
142
+ writer.finish
143
+
144
+ Archive::Zip.open output do |archive|
145
+ copy = archive.each.first #: Archive::Zip::Entry::File
146
+ assert_equal inner_path, copy.zip_path
147
+ assert_equal content, copy.file_data.read(content.length)
148
+ end
149
+ end
150
+ end
151
+
152
+ test "raw_copy_file with name" do
153
+ Dir.mktmpdir do |dir|
154
+ input = File.join(dir, "input.zip")
155
+ output = File.join(dir, "output.zip")
156
+ inner_path = "hello.txt"
157
+ content = "Hello, World!"
158
+ Archive::Zip.open input, :w do |archive|
159
+ entry = Archive::Zip::Entry::File.new(inner_path)
160
+ entry.file_data = IO::LikeHelpers::IOWrapper.new(StringIO.new(content))
161
+ archive << entry
162
+ end
163
+
164
+ renamed_path = "renamed.txt"
165
+ file = RuZip::Archive.new(input).by_index(0)
166
+ writer = RuZip::Writer.new(output)
167
+ writer.raw_copy_file file, name: renamed_path
168
+ writer.finish
169
+
170
+ Archive::Zip.open output do |archive|
171
+ copy = archive.each.first #: Archive::Zip::Entry::File
172
+ assert_equal renamed_path, copy.zip_path
173
+ assert_equal content, copy.file_data.read(content.length)
174
+ end
175
+ end
176
+ end
177
+
178
+ test "raw_copy_file with timestamp" do
179
+ Dir.mktmpdir do |dir|
180
+ input = File.join(dir, "input.zip")
181
+ output = File.join(dir, "output.zip")
182
+ inner_path = "hello.txt"
183
+ content = "Hello, World!"
184
+ Archive::Zip.open input, :w do |archive|
185
+ entry = Archive::Zip::Entry::File.new(inner_path)
186
+ entry.file_data = IO::LikeHelpers::IOWrapper.new(StringIO.new(content))
187
+ archive << entry
188
+ end
189
+
190
+ file = RuZip::Archive.new(input).by_index(0)
191
+ writer = RuZip::Writer.new(output)
192
+ writer.raw_copy_file file, last_modified_time: Time.new(2026, 1, 1)
193
+ writer.finish
194
+
195
+ Archive::Zip.open output do |archive|
196
+ copy = archive.each.first #: Archive::Zip::Entry::File
197
+ assert_equal Time.new(2026, 1, 1), copy.mtime
198
+ end
199
+ end
200
+ end
201
+
202
+ test "raw_copy_file with nil timestamp" do
203
+ Dir.mktmpdir do |dir|
204
+ input = File.join(dir, "input.zip")
205
+ output = File.join(dir, "output.zip")
206
+ inner_path = "hello.txt"
207
+ content = "Hello, World!"
208
+ Archive::Zip.open input, :w do |archive|
209
+ entry = Archive::Zip::Entry::File.new(inner_path)
210
+ entry.file_data = IO::LikeHelpers::IOWrapper.new(StringIO.new(content))
211
+ archive << entry
212
+ end
213
+ orig = Archive::Zip.open(input) {|archive| archive.each.first} #: Archive::Zip::Entry::File
214
+
215
+ file = RuZip::Archive.new(input).by_index(0)
216
+ writer = RuZip::Writer.new(output)
217
+ writer.raw_copy_file file, last_modified_time: nil
218
+ writer.finish
219
+
220
+ Archive::Zip.open output do |archive|
221
+ copy = archive.each.first #: Archive::Zip::Entry::File
222
+ assert_equal orig.mtime, copy.mtime
223
+ end
224
+ end
225
+ end
44
226
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruzip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kitaiti Makoto
@@ -135,6 +135,20 @@ dependencies:
135
135
  - - ">="
136
136
  - !ruby/object:Gem::Version
137
137
  version: '0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rubyzip
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
138
152
  - !ruby/object:Gem::Dependency
139
153
  name: ruby-lsp
140
154
  requirement: !ruby/object:Gem::Requirement
@@ -180,11 +194,18 @@ files:
180
194
  - MIT-LICENSE
181
195
  - README.md
182
196
  - Rakefile
197
+ - Steepfile
183
198
  - ext/Cargo.lock
184
199
  - ext/Cargo.toml
185
200
  - ext/src/lib.rs
201
+ - rbs_collection.yaml
186
202
  - ruzip.gemspec
187
203
  - sig/ruzip.rbs
204
+ - sig/test/archive-zip.rbs
205
+ - sig/test/gem.rbs
206
+ - sig/test/io-like.rbs
207
+ - sig/test/test-unit.rbs
208
+ - sig/test/test.rbs
188
209
  - test/fixtures/.gitkeep
189
210
  - test/helper.rb
190
211
  - test/test_archive.rb
@@ -205,7 +226,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
205
226
  requirements:
206
227
  - - ">="
207
228
  - !ruby/object:Gem::Version
208
- version: 3.1.0
229
+ version: 3.2.0
209
230
  required_rubygems_version: !ruby/object:Gem::Requirement
210
231
  requirements:
211
232
  - - ">="