ziperu 0.1.1-x86_64-linux-musl

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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE-rubyzip.txt +24 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +276 -0
  6. data/lib/rubyzip.rb +4 -0
  7. data/lib/zip/central_directory.rb +5 -0
  8. data/lib/zip/compressor.rb +5 -0
  9. data/lib/zip/constants.rb +5 -0
  10. data/lib/zip/crypto/aes_encryption.rb +5 -0
  11. data/lib/zip/crypto/decrypted_io.rb +5 -0
  12. data/lib/zip/crypto/encryption.rb +5 -0
  13. data/lib/zip/crypto/null_encryption.rb +5 -0
  14. data/lib/zip/crypto/traditional_encryption.rb +5 -0
  15. data/lib/zip/crypto.rb +5 -0
  16. data/lib/zip/decompressor.rb +5 -0
  17. data/lib/zip/deflater.rb +5 -0
  18. data/lib/zip/dirtyable.rb +5 -0
  19. data/lib/zip/dos_time.rb +5 -0
  20. data/lib/zip/entry.rb +5 -0
  21. data/lib/zip/entry_set.rb +5 -0
  22. data/lib/zip/errors.rb +5 -0
  23. data/lib/zip/extra_field/aes.rb +5 -0
  24. data/lib/zip/extra_field/generic.rb +5 -0
  25. data/lib/zip/extra_field/ntfs.rb +5 -0
  26. data/lib/zip/extra_field/old_unix.rb +5 -0
  27. data/lib/zip/extra_field/universal_time.rb +5 -0
  28. data/lib/zip/extra_field/unix.rb +5 -0
  29. data/lib/zip/extra_field/unknown.rb +5 -0
  30. data/lib/zip/extra_field/zip64.rb +5 -0
  31. data/lib/zip/extra_field.rb +5 -0
  32. data/lib/zip/file.rb +5 -0
  33. data/lib/zip/file_split.rb +5 -0
  34. data/lib/zip/filesystem/dir.rb +5 -0
  35. data/lib/zip/filesystem/directory_iterator.rb +5 -0
  36. data/lib/zip/filesystem/file.rb +5 -0
  37. data/lib/zip/filesystem/file_stat.rb +5 -0
  38. data/lib/zip/filesystem/zip_file_name_mapper.rb +5 -0
  39. data/lib/zip/filesystem.rb +5 -0
  40. data/lib/zip/inflater.rb +5 -0
  41. data/lib/zip/input_stream.rb +5 -0
  42. data/lib/zip/ioextras/abstract_input_stream.rb +5 -0
  43. data/lib/zip/ioextras/abstract_output_stream.rb +5 -0
  44. data/lib/zip/ioextras/fake_io.rb +5 -0
  45. data/lib/zip/ioextras.rb +5 -0
  46. data/lib/zip/null_compressor.rb +5 -0
  47. data/lib/zip/null_decompressor.rb +5 -0
  48. data/lib/zip/null_input_stream.rb +5 -0
  49. data/lib/zip/output_stream.rb +5 -0
  50. data/lib/zip/pass_thru_compressor.rb +5 -0
  51. data/lib/zip/pass_thru_decompressor.rb +5 -0
  52. data/lib/zip/streamable_directory.rb +5 -0
  53. data/lib/zip/streamable_stream.rb +5 -0
  54. data/lib/zip/version.rb +5 -0
  55. data/lib/zip.rb +19 -0
  56. data/lib/ziperu/3.1/ziperu.so +0 -0
  57. data/lib/ziperu/3.2/ziperu.so +0 -0
  58. data/lib/ziperu/3.3/ziperu.so +0 -0
  59. data/lib/ziperu/3.4/ziperu.so +0 -0
  60. data/lib/ziperu/4.0/ziperu.so +0 -0
  61. data/lib/ziperu/central_directory.rb +136 -0
  62. data/lib/ziperu/compressor.rb +14 -0
  63. data/lib/ziperu/constants.rb +148 -0
  64. data/lib/ziperu/crypto.rb +72 -0
  65. data/lib/ziperu/decompressor.rb +35 -0
  66. data/lib/ziperu/deflater.rb +41 -0
  67. data/lib/ziperu/dirtyable.rb +48 -0
  68. data/lib/ziperu/dos_time.rb +87 -0
  69. data/lib/ziperu/entry.rb +760 -0
  70. data/lib/ziperu/entry_set.rb +147 -0
  71. data/lib/ziperu/errors.rb +154 -0
  72. data/lib/ziperu/extra_field/aes.rb +52 -0
  73. data/lib/ziperu/extra_field/generic.rb +45 -0
  74. data/lib/ziperu/extra_field/ntfs.rb +98 -0
  75. data/lib/ziperu/extra_field/old_unix.rb +52 -0
  76. data/lib/ziperu/extra_field/universal_time.rb +81 -0
  77. data/lib/ziperu/extra_field/unix.rb +45 -0
  78. data/lib/ziperu/extra_field/unknown.rb +37 -0
  79. data/lib/ziperu/extra_field/zip64.rb +86 -0
  80. data/lib/ziperu/extra_field.rb +109 -0
  81. data/lib/ziperu/file.rb +614 -0
  82. data/lib/ziperu/file_split.rb +93 -0
  83. data/lib/ziperu/filesystem/dir.rb +88 -0
  84. data/lib/ziperu/filesystem/directory_iterator.rb +50 -0
  85. data/lib/ziperu/filesystem/file.rb +265 -0
  86. data/lib/ziperu/filesystem/file_stat.rb +112 -0
  87. data/lib/ziperu/filesystem/zip_file_name_mapper.rb +83 -0
  88. data/lib/ziperu/filesystem.rb +76 -0
  89. data/lib/ziperu/inflater.rb +60 -0
  90. data/lib/ziperu/input_stream.rb +296 -0
  91. data/lib/ziperu/ioextras/abstract_input_stream.rb +137 -0
  92. data/lib/ziperu/ioextras/abstract_output_stream.rb +57 -0
  93. data/lib/ziperu/ioextras/fake_io.rb +41 -0
  94. data/lib/ziperu/ioextras.rb +42 -0
  95. data/lib/ziperu/null_compressor.rb +20 -0
  96. data/lib/ziperu/null_decompressor.rb +25 -0
  97. data/lib/ziperu/null_input_stream.rb +55 -0
  98. data/lib/ziperu/output_stream.rb +223 -0
  99. data/lib/ziperu/pass_thru_compressor.rb +29 -0
  100. data/lib/ziperu/pass_thru_decompressor.rb +37 -0
  101. data/lib/ziperu/streamable_directory.rb +15 -0
  102. data/lib/ziperu/streamable_stream.rb +59 -0
  103. data/lib/ziperu/version.rb +7 -0
  104. data/lib/ziperu.rb +116 -0
  105. metadata +156 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 48f3864d9b4129cfb594ed6f276a148e837b733543b3ddb7e52c06a57e6e8ce0
4
+ data.tar.gz: 0271a62ec1a484c1a8fa2663e876b79c53afddba0f31e5b372c5db96f53735b5
5
+ SHA512:
6
+ metadata.gz: eb39810d9bbdad29bb76930b96957800b1cc68cab33df05254fa9fb5e7661c3390bf98c522a3f05f5dbfffa7700370aba0c8689d7e81a77b08728d19c7c679fa
7
+ data.tar.gz: 317d64c33e98a26c030d288f463b95c6d0235829da090ca12a4cb6c57232ec8d96ebefb17955047da4c8ce54cbf940f919274af42383489b9e91b634533783e8
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1
4
+
5
+ First release, as far as rubygems.org is concerned: 0.1.0 was tagged but
6
+ its precompiled gems were built for Ruby 3.3 and later only, so it was
7
+ never published. The native gems now cover every Ruby the gemspec
8
+ promises (3.1 through 4.0).
9
+
10
+ ## 0.1.0
11
+
12
+ Not published.
13
+
14
+ - rubyzip 3.x API (`Ziperu::File`, `Entry`, `InputStream`, `OutputStream`,
15
+ `FileSystem`) on a Rust core; `require "zip"` aliases `Zip = Ziperu`.
16
+ - Reads Stored, Deflate, Deflate64, bzip2, zstd, LZMA, XZ, PPMd and the
17
+ PKZIP 1.x methods; writes Stored, Deflate, bzip2 and zstd; AES and
18
+ ZipCrypto encryption both ways.
19
+ - Precompiled gems for Linux, macOS and Windows; no toolchain needed.
20
+ - Beyond rubyzip: archive limits (`Ziperu.max_entries`,
21
+ `Ziperu.max_total_size`), `File#verify`, symlink entries with opt-in
22
+ extraction, progress callbacks, native `extract_all`, streaming reads
23
+ and writes through any IO, UTF-8 names when the archive flags them.
24
+ - See the README for the deliberate differences from rubyzip.
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2002-2026, The Rubyzip Developers
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maor Caplan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # ziperu
2
+
3
+ A drop-in replacement for [rubyzip](https://github.com/rubyzip/rubyzip) with a
4
+ Rust core. Same `Ziperu::File` / `Ziperu::Entry` / `Ziperu::InputStream` /
5
+ `Ziperu::OutputStream` API, backed by the [`zip`](https://github.com/zip-rs/zip2)
6
+ crate through [Magnus](https://github.com/matsadler/magnus).
7
+
8
+ ```ruby
9
+ gem "ziperu" # in the Gemfile, in place of gem "rubyzip"
10
+ ```
11
+
12
+ Precompiled gems are published for Linux, macOS and Windows, so no Rust
13
+ toolchain is needed to install. Changes are listed in
14
+ [CHANGELOG.md](CHANGELOG.md).
15
+
16
+ ```ruby
17
+ require "ziperu" # or require "zip" to get the Zip alias
18
+
19
+ Ziperu::File.open("archive.zip") do |zf|
20
+ zf.each { |entry| puts "#{entry.name} (#{entry.size} bytes)" }
21
+ puts zf.read("README.txt")
22
+ end
23
+ ```
24
+
25
+ **Status: early.** `Ziperu::File` (read, `extract`/`extract_all`, `add`,
26
+ `add_recursive`, `get_output_stream`, `remove`, `rename`, `replace`,
27
+ `mkdir`, `commit`, `write_buffer`/`open_buffer`), `Ziperu::InputStream` and
28
+ `Ziperu::OutputStream` (with AES and ZipCrypto) and `Ziperu::FileSystem`
29
+ work; see [COMPAT.md](COMPAT.md) for what rubyzip's own test suite says.
30
+ Tracks the rubyzip 3.x API (`Ziperu::RUBYZIP_COMPAT_VERSION`).
31
+
32
+ ### Migrating from rubyzip
33
+
34
+ The library lives in the `Ziperu` namespace. `require "zip"` loads it and
35
+ sets `Zip = Ziperu`, so existing code keeps working with a one-line change
36
+ to the Gemfile:
37
+
38
+ ```ruby
39
+ gem "ziperu" # instead of gem "rubyzip"
40
+ ```
41
+
42
+ If rubyzip has to stay installed during the migration (another gem depends
43
+ on it, say), the two cannot share the `Zip` name: `require "zip"` raises a
44
+ `LoadError` naming the file that already defined `Zip`. Use `require "ziperu"`
45
+ and `Ziperu::` for the code you have switched, and leave `Zip::` to rubyzip
46
+ until it goes. Exception classes follow the namespace, so rescue
47
+ `Ziperu::Error` (or `Zip::Error`, which is the same class once aliased).
48
+
49
+ ### Deliberate differences from rubyzip
50
+
51
+ - `Ziperu::InputStream` locates entries through the central directory instead
52
+ of walking local headers, so archives written in streaming mode (general
53
+ purpose bit 3, e.g. by macOS Archive Utility) read normally instead of
54
+ raising `Ziperu::StreamingError`.
55
+ - Every read path stops at the entry's declared size, so a header that lies
56
+ can't inflate into a zip bomb; a `Ziperu::EntrySizeError` is raised instead.
57
+ (rubyzip only checks this in `extract`, not in `InputStream`.)
58
+ `Ziperu.validate_entry_sizes = false` restores rubyzip's behaviour of
59
+ extracting the full data with a warning.
60
+ - ZipCrypto ("traditional") encryption is written by ziperu itself (the
61
+ crate keeps that broken cipher read-only), with the same header layout
62
+ rubyzip produces: general purpose bits 0 and 3 set, the check byte from
63
+ the DOS time. Only Stored and Deflate entries can be encrypted this way,
64
+ and a committed `Ziperu::File` never encrypts. `AESEncrypter` is the
65
+ better choice wherever the consumer can open AES.
66
+ - Archives compressed with bzip2, Deflate64, zstd, LZMA, XZ, PPMd or the
67
+ PKZIP 1.x methods (Shrink, Reduce, Implode) read normally; rubyzip raises
68
+ `Ziperu::CompressionMethodError` for anything but Stored and Deflate.
69
+ Entries can also be *written* with bzip2 or zstd
70
+ (`put_next_entry(name, "", ExtraField.new, Ziperu::Entry::ZSTD, 3)`), which
71
+ rubyzip, Windows Explorer and Java cannot read, so those are opt-in.
72
+ - AES entries of 20 bytes or more are written as AE-1 (with a real CRC in the
73
+ header) rather than rubyzip's always-AE-2 with a zero CRC; both are read
74
+ by every AES-aware tool, and AE-1 adds an integrity check.
75
+ - Entry names and comments come back as UTF-8 strings when the entry's
76
+ language-encoding flag is set (which is how most tools, and ziperu
77
+ itself, write non-ASCII names); rubyzip returns the raw bytes as binary
78
+ strings. Entries without the flag, or whose flagged name is not valid
79
+ UTF-8, stay binary as in rubyzip, and `Ziperu.force_entry_names_encoding`
80
+ overrides both. A lookup (`find_entry`, `get_entry`, `read`) accepts the
81
+ name in any encoding with the same bytes, so neither `.b` nor
82
+ `force_encoding` is needed to find an entry.
83
+ - Writing two entries with the same name to one `OutputStream` raises
84
+ `Ziperu::Error`; rubyzip writes both and the last one wins.
85
+ - A CRC mismatch while reading raises `Ziperu::DecompressionError`;
86
+ rubyzip hands back the bytes.
87
+ - Every written file entry carries Unix permissions in its external
88
+ attributes (0644 by default), and such permissions are read back whatever
89
+ the entry's host system byte says; rubyzip leaves `unix_perms` nil for
90
+ entries written on Windows. Setuid, setgid and sticky bits are not
91
+ written, and a symlink added from disk is written as a regular file
92
+ holding the link target.
93
+ - Editing an existing entry's comment, time or extra fields and committing
94
+ rewrites that entry's data under a fresh header; a rename or a chmod on
95
+ its own keeps the byte-for-byte copy. So does an untouched entry, unless
96
+ it carries an extended-timestamp, NTFS or Info-ZIP unix extra field, which
97
+ the native copy would not carry over intact. An encrypted entry is always
98
+ copied as is: its name and permissions can change, and any other edit
99
+ raises `Ziperu::Error` at commit rather than vanishing, which is what
100
+ rubyzip does for compression methods it cannot write.
101
+ - Several records with the same name in one archive (Python's `zipfile`
102
+ writes such archives, with a warning): `Ziperu::File` lists every record
103
+ and each reads its own data, where rubyzip keeps only the last. A lookup
104
+ by name (`find_entry`, `read`, `extract_all`) finds the last, as in
105
+ rubyzip, and so does a commit, which writes one record per name; a
106
+ shadowed record given a new name before the commit is written afresh.
107
+ `Ziperu::InputStream` yields every record, as rubyzip's does.
108
+ - `Ziperu::InputStream` over something that is not a zip archive at all
109
+ returns `nil` from the first `get_next_entry`, as rubyzip does; a corrupt
110
+ archive raises `Ziperu::Error` from `Ziperu::File.open` in both.
111
+ - `Zip::VERSION` is ziperu's own version, not rubyzip's. Code that gates on
112
+ it (`Gem::Version.new(Zip::VERSION) >= "3"`) should check
113
+ `Ziperu::RUBYZIP_COMPAT_VERSION` instead.
114
+ - An open `Ziperu::File` holds its file open until `close`. On Windows a
115
+ commit by *another* `Ziperu::File` on the same path therefore moves the
116
+ old archive aside before putting the new one in place, and removes it
117
+ once nothing holds it; the first instance keeps reading the old bytes,
118
+ as it would on Unix. rubyzip closes the file between operations.
119
+ - Using one `InputStream`, `OutputStream` or `Zip::File` from two threads
120
+ at the same time raises `ThreadError` ("in use by another thread")
121
+ rather than corrupting state; rubyzip's objects are not thread-safe
122
+ either, they just fail less loudly.
123
+ - Class names are `Ziperu::...` even when loaded through the `Zip` alias:
124
+ `Zip::Entry.name` is `"Ziperu::Entry"`, and so are `inspect` output and
125
+ exception messages. Comparisons against the string `"Zip::Entry"` need
126
+ updating; comparisons against the constant do not.
127
+ - Extraction never creates symlinks unless asked to (see below; rubyzip 3.x
128
+ never does) and refuses paths that resolve outside the destination
129
+ directory, checking the resolved absolute path with a trailing separator,
130
+ the fix for CVE-2026-85396.
131
+
132
+ ### Beyond rubyzip
133
+
134
+ Everything here is additive: code that does not use it behaves as above.
135
+
136
+ - **Archive limits.** `Ziperu.max_entries` and `Ziperu.max_total_size`
137
+ (declared uncompressed bytes) make `Ziperu::File.open` and
138
+ `Ziperu::InputStream.open` raise `Ziperu::LimitError` before any entry is
139
+ read. With `validate_entry_sizes` on (the default) no entry inflates past
140
+ its declared size, so the total is a real bound on what an extraction can
141
+ write.
142
+ - **Integrity check.** `Ziperu::File#verify` inflates every entry to nowhere
143
+ so the CRC of each is checked, releasing the GVL meanwhile, and returns a
144
+ Hash of failing entries to the exception each raised (empty when sound).
145
+ Encrypted entries are checked when a `decrypter:` is given.
146
+ - **Symlink entries.** `add` of a symlink stores a link entry (target as
147
+ content, `S_IFLNK` in the attributes) instead of a copy of the target.
148
+ `extract_all(dir, symlinks: :create)` and `extract(..., symlinks: :create)`
149
+ create such links, after the links' targets have been checked to resolve
150
+ inside the destination; links are created after everything else so no
151
+ file is written through one. The default stays `:skip`, with rubyzip's
152
+ warning. `add_recursive` still skips links on disk.
153
+ - **Progress.** `extract_all(dir, progress: ->(entry, done, total) {})` and
154
+ `add_recursive(dir, progress: ->(entry, done, nil) {})` report after each
155
+ entry; `extract_all` keeps its native fast path and reports per batch.
156
+ - **More compression methods.** bzip2 and zstd on write; LZMA, XZ, PPMd,
157
+ Deflate64 and the PKZIP 1.x methods on read (see above).
158
+ - **Streaming reads and writes through any IO.** `open_buffer(io)` reads an
159
+ IO lazily and `write_buffer(io)` writes entry by entry, so neither holds
160
+ the archive in memory.
161
+
162
+ ## Why
163
+
164
+ rubyzip is pure Ruby and does per-entry work in Ruby objects. ziperu parses
165
+ the central directory once in Rust and hands Ruby a flat metadata dump, reads
166
+ entries in large chunks, copies unchanged entries byte-for-byte on `commit`,
167
+ and releases the GVL while inflating from a file or String buffer and while
168
+ deflating, so other Ruby threads keep running. A StringIO is read from its
169
+ string directly; an archive opened from any other IO object
170
+ (`open_buffer(io)`, `InputStream.open(io)`) is read through it lazily,
171
+ under the GVL; a pipe is read to its end first. The intent is
172
+ precompiled gems for every platform rubyzip runs on, zero runtime dependencies,
173
+ and stricter defaults on path traversal and declared-size checks.
174
+
175
+ ## Development
176
+
177
+ Needs Ruby >= 3.1 and Rust >= 1.88.
178
+
179
+ ```
180
+ bundle install
181
+ bundle exec rake compile test
182
+ ```
183
+
184
+ ### Windows
185
+
186
+ RubyInstaller is MinGW-based, so the extension builds for the GNU Rust
187
+ target and bindgen needs clang's resource headers. Once:
188
+
189
+ ```
190
+ winget install RubyInstallerTeam.RubyWithDevKit.3.4
191
+ rustup target add x86_64-pc-windows-gnu
192
+ ```
193
+
194
+ then in each shell:
195
+
196
+ ```
197
+ . .\bin\windows-env.ps1
198
+ bundle exec rake compile test
199
+ ```
200
+
201
+ ### rubyzip compatibility
202
+
203
+ `rake compat` downloads rubyzip's source into `tmp/`, generates its fixtures
204
+ with the real rubyzip, then runs rubyzip's own test suite with
205
+ `require "zip"` resolving to this gem, so it also proves the `Zip` alias is
206
+ complete. The per-file scorecard is written to
207
+ [COMPAT.md](COMPAT.md); per-file logs land in `tmp/compat-logs/`. Use
208
+ `ONLY=input_stream rake compat` to run a subset.
209
+
210
+ ### Fuzzing
211
+
212
+ `rake fuzz` runs [fuzz/differential.rb](fuzz/differential.rb): random
213
+ archives go through ziperu and Python's `zipfile` (plus rubyzip, once
214
+ `rake compat:fetch` has run) in both directions, random edits are made
215
+ through the `Zip::File` API and checked by the oracles, and corrupted
216
+ archives must fail with ziperu's own error classes while agreeing with the
217
+ oracle on anything both still accept. CI runs three minutes of it per push
218
+ with a fresh seed; a failure is saved under `tmp/fuzz/failures/` with the
219
+ seed that reproduces it (`SEED=n ITERATIONS=m rake fuzz`).
220
+
221
+ There is deliberately no cargo-fuzz harness: the Rust layer is thin
222
+ wrappers over the `zip` crate, which has its own fuzzing, and the code
223
+ that is ours (entry ownership, the chunked reader, the Ruby IO source) is
224
+ only reachable through the Ruby API, which is what the differential fuzzer
225
+ drives.
226
+
227
+ ### Benchmarks
228
+
229
+ `rake bench` runs [bench/bench.rb](bench/bench.rb) with rubyzip and ziperu
230
+ loaded side by side. On one Windows machine (Ruby 3.4, median of 5):
231
+
232
+ | scenario | rubyzip 3.7.0 | ziperu 0.1.0 | speedup |
233
+ |---|---:|---:|---:|
234
+ | open and list 10000 entries | 298 ms | 130 ms | 2.3x |
235
+ | read 1000 x 8 KB deflated entries | 196 ms | 23 ms | 8.4x |
236
+ | InputStream over 1000 entries | 111 ms | 64 ms | 1.7x |
237
+ | inflate one 64 MB entry | 106 ms | 54 ms | 2.0x |
238
+ | extract_all 1000 entries | 984 ms | 459 ms | 2.1x |
239
+ | write 1000 x 8 KB entries (OutputStream) | 89 ms | 39 ms | 2.3x |
240
+ | deflate one 64 MB entry (OutputStream) | 245 ms | 58 ms | 4.3x |
241
+ | open_buffer(String) and read 1000 entries | 69 ms | 15 ms | 4.5x |
242
+ | Zip::File add 100 files and commit | 40 ms | 33 ms | 1.2x |
243
+
244
+ Inflating and deflating are where the Rust core pays off. Listing a large
245
+ central directory is dominated by building Ruby `Entry` objects, which both
246
+ libraries do in Ruby. `extract_all` writes every entry in one native call
247
+ (one open per file, with its mtime set on that handle) and is left bounded
248
+ by file creation itself, which is slow on Windows.
249
+
250
+ ### Releasing
251
+
252
+ A `v*` tag runs [release.yml](.github/workflows/release.yml): the source
253
+ gem plus one precompiled gem per platform (built with
254
+ [oxidize-rb/actions/cross-gem](https://github.com/oxidize-rb/actions), each
255
+ holding the extension for every stable Ruby), a smoke test that installs
256
+ the right gem on Linux, macOS and Windows without a Rust toolchain, and a
257
+ draft GitHub release with the gems attached. Pushing to rubygems.org is a
258
+ manual `gem push` from that draft.
259
+
260
+ ## Layout
261
+
262
+ - `ext/ziperu/` — the Rust extension (`Ziperu::Native::*`): a few primitive,
263
+ positional-argument methods.
264
+ - `lib/ziperu/` — the rubyzip-shaped API in Ruby: kwargs, blocks, `Enumerable`,
265
+ the error hierarchy, extra-field parsing.
266
+ - `test/` — minitest; `test/data/` holds fixture archives.
267
+ - `fuzz/` — the differential fuzzer and its Python and rubyzip oracles.
268
+ - `bench/` — the rubyzip comparison benchmark.
269
+
270
+ ## License
271
+
272
+ MIT, see [LICENSE.txt](LICENSE.txt). The Ruby layer ports a good part of
273
+ rubyzip (the extra-field, filesystem, DOS time, entry-set and split code,
274
+ and the test fixtures under `test/data/rubyzip/`), which is BSD-2-Clause;
275
+ its notice is in [LICENSE-rubyzip.txt](LICENSE-rubyzip.txt) and the ported
276
+ files say so at the top.
data/lib/rubyzip.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip 3.x ships this file too (Bundler autorequires it for `gem "rubyzip"`).
4
+ require_relative "zip"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/central_directory"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/compressor"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/constants"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
data/lib/zip/crypto.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/decompressor"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/deflater"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/dirtyable"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/dos_time"
data/lib/zip/entry.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/entry"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/entry_set"
data/lib/zip/errors.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/errors"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/aes"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/generic"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/ntfs"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/old_unix"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/universal_time"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/unix"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/unknown"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/zip64"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field"
data/lib/zip/file.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/file"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/file_split"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/dir"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/directory_iterator"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/file"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/file_stat"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/zip_file_name_mapper"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/inflater"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/input_stream"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/ioextras/abstract_input_stream"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/ioextras/abstract_output_stream"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/ioextras/fake_io"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/ioextras"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/null_compressor"