libzip-ruby 0.1.0-x86_64-darwin
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 +7 -0
- data/LICENSE +191 -0
- data/README.md +521 -0
- data/doc/api.rb +577 -0
- data/lib/libzip-ruby.rb +4 -0
- data/lib/libzip.rb +18 -0
- data/lib/libzip_ruby/3.3/libzip_ruby.bundle +0 -0
- data/lib/libzip_ruby/3.4/libzip_ruby.bundle +0 -0
- data/lib/libzip_ruby/4.0/libzip_ruby.bundle +0 -0
- data/libzip-ruby.gemspec +41 -0
- metadata +61 -0
data/doc/api.rb
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
# doc/api.rb - the API reference, and only that.
|
|
2
|
+
#
|
|
3
|
+
# The extension is written in Zig (src/*.zig). RDoc can read a C extension but
|
|
4
|
+
# not a Zig one, so the Ruby surface is written out here: each body is empty,
|
|
5
|
+
# and the working code is whatever rb_define_method points at.
|
|
6
|
+
#
|
|
7
|
+
# This file is outside lib/, off the $LOAD_PATH, so it can't be required by
|
|
8
|
+
# mistake; loading it would shadow the extension with empty methods. It goes
|
|
9
|
+
# into the gem (spec.extra_rdoc_files) so that `ri LibZip::File` works from an
|
|
10
|
+
# installed gem, and `script/doc --check` compares it with the loaded
|
|
11
|
+
# extension, so the 2 can't fall out of step.
|
|
12
|
+
|
|
13
|
+
# Ruby bindings for libzip[https://libzip.org], with libzip, zlib and mbedTLS
|
|
14
|
+
# linked into the extension.
|
|
15
|
+
#
|
|
16
|
+
# require "libzip"
|
|
17
|
+
#
|
|
18
|
+
# LibZip::File.open("archive.zip", create: true) do |zip|
|
|
19
|
+
# zip.add("hello.txt", "hello.txt")
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# Start at LibZip::File. A listing gives LibZip::Entry snapshots,
|
|
23
|
+
# LibZip::InputStream and LibZip::OutputStream move entry bytes in and out, and
|
|
24
|
+
# anything that fails is a LibZip::Error.
|
|
25
|
+
module LibZip
|
|
26
|
+
# The gem version, written into the extension at build time from
|
|
27
|
+
# libzip-ruby.gemspec.
|
|
28
|
+
VERSION = "0.1.0"
|
|
29
|
+
|
|
30
|
+
# A zip archive, open for reading, writing, or both.
|
|
31
|
+
#
|
|
32
|
+
# An archive is a transaction: entries added, removed and renamed are noted
|
|
33
|
+
# as you go, and #close writes them to disk. The block form of ::open commits
|
|
34
|
+
# on normal exit and throws the archive away if the block fails, so a failed
|
|
35
|
+
# write can't leave a truncated zip behind.
|
|
36
|
+
#
|
|
37
|
+
# LibZip::File.open("archive.zip", create: true) do |zip|
|
|
38
|
+
# zip.add("a.txt", "a.txt")
|
|
39
|
+
# zip.get_output_stream("b.txt") { |out| out.write("from memory") }
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# LibZip::File.open("archive.zip") do |zip|
|
|
43
|
+
# zip.names # => ["a.txt", "b.txt"]
|
|
44
|
+
# zip.read("b.txt") # => "from memory"
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# File includes Enumerable over its LibZip::Entry records, so #map, #select
|
|
48
|
+
# and the rest work on a listing.
|
|
49
|
+
#
|
|
50
|
+
# Once the archive is closed, all of these fail with LibZip::EntryError.
|
|
51
|
+
class File
|
|
52
|
+
include Enumerable
|
|
53
|
+
|
|
54
|
+
# Opens the archive at +path+.
|
|
55
|
+
#
|
|
56
|
+
# With +create+ the archive is created when +path+ doesn't exist; an
|
|
57
|
+
# existing file is opened as it is and isn't truncated, so +create+ is safe
|
|
58
|
+
# to pass whenever you intend to write. Without it, a missing +path+ fails
|
|
59
|
+
# with LibZip::NotFoundError.
|
|
60
|
+
#
|
|
61
|
+
# +password+ becomes the archive default: the password each read of an
|
|
62
|
+
# encrypted entry falls back to. See #password=, and the call-level
|
|
63
|
+
# +password+ of #read and #get_input_stream.
|
|
64
|
+
#
|
|
65
|
+
# Without a block you get the archive and you call #close yourself. With a
|
|
66
|
+
# block the archive is yielded; on normal exit it's closed and committed
|
|
67
|
+
# and the block's value comes back from ::open. If the block fails, the
|
|
68
|
+
# archive is thrown away: no bytes are written, and an archive that ::open
|
|
69
|
+
# had just created is removed from disk.
|
|
70
|
+
#
|
|
71
|
+
# LibZip::File.open("archive.zip") # read
|
|
72
|
+
# LibZip::File.open("archive.zip", create: true) # read and write
|
|
73
|
+
# LibZip::File.open("secrets.zip", password: "hunter2") { |zip| ... }
|
|
74
|
+
#
|
|
75
|
+
# A 2nd argument that isn't a Hash is a TypeError.
|
|
76
|
+
def self.open(path, create: false, password: nil) # :yields: zip
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Writes the central directory, flushes the archive to disk, and releases
|
|
80
|
+
# it. Live input streams on this archive are closed first.
|
|
81
|
+
#
|
|
82
|
+
# Returns +nil+. Closing an already closed archive fails with
|
|
83
|
+
# LibZip::EntryError, so a double #close is a visible bug.
|
|
84
|
+
#
|
|
85
|
+
# An archive that was created and then not written to produces no file:
|
|
86
|
+
# libzip won't write an empty archive, and removing the last remaining
|
|
87
|
+
# entry deletes the file instead.
|
|
88
|
+
def close
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Whether the archive has been closed.
|
|
92
|
+
def closed?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Adds +source_path+ from disk as the entry +name+, and returns +self+, so
|
|
96
|
+
# calls chain.
|
|
97
|
+
#
|
|
98
|
+
# zip.add("a.txt", "a.txt").add("docs/b.txt", "b.txt")
|
|
99
|
+
#
|
|
100
|
+
# The file is read when the archive is written, not now, so a source that
|
|
101
|
+
# disappears, or that can't be read any more, shows up as an error from
|
|
102
|
+
# #close. An entry of the same name is replaced.
|
|
103
|
+
#
|
|
104
|
+
# +encryption+ is one of +:aes128+, +:aes192+, +:aes256+, +:none+ or +nil+,
|
|
105
|
+
# and needs a +password+, either here or as the archive default. +:pkware+
|
|
106
|
+
# (ZipCrypto) isn't allowed for writing.
|
|
107
|
+
#
|
|
108
|
+
# Fails with LibZip::NotFoundError when +source_path+ doesn't exist,
|
|
109
|
+
# LibZip::PermissionError when it can't be read, and
|
|
110
|
+
# LibZip::InvalidArgumentError when it isn't a regular file, when
|
|
111
|
+
# +password+ comes without +encryption+, or when +encryption+ isn't one of
|
|
112
|
+
# the symbols above.
|
|
113
|
+
def add(name, source_path, encryption: nil, password: nil)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Loads the entry +name+ and returns its bytes as an ASCII-8BIT String.
|
|
117
|
+
#
|
|
118
|
+
# +password+ decrypts this one entry and overrides the archive default, so
|
|
119
|
+
# an incorrect password here fails even when the archive was opened with
|
|
120
|
+
# the correct one.
|
|
121
|
+
#
|
|
122
|
+
# Fails with LibZip::NotFoundError when there's no such entry,
|
|
123
|
+
# LibZip::PasswordError when the password is missing or incorrect,
|
|
124
|
+
# LibZip::DecompressionError when the data doesn't match its CRC or its AES
|
|
125
|
+
# HMAC, and LibZip::CorruptArchiveError when the entry is shorter or longer
|
|
126
|
+
# than the size in the central directory.
|
|
127
|
+
#
|
|
128
|
+
# Use #get_input_stream for an entry too large to keep in memory.
|
|
129
|
+
def read(name, password: nil)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns a LibZip::InputStream over the entry +name+, which pulls the entry
|
|
133
|
+
# in chunks instead of building 1 String.
|
|
134
|
+
#
|
|
135
|
+
# zip.get_input_stream("big.csv") do |stream|
|
|
136
|
+
# stream.read(64 * 1024) until stream.eof?
|
|
137
|
+
# end
|
|
138
|
+
#
|
|
139
|
+
# With a block the stream is yielded and closed on the way out, including
|
|
140
|
+
# when the block fails, and the block's value comes back. Without one you
|
|
141
|
+
# get the stream; close it with LibZip::InputStream#close, or let #close on
|
|
142
|
+
# the archive do it.
|
|
143
|
+
#
|
|
144
|
+
# +password+ works as in #read. Fails with LibZip::NotFoundError when
|
|
145
|
+
# there's no such entry.
|
|
146
|
+
def get_input_stream(name, password: nil) # :yields: stream
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Returns a LibZip::OutputStream that writes the entry +name+ from memory.
|
|
150
|
+
#
|
|
151
|
+
# zip.get_output_stream("data.csv") do |out|
|
|
152
|
+
# out.write("a,b,c\n")
|
|
153
|
+
# out << "1,2,3\n"
|
|
154
|
+
# end
|
|
155
|
+
#
|
|
156
|
+
# The stream buffers what you give it and adds the entry only when it's
|
|
157
|
+
# closed. A stream left to the garbage collector, or one where the block
|
|
158
|
+
# failed, contributes no entry, while entries written before it remain.
|
|
159
|
+
# An entry of the same name is replaced.
|
|
160
|
+
#
|
|
161
|
+
# With a block the stream is yielded, committed on normal exit, and the
|
|
162
|
+
# block's value comes back. Without one you get the stream, and
|
|
163
|
+
# LibZip::OutputStream#close commits it.
|
|
164
|
+
#
|
|
165
|
+
# +encryption+ and +password+ work as in #add.
|
|
166
|
+
def get_output_stream(name, encryption: nil, password: nil) # :yields: stream
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Deletes an entry, named by String or by the LibZip::Entry, and returns it
|
|
170
|
+
# as a snapshot taken before the deletion, so its metadata remains readable
|
|
171
|
+
# after the deletion and after the archive closes.
|
|
172
|
+
#
|
|
173
|
+
# removed = zip.remove("old.txt")
|
|
174
|
+
# removed.size # => 1234
|
|
175
|
+
#
|
|
176
|
+
# The entry disappears from the listing at once; #close writes the file
|
|
177
|
+
# again. Removing the last remaining entry deletes the archive from disk
|
|
178
|
+
# instead of writing an empty one.
|
|
179
|
+
#
|
|
180
|
+
# Fails with LibZip::NotFoundError when the entry isn't in the archive,
|
|
181
|
+
# including on a 2nd #remove of the same name, and
|
|
182
|
+
# LibZip::InvalidArgumentError for a LibZip::Entry belonging to another
|
|
183
|
+
# archive. There's no 3rd outcome: you get the entry back, or you get an
|
|
184
|
+
# exception.
|
|
185
|
+
def remove(name)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Renames an entry, named by String or by the LibZip::Entry, and returns a
|
|
189
|
+
# snapshot taken after the rename. Size, CRC, time and compression are
|
|
190
|
+
# preserved.
|
|
191
|
+
#
|
|
192
|
+
# zip.rename("draft.txt", "final.txt")
|
|
193
|
+
#
|
|
194
|
+
# A rename moves 1 record: the children of a directory entry keep the names
|
|
195
|
+
# they were stored with, whatever the directory is called now.
|
|
196
|
+
#
|
|
197
|
+
# Fails with LibZip::NotFoundError when +old_name+ isn't in the archive,
|
|
198
|
+
# LibZip::AlreadyExistsError when +new_name+ already is, and
|
|
199
|
+
# LibZip::InvalidArgumentError when 1 name has the trailing +/+ of a
|
|
200
|
+
# directory entry and the other doesn't, or for a LibZip::Entry belonging
|
|
201
|
+
# to another archive.
|
|
202
|
+
def rename(old_name, new_name)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Returns the central directory as an Array of LibZip::Entry snapshots.
|
|
206
|
+
#
|
|
207
|
+
# No extraction happens, and no entry bytes are read. The array is built
|
|
208
|
+
# fresh on each call, so it reflects entries added or removed since the
|
|
209
|
+
# previous one.
|
|
210
|
+
def entries
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Passes each LibZip::Entry to the block, and returns the full Array.
|
|
214
|
+
# Without a block, returns an Enumerator.
|
|
215
|
+
#
|
|
216
|
+
# This is what File's Enumerable methods run on.
|
|
217
|
+
#
|
|
218
|
+
# zip.map(&:name)
|
|
219
|
+
# zip.select(&:directory?)
|
|
220
|
+
def each # :yields: entry
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Same as #each. Named for what it passes to the block, and the method an
|
|
224
|
+
# Enumerator from #each calls.
|
|
225
|
+
def each_entry # :yields: entry
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Returns the entry names as an Array of UTF-8 Strings, directory entries
|
|
229
|
+
# included, with the trailing +/+ they're stored with.
|
|
230
|
+
def names
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Number of entries in the archive, not counting the ones removed since it
|
|
234
|
+
# was opened.
|
|
235
|
+
def size
|
|
236
|
+
end
|
|
237
|
+
alias length size
|
|
238
|
+
|
|
239
|
+
# Whether +name+ is an entry of this archive. Accepts a String or a
|
|
240
|
+
# LibZip::Entry.
|
|
241
|
+
def include?(name)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Returns the LibZip::Entry named +name+, or +nil+ when the archive has no
|
|
245
|
+
# such entry. See #get_entry to fail instead.
|
|
246
|
+
def find_entry(name)
|
|
247
|
+
end
|
|
248
|
+
alias [] find_entry
|
|
249
|
+
|
|
250
|
+
# Returns the LibZip::Entry named +name+, or fails with
|
|
251
|
+
# LibZip::NotFoundError. See #find_entry to get +nil+ instead.
|
|
252
|
+
def get_entry(name)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns the LibZip::Entry records matching +pattern+, and passes each of
|
|
256
|
+
# them to the block if one is given.
|
|
257
|
+
#
|
|
258
|
+
# zip.glob("*.txt") # => the .txt entries at the top level
|
|
259
|
+
# zip.glob("**/*.txt") # => the .txt entries at any depth
|
|
260
|
+
# zip.glob("docs") # => the "docs/" entry
|
|
261
|
+
#
|
|
262
|
+
# Matching is ::File.fnmatch? with +FNM_PATHNAME+, +FNM_DOTMATCH+ and
|
|
263
|
+
# +FNM_EXTGLOB+: <tt>*</tt> doesn't cross a +/+, <tt>**</tt> matches 0 or
|
|
264
|
+
# more directories, <tt>?</tt>, <tt>[...]</tt> and <tt>{a,b}</tt> all work,
|
|
265
|
+
# and a leading dot isn't special. A directory entry matches without the
|
|
266
|
+
# trailing +/+ it's stored with.
|
|
267
|
+
#
|
|
268
|
+
# Unlike Dir.glob on macOS, matching is case-sensitive on all platforms.
|
|
269
|
+
def glob(pattern) # :yields: entry
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# The archive comment, as a UTF-8 String, or +nil+ when the archive has
|
|
273
|
+
# none. A comment that isn't valid UTF-8 comes back as ASCII-8BIT, instead
|
|
274
|
+
# of as a broken String.
|
|
275
|
+
def comment
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Sets the archive comment, which #close writes. +nil+ clears it.
|
|
279
|
+
#
|
|
280
|
+
# A comment longer than 65535 bytes, the limit of the field in the zip
|
|
281
|
+
# format, fails with LibZip::InvalidArgumentError.
|
|
282
|
+
def comment=(comment)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Sets the comment of the entry +name+, and returns +self+. +nil+ clears
|
|
286
|
+
# it. Read it back with LibZip::Entry#comment.
|
|
287
|
+
#
|
|
288
|
+
# The comment is stored as UTF-8 when its bytes are valid UTF-8, and as raw
|
|
289
|
+
# bytes otherwise. Fails with LibZip::NotFoundError when there's no such
|
|
290
|
+
# entry, and LibZip::InvalidArgumentError past the 65535-byte limit.
|
|
291
|
+
def set_comment(name, comment)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Sets the archive default password: the one used for an encrypted entry
|
|
295
|
+
# read with no +password+ argument. +nil+ clears it.
|
|
296
|
+
#
|
|
297
|
+
# There's no reader; a password that goes in doesn't come back out.
|
|
298
|
+
#
|
|
299
|
+
# Zip encryption covers entry contents only. Names, comments and the
|
|
300
|
+
# central directory remain in the clear whatever the password.
|
|
301
|
+
def password=(password)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# One record of the central directory: what the archive stores about an entry,
|
|
306
|
+
# without reading the entry.
|
|
307
|
+
#
|
|
308
|
+
# An Entry is an immutable snapshot taken when it was listed, so its metadata
|
|
309
|
+
# remains readable after the entry is removed and after the archive is
|
|
310
|
+
# closed. It isn't a handle: pass the name to LibZip::File#read for the
|
|
311
|
+
# bytes.
|
|
312
|
+
#
|
|
313
|
+
# entry = zip["docs/a.txt"]
|
|
314
|
+
# entry.name # => "docs/a.txt"
|
|
315
|
+
# entry.size # => 1234
|
|
316
|
+
# entry.compression_method # => LibZip::Entry::DEFLATED
|
|
317
|
+
#
|
|
318
|
+
# Each accessor except #name, #index and #directory? returns +nil+ when the
|
|
319
|
+
# archive didn't store that field, which a zip written by another tool is
|
|
320
|
+
# free to skip.
|
|
321
|
+
#
|
|
322
|
+
# Entries come from LibZip::File#entries, #each, #[], #find_entry,
|
|
323
|
+
# #get_entry, #glob, #remove and #rename; there's no public constructor.
|
|
324
|
+
class Entry
|
|
325
|
+
# Compression method: stored, no compression.
|
|
326
|
+
STORED = 0
|
|
327
|
+
|
|
328
|
+
# Compression method: deflate.
|
|
329
|
+
DEFLATED = 8
|
|
330
|
+
|
|
331
|
+
# Encryption method: none. What #encryption_method gives for a plain entry.
|
|
332
|
+
NONE = 0
|
|
333
|
+
|
|
334
|
+
# Encryption method: traditional PKWARE (ZipCrypto). Readable, and not
|
|
335
|
+
# allowed for writing, since libzip documents it as broken.
|
|
336
|
+
TRAD_PKWARE = 1
|
|
337
|
+
|
|
338
|
+
# Encryption method: AES-128 (Winzip AE-2).
|
|
339
|
+
AES_128 = 257
|
|
340
|
+
|
|
341
|
+
# Encryption method: AES-192 (Winzip AE-2).
|
|
342
|
+
AES_192 = 258
|
|
343
|
+
|
|
344
|
+
# Encryption method: AES-256 (Winzip AE-2).
|
|
345
|
+
AES_256 = 259
|
|
346
|
+
|
|
347
|
+
# The entry name, as a UTF-8 String. Directory entries keep the trailing
|
|
348
|
+
# +/+ they're stored with.
|
|
349
|
+
def name
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# The entry's position in the archive, counting from 0.
|
|
353
|
+
def index
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# The uncompressed size in bytes, or +nil+ when the archive didn't store
|
|
357
|
+
# it.
|
|
358
|
+
def size
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# The size in bytes as stored, after compression, or +nil+ when the archive
|
|
362
|
+
# didn't store it.
|
|
363
|
+
def compressed_size
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# The CRC-32 of the uncompressed data as an Integer, or +nil+ when the
|
|
367
|
+
# archive didn't store it.
|
|
368
|
+
#
|
|
369
|
+
# AES entries use AE-2, which keeps no CRC, so this can be 0 for them;
|
|
370
|
+
# their integrity check is the HMAC, verified when the entry is read to the
|
|
371
|
+
# end.
|
|
372
|
+
def crc
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# The modification time, as a Time, or +nil+ when the archive didn't store
|
|
376
|
+
# it.
|
|
377
|
+
def time
|
|
378
|
+
end
|
|
379
|
+
alias mtime time
|
|
380
|
+
|
|
381
|
+
# STORED or DEFLATED, or +nil+ when the archive didn't store it. Directory
|
|
382
|
+
# entries give STORED.
|
|
383
|
+
def compression_method
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# libzip's numeric encryption method: NONE, TRAD_PKWARE, AES_128, AES_192
|
|
387
|
+
# or AES_256. +nil+ when the archive didn't store it.
|
|
388
|
+
def encryption_method
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Whether the entry's contents are encrypted, which means #encryption_method
|
|
392
|
+
# is anything but NONE. +false+ when the archive didn't store the method.
|
|
393
|
+
def encrypted?
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Whether this entry is a directory.
|
|
397
|
+
#
|
|
398
|
+
# True for a name ending in +/+, and for an entry with external attributes
|
|
399
|
+
# that agree: a UNIX mode with +S_IFDIR+, or the MS-DOS directory bit.
|
|
400
|
+
# That's how a directory written by another tool is identified.
|
|
401
|
+
def directory?
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# The entry comment, as a UTF-8 String, or +nil+ when it has none. A
|
|
405
|
+
# comment that isn't valid UTF-8 comes back as ASCII-8BIT. Set it with
|
|
406
|
+
# LibZip::File#set_comment.
|
|
407
|
+
def comment
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# The entry name, so an Entry interpolates into a String as its name.
|
|
411
|
+
def to_s
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# <tt>#<LibZip::Entry name="docs/a.txt" size=1234></tt>
|
|
415
|
+
def inspect
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# A read handle on 1 entry, from LibZip::File#get_input_stream.
|
|
420
|
+
#
|
|
421
|
+
# The entry is decompressed and decrypted in chunks as you read, so memory
|
|
422
|
+
# remains flat whatever the entry's size.
|
|
423
|
+
#
|
|
424
|
+
# LibZip::File.open("archive.zip") do |zip|
|
|
425
|
+
# zip.get_input_stream("big.csv") do |stream|
|
|
426
|
+
# stream.read(64 * 1024) until stream.eof?
|
|
427
|
+
# end
|
|
428
|
+
# end
|
|
429
|
+
#
|
|
430
|
+
# A stream keeps a libzip handle and has to be closed. The block form of
|
|
431
|
+
# LibZip::File#get_input_stream does that for you; otherwise #close does, and
|
|
432
|
+
# LibZip::File#close closes whatever is still open on that archive.
|
|
433
|
+
class InputStream
|
|
434
|
+
# Returns +length+ bytes, or the rest of the entry when +length+ is
|
|
435
|
+
# omitted.
|
|
436
|
+
#
|
|
437
|
+
# Returns an ASCII-8BIT String. A short read means the entry ended: fewer
|
|
438
|
+
# than +length+ bytes come back, and the call after that gives +nil+. With
|
|
439
|
+
# no +length+, a stream already at EOF gives <tt>""</tt>, the same
|
|
440
|
+
# distinction IO makes. <tt>read(0)</tt> gives <tt>""</tt> and consumes no
|
|
441
|
+
# bytes, even at EOF.
|
|
442
|
+
#
|
|
443
|
+
# Fails with ArgumentError for a negative +length+, LibZip::EntryError once
|
|
444
|
+
# the stream is closed, and LibZip::DecompressionError when the data
|
|
445
|
+
# doesn't match its CRC or its AES HMAC, which is only known once the entry
|
|
446
|
+
# has been read to the end.
|
|
447
|
+
def read(length = nil)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Whether the entry has been read to the end. Fails with
|
|
451
|
+
# LibZip::EntryError once the stream is closed.
|
|
452
|
+
def eof?
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Whether the stream has been closed, by #close or by closing the archive.
|
|
456
|
+
def closed?
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Releases the entry handle. Returns +nil+, and has no effect on a stream
|
|
460
|
+
# that's already closed.
|
|
461
|
+
def close
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# A write handle on 1 entry, from LibZip::File#get_output_stream.
|
|
466
|
+
#
|
|
467
|
+
# zip.get_output_stream("data.csv") do |out|
|
|
468
|
+
# out.write("a,b,c\n")
|
|
469
|
+
# out << "1,2,3\n"
|
|
470
|
+
# end
|
|
471
|
+
#
|
|
472
|
+
# What you write is buffered in memory, and becomes an entry only on #close,
|
|
473
|
+
# which the block form calls for you. A stream that isn't closed, because the
|
|
474
|
+
# block failed or because the garbage collector took it, adds no entry. The
|
|
475
|
+
# entry is written to disk when the archive is closed.
|
|
476
|
+
class OutputStream
|
|
477
|
+
# Appends +string+ to the buffer, and returns the number of bytes written.
|
|
478
|
+
# Binary data, NUL bytes included, is written as given.
|
|
479
|
+
#
|
|
480
|
+
# Fails with LibZip::EntryError once the stream has been closed, or the
|
|
481
|
+
# archive has.
|
|
482
|
+
def write(string)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Appends +string+ and returns +self+, so writes chain.
|
|
486
|
+
#
|
|
487
|
+
# out << "Hello, " << "world!\n"
|
|
488
|
+
def <<(string)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Adds the buffered bytes to the archive as the entry, and returns +nil+.
|
|
492
|
+
# LibZip::File#close writes the archive to disk.
|
|
493
|
+
#
|
|
494
|
+
# Encryption is set up here, so a bad encryption setup shows up on #close
|
|
495
|
+
# instead of on #write.
|
|
496
|
+
def close
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Base class for anything this gem fails with, and a StandardError, so a
|
|
501
|
+
# single +rescue LibZip::Error+ handles all of them.
|
|
502
|
+
#
|
|
503
|
+
# begin
|
|
504
|
+
# LibZip::File.open("archive.zip") { |zip| zip.read("nope.txt") }
|
|
505
|
+
# rescue LibZip::NotFoundError => e
|
|
506
|
+
# warn "missing entry: #{e.message}"
|
|
507
|
+
# rescue LibZip::Error => e
|
|
508
|
+
# warn "libzip error: #{e.message}"
|
|
509
|
+
# end
|
|
510
|
+
#
|
|
511
|
+
# Each libzip error code maps to one of the subclasses below; a code with no
|
|
512
|
+
# better home comes back as LibZip::Error.
|
|
513
|
+
class Error < StandardError
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# An operation or an encryption method libzip can't perform.
|
|
517
|
+
class UnsupportedError < Error
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# An open, seek, tell, close, rename or remove of an underlying file failed,
|
|
521
|
+
# or a file ended sooner than expected.
|
|
522
|
+
class IoError < Error
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Reading archive data failed.
|
|
526
|
+
class ReadError < Error
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Writing archive data failed.
|
|
530
|
+
class WriteError < Error
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# No such archive, entry, or source file.
|
|
534
|
+
class NotFoundError < Error
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# An entry exists where one must not, as when renaming to a name that's
|
|
538
|
+
# taken.
|
|
539
|
+
class AlreadyExistsError < Error
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Not a zip file, truncated, inconsistent, multi-disk, or with compressed
|
|
543
|
+
# data that makes no sense.
|
|
544
|
+
class CorruptArchiveError < Error
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# The compression method isn't supported.
|
|
548
|
+
class CompressionError < Error
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Decompression failed: the data doesn't match its CRC or its AES HMAC.
|
|
552
|
+
class DecompressionError < Error
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# An argument libzip or this gem turns away: a directory as a source file, a
|
|
556
|
+
# comment past 65535 bytes, an unknown +encryption+, a password without
|
|
557
|
+
# encryption, an entry from another archive.
|
|
558
|
+
class InvalidArgumentError < Error
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# The archive is read-only, or the operation isn't allowed on it.
|
|
562
|
+
class PermissionError < Error
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# The password is missing or incorrect. Comes when the entry is read, not
|
|
566
|
+
# when the archive is opened.
|
|
567
|
+
class PasswordError < Error
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# An entry, stream or archive used after it was closed, deleted or changed.
|
|
571
|
+
class EntryError < Error
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# libzip or zlib failed internally, or ran out of memory.
|
|
575
|
+
class InternalError < Error
|
|
576
|
+
end
|
|
577
|
+
end
|
data/lib/libzip-ruby.rb
ADDED
data/lib/libzip.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "rbconfig"
|
|
2
|
+
|
|
3
|
+
# The entry point: `require "libzip"`.
|
|
4
|
+
#
|
|
5
|
+
# A precompiled gem ships one extension per Ruby ABI under
|
|
6
|
+
# lib/libzip_ruby/<abi>/, <abi> being the major.minor of the interpreter it was
|
|
7
|
+
# built for (an extension built for 4.0 loads on any 4.0.x). In a source
|
|
8
|
+
# checkout that directory does not exist and we fall through to $LOAD_PATH,
|
|
9
|
+
# which `zig build`/`script/package` point at zig-out/lib.
|
|
10
|
+
abi = RbConfig::CONFIG["ruby_version"][/\A\d+\.\d+/]
|
|
11
|
+
precompiled = File.expand_path("libzip_ruby/#{abi}", __dir__)
|
|
12
|
+
$LOAD_PATH.unshift(precompiled) if File.directory?(precompiled)
|
|
13
|
+
|
|
14
|
+
require "libzip_ruby"
|
|
15
|
+
|
|
16
|
+
# Everything this gem defines lives under LibZip. There is deliberately no
|
|
17
|
+
# top-level `Zip` constant: rubyzip owns that name, and defining a second, much
|
|
18
|
+
# smaller `Zip` would hijack any transitive `require "zip"` that resolves to us.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/libzip-ruby.gemspec
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Gem::Specification.new do |spec|
|
|
2
|
+
spec.name = "libzip-ruby"
|
|
3
|
+
# Single source of truth: build.zig reads this file and stamps the value into
|
|
4
|
+
# LibZip::VERSION, script/package tags the release, so they cannot disagree.
|
|
5
|
+
spec.version = "0.1.0"
|
|
6
|
+
spec.summary = "libzip bindings for Ruby, with libzip and zlib linked in"
|
|
7
|
+
spec.description = <<~TEXT
|
|
8
|
+
Ruby bindings for libzip, the C library for reading and writing zip
|
|
9
|
+
archives.
|
|
10
|
+
TEXT
|
|
11
|
+
spec.authors = ["Jean-Paul Pierre Louis Fiorini"]
|
|
12
|
+
spec.email = ["fiorini751@proton.me"]
|
|
13
|
+
spec.license = "Apache-2.0"
|
|
14
|
+
|
|
15
|
+
# The oldest interpreter we ship a compiled extension for (script/package).
|
|
16
|
+
spec.required_ruby_version = ">= 3.3"
|
|
17
|
+
|
|
18
|
+
# script/package sets this while assembling a precompiled gem, so the .gem
|
|
19
|
+
# carries its own platform; a plain `gem build` stays platform-independent.
|
|
20
|
+
spec.platform = Gem::Platform.new(ENV["LIBZIP_RUBY_PLATFORM"]) if ENV["LIBZIP_RUBY_PLATFORM"]
|
|
21
|
+
|
|
22
|
+
spec.homepage = "https://github.com/Tipuch/libzip-ruby"
|
|
23
|
+
# No homepage_uri: spec.homepage already is that URL, and RubyGems complains
|
|
24
|
+
# when 2 metadata keys point at the same URL.
|
|
25
|
+
spec.metadata = {
|
|
26
|
+
"source_code_uri" => spec.homepage,
|
|
27
|
+
"bug_tracker_uri" => "#{spec.homepage}/issues",
|
|
28
|
+
"documentation_uri" => "https://tipuch.github.io/libzip-ruby/",
|
|
29
|
+
"rubygems_mfa_required" => "true",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
spec.files = Dir["lib/**/*"] + Dir["doc/*.rb"] +
|
|
33
|
+
["libzip-ruby.gemspec", "LICENSE", "README.md"]
|
|
34
|
+
spec.require_paths = ["lib"]
|
|
35
|
+
|
|
36
|
+
# RubyGems documents require_paths + extra_rdoc_files at install time, and
|
|
37
|
+
# the entire API is written out in doc/api.rb (the extension is Zig, which
|
|
38
|
+
# RDoc can't parse), so `ri LibZip::File` works only if it's listed here.
|
|
39
|
+
spec.extra_rdoc_files = Dir["doc/*.rb"] + ["README.md"]
|
|
40
|
+
spec.rdoc_options = ["--main", "README.md", "--title", "libzip-ruby #{spec.version}"]
|
|
41
|
+
end
|