Compare commits

...

326 Commits
2.4 ... master

Author SHA1 Message Date
Robert Haines 1f3f84c889 Update version number and Changelog for RC2. 2025-02-08 17:18:43 +00:00
Robert Haines deca4d5aeb Fix de facto regression for input streams.
A close reading of the ZIP spec insists that if bit 3 of the GP flags is set
then the archive cannot be read via `Zip::InputStream`. But in most cases
the correct information is present to be able to do so, both safely and
reliably, and v2.4 does allow this.

This commit ensures that behaviour is present in v3.0.
2025-02-08 16:51:30 +00:00
Robert Haines 98881e23d1 Add a test to ensure correct version number format.
Hopefully this will avoid a repeat of the '2.4' debacle...
2025-02-01 16:31:20 +00:00
Robert Haines 43d845c2cb Update version number, README and Changelog for RC1.
Also update the year to 2025 in the licence.
2025-01-26 22:23:25 +00:00
Geremia Taglialatela 3f909b2bdc Fix CI against JRuby, JRuby-head, and Windows
Additionally:
- Use `latest` rubygems, instead of specifying a version and keeping
  it up to date
- Bump `rake` dependency to `~> 13.2.0` to allow tests to pass against
  Windows
2025-01-25 17:39:22 +00:00
Robert Haines f7c6b79256 Update some dev dependency gems and relax version matching.
For gems such as rake, minitest and rdoc it is appropriate to allow version
matches against major versions, allowing us to get the latest minor and
patch versions automatically.

Also update the minimum minor versions while we are at it.

We'll leave rubocop* and simplecov* gems pinned to minor versions.
2025-01-25 09:55:27 +00:00
Geremia Taglialatela 8ed6662891 Fix JRuby CI tests
Pin `jar-dependencies` to `0.4.1`.

Ref: jruby/jruby#7262

Close #626
2025-01-07 19:14:56 +00:00
Geremia Taglialatela 0d920d552f Update README with Ruby version compatibility
This commit updates the README to reflect the Ruby versions that are
known to work with version 2.x of the library. Specifically,
it documents that version 2.x works on Ruby 3.x.

Ref: 0001864cfe
2025-01-07 17:58:42 +00:00
Robert Haines 89cdf82a77 Copy the 2.4 branch changelog into the main branch. 2025-01-06 18:58:44 +00:00
Robert Haines b3186d7ad1 Update the version of RubyGems in the actions. 2025-01-04 16:43:51 +00:00
Robert Haines 7412338c2e Fix variable name typo in `DecryptedIo`.
Fixes: #580
2025-01-04 16:18:27 +00:00
Robert Haines d908e72819 Add Ruby 3.4 to the tests. 2025-01-04 16:17:25 +00:00
Bastien 5b0d25e416 Fix misspell 2024-10-26 20:05:18 +01:00
Robert Haines f81175b3d3 Update Changlog. 2024-10-26 19:16:55 +01:00
Jean Boussier 5c6a7c9ad9 Fix `File#write_buffer` to always return the given `io`
Ref: ef89a62b70

This fixes a regression in 2.4.rc1.

Cherry-picked into 3.0 for consistency.
2024-04-09 10:10:47 +01:00
Robert Haines 0c0003cfda Add `DOSTime#absolute_time?`.
This method returns `true` if the time instance was created with accurate
timezone information. Ultimately, only those times parsed from binary
DOS format are missing accurate timezone information, but we need this
flag because ruby `Time` objects (from which `DOSTime` is decended) always
have a timezone set (usually whatever is local at the time).
2024-04-08 18:37:08 +01:00
Robert Haines d53f046bc7 Add `Entry#absolute_time?`.
This method returns `true` if an entry has timezone information in its
timestamps, `false` otherwise.
2024-03-07 21:31:24 +00:00
Robert Haines 8afc2514f7 Use explicit named parameters for `File` methods.
Stop using implicit options (`**options`).
2024-03-07 16:57:50 +00:00
Robert Haines 73c8e110ed Update README with up-to-date licence information.
Fixes: #556
2024-03-06 20:53:48 +00:00
Oleksii Leonov 0e4fc83b25 Add LICENSE file 2024-03-06 20:50:19 +00:00
Robert Haines c6229cc8c2 Remove TODO file.
Out of date and we use GitHub issues now anyway.
2024-03-05 20:35:56 +00:00
Robert Haines bfc9324a75 Remove deprecated InputStream::open_buffer method. 2024-03-02 14:55:46 +00:00
Robert Haines e83bec471b Update API documentation.
Mostly move things out of the public API by removing them from the docs,
but also add and correct docs where appropriate.
2024-03-02 14:52:24 +00:00
Robert Haines 9cfa01a479 Update note about minimum Ruby version in the README. 2024-03-02 14:51:25 +00:00
Robert Haines 04376dc2bc Update GitHub actions to Node.js 20 versions. 2024-03-01 22:32:19 +00:00
Robert Haines 1c06454985 Update minimum ruby version to 3.0.
All rubies before 3.0 are EOL and this is a major version bump, so it's
the right time to do this.
2024-03-01 22:14:48 +00:00
Kyle Huston b1ee5cf272 Note write_zip64_support default is false before 3.0
Improve accuracy of readme to note that write_zip64_support
is enabled by default in versions 3.0 and later.
2024-02-24 18:33:52 +00:00
Robert Haines fd0cf5443e Update ZipCrypto instructions for 2.x versions.
Suggested by @KamilDzierbicki in #568.
2024-02-24 17:52:33 +00:00
Robert Haines e3c173b0fc Update the compatibility matrix in the README. 2024-02-24 17:36:52 +00:00
m-nakamura145 5e9a9cbf18 Add Ruby 3.3 to CI matrix 2024-02-24 17:15:35 +00:00
MSP-Greg f811b2d00e Update README.md 2023-08-12 09:51:01 +01:00
MSP-Greg bb09f90ef9 Action - add 2 Windows head builds 2023-08-12 09:51:01 +01:00
Robert Haines 8cec9491b2 README: add link to wiki for version 3 details. 2023-04-16 16:13:30 +01:00
OZAWA Sakuro a2fc20db8f Zip::InputStream#read returns '' with 0 2023-04-14 11:25:22 +01:00
OZAWA Sakuro a4f9ec6423 Add compatibility test for Zip::InputStream#read(0) 2023-04-14 11:25:22 +01:00
Peter Boling 3ce504fcfb Fix typo know -> known 2023-04-14 11:25:22 +01:00
Robert Haines efa0035634 Turn on yjit for Ruby 3.2 in CI. 2023-04-14 11:25:22 +01:00
Peter Goldstein c0a3c95eee Add Ruby 3.2 to the CI matrix. 2023-04-14 11:25:22 +01:00
Robert Haines e672c46895 Use an updated rubygems in the tests CI.
This is needed for Ruby 2.5, which we need to support a bit longer.
2023-04-14 11:25:22 +01:00
Robert Haines f4a1a09962 Update the linter CI to use Ruby 2.6. 2023-04-14 11:25:22 +01:00
Robert Haines f5ea5a8708 Update Actions to use checkout@v3. 2023-04-14 11:25:22 +01:00
Robert Haines 2ffbfebb88 Document or hide classes from the docs. 2023-04-14 11:25:22 +01:00
Robert Haines 0aa10bf7d5 Document or hide modules from the docs. 2023-04-14 11:25:22 +01:00
Robert Haines 016e1000ba Suppress Rubocop extension notice. 2023-04-14 11:25:22 +01:00
Robert Haines edeab0713c Add RDoc and tasks to the Rakefile. 2023-04-14 11:25:22 +01:00
Robert Haines ccc3d4ff1a Set version to be 3.0.0.alpha. 2023-04-14 11:25:22 +01:00
Robert Haines 84087e5774 Ensure that entries can be extracted safely without path traversal.
This commit adds a parameter to the `File#extract` and `Entry#extract` methods
so that a base destination directory can be specified for extracting archives
in bulk to somewhere in the filesystem that isn't the current working
directory. This directory is `.` by default. It is combined with the entry
path - which shouldn't but could have relative directories (e.g. `..`) in it -
and tested for safety before extracting.

Resolves #540.
2023-04-14 11:15:24 +01:00
Robert Haines 58f053afb0 Only use the Zip64 CDIR end locator if needed.
Previously the central directory Zip64 data was written even if it wasn't
strictly needed. The standard allows for entries to include Zip64 data
(say, if they are streamed and their size is unknown when writing the file
data) without needing any Zip64 data in the central directory. So now we
only write central directory Zip64 data if there are over 65535 files or
the file data is huge.
2023-01-03 20:19:40 +00:00
Robert Haines f460da3afb Prevent unnecessary Zip64 data being stored.
With Zip64 write support enabled by default, it's important that we
only store the extra data when we need to. This commit ensures that
the Zip64 extra data is included for an entry if its size is over
4GB, or if we don't know how big it will be at the point of writing
the local header data.

This commit also removes the need for the Zip64Placeholder extra
data field. Now we just use the Zip64 field itself and ensure it's
filled in correctly.
2023-01-03 20:19:40 +00:00
Stan Hu d6eb73566c Enable Zip64 by default
Previously if RubyZip attempted to create an archive with more than
64K entries, the central directory would truncate the count. `unzip`
and `zipinfo` would fail with an error message such as:

```
error:  expected central file header signature not found (file #93272).
  (please check that you have transferred or created the zipfile in the
  appropriate BINARY mode and that you have compiled UnZip properly)
```

This generated a lot of confusion and a production issue since many
tools fail to decode a RubyZip-created archive if Zip64 is not enabled
for a large number of files. Since Zip64 support is now the norm,
enable this by default.
2023-01-03 20:19:40 +00:00
Robert Haines 750d372380 Rename DestinationFileExistsError -> DestinationExistsError.
And define the error message within the class.
2022-08-16 11:13:30 +01:00
Robert Haines e3f0aecf93 Define the EntryNameError message within the error class. 2022-08-16 10:52:18 +01:00
Robert Haines 07eca2bae8 Define the EntrySizeError message within the error class. 2022-08-15 22:02:33 +01:00
Robert Haines 7097492dc8 Define the EntryExistsError message within the error class. 2022-08-14 22:23:51 +01:00
Robert Haines 04cc10a80f Remove the InternalError class (never used). 2022-08-14 22:23:51 +01:00
Robert Haines 51231673a4 Define the DecompressionError message within the error class. 2022-08-14 22:23:51 +01:00
Robert Haines 19fe79e31e Define the SplitArchiveError message within the error class. 2022-08-14 22:23:51 +01:00
Robert Haines 03a9ee6b8a Rename `GPFBit3Error` to `StreamingError`.
`GPFBit3Error` doesn't really mean anything to the general user, and
it's not descriptive of the issue at hand. This error is raised when a
zip file cannot be streamed via `InputStream`, so `StreamingError` makes
more sense.

Also standardize the error message while we're about it.
2022-08-14 22:23:51 +01:00
Robert Haines 2e4dd9e0aa Improve the message for CompressionMethodError.
Convert the compression method number into a meaningful text
representation, e.g., "BZIP2" instead of "12".
2022-08-14 22:23:51 +01:00
Robert Haines 08391da4d5 Ensure that `Entry.ftype` is correct via `InputStream`.
When reading an archive with `InputStream`, `Entry.ftype` was returning
`:file` for all entries, even if they were a directory. This is due to
various side-effects in many methods in `Entry`. This commit fixes the
behaviour, but not the side-effects.

Fixes #533.
2022-08-13 22:09:55 +01:00
Brian Williams 6f1ad8b37d Fix unraised error on encrypted archives 2022-08-09 22:11:42 +01:00
Robert Haines 708b7f5393 Add a couple more checks in the tests for double `commit`s.
Just ensure that a `commit` really does stick with both new and edited
zip files.
2022-06-25 08:53:35 +01:00
Robert Haines 14ff11ba05 Re-initialize CDir after a `commit`.
Using the factored-out code preserves options set in `File`.

Fixes #529.
2022-06-25 08:51:32 +01:00
Robert Haines c243b4429a Factor out the code in `File` to init the CDir.
This allows us to reuse it without overwriting any options passed to `File`.
2022-06-25 08:48:23 +01:00
Robert Haines 6486047d5f Use the new `Entry` time methods in `Filesystem::File`. 2022-06-20 17:18:20 +01:00
Robert Haines 466383ff1a Add other `Entry` time methods and test them all. 2022-06-20 17:18:20 +01:00
Robert Haines fff1f8ea8a Add `Entry#mtime=` as an alias of `Entry#time=`. 2022-06-20 17:18:20 +01:00
Robert Haines 62ed397b1a Generalize `Entry#time=`.
So we can use it for `atime=`, `ctime=` and `utime=` as well.
2022-06-20 17:18:20 +01:00
Robert Haines d6482bd567 Generalize `Entry#time`.
So we can use it for `atime`, `ctime` and `utime` as well.
2022-06-20 17:18:20 +01:00
Robert Haines ae0262df2e Add `Entry#zip64?` as a better way detect Zip64 entries. 2022-06-20 17:18:20 +01:00
Robert Haines 307fc6c6e9 Mark other mutating methods in `Entry` as dirty.
Also, remove `Entry#extra=` as it makes no sense (and wasn't even being
tested).

And remove slightly odd test that was assuming an archive would not be
changed if its utime was changed - even if it was being changed back
immediately. This test was merely confirming that we weren't catching
timestamp changes correctly.
2022-06-20 17:18:20 +01:00
Robert Haines 33dce510a6 Remove `Entry#dirty=` as 'dirtyness' is now monitored internally.
Had to round out some of the accessors that mark an `Entry` as dirty.
2022-06-20 17:18:20 +01:00
Robert Haines 5cd1ef2910 Use new dirty statuses to detect zip file changes.
This also means that we no longer need to keep a copy of the original
set of `Entry`s or the central directory comment to test for changes.

For situations where a zip file has a lot of entries (e.g. #506) this
means we save a lot of memory, and a lot of time constructing the zip
file in memory.
2022-06-20 17:18:20 +01:00
Robert Haines 08641db9f8 Make `CentralDirectory` dirtyable. 2022-06-20 17:18:20 +01:00
Robert Haines 7b340d62a6 Abstract marking as dirty into `Dirtyable` for reuse. 2022-06-20 17:18:20 +01:00
Robert Haines 3002251048 Mark certain methods in `Entry` as making it dirty.
This allows us to track which entries have changed without keeping a
copy of all entries. I hope.
2022-06-20 17:18:19 +01:00
Robert Haines 78a3cc596f Make `Entry::zipfile` private.
No need for it to be public, and especially not writeable.
2022-06-20 17:18:19 +01:00
Robert Haines e0e754ae65 Switch how the `Entry::dirty` flag is used.
Set it to true by default - because a new `Entry` is dirty by
definition, having not been written yet. Then make sure that an `Entry`
that is created by reading from a zip file is set as not dirty.
2022-06-20 17:18:19 +01:00
Robert Haines 48d6acf9ca Ensure all streams passed to `File.new` are in `binmode`.
Previously, only those streams that were passed to `new` by `open_buffer`
were in the correct mode.
2022-06-18 16:19:52 +01:00
Robert Haines 513ce5e5f7 Remove unnecessary encoding change in tests for `File`. 2022-06-18 12:45:59 +01:00
Finn Bacall 451a04f7a2 Test for `Errno::ENOENT` 2022-06-16 20:31:35 +01:00
Finn Bacall 8b87b0e200 Implement `Zip::FileSystem::ZipFsFile#symlink?` 2022-06-16 20:31:35 +01:00
Robert Haines ffa90a37cb README: improve the crypto documentation.
Clean it up and provide a decryption example.
2022-04-23 13:54:09 +01:00
Robert Haines e07f019507 Improve the description of `InputStream#get_next_entry`.
Documentation now refects the fact that the stream is positioned at the start
of the entry data.
2022-04-23 13:52:50 +01:00
naoto hamada 243a66496a Fix indent 2022-04-09 07:48:45 +01:00
Robert Haines d2789dd0e3 Add a note to the README about 2.3 compatibility.
Closes #520.
2022-02-06 15:03:01 +00:00
Robert Haines 05a1739069 Properly test `File#mkdir`. 2022-01-22 08:39:43 +00:00
Robert Haines 31e6688528 Remove unused private method `File#directory?`.
This was a fairly horrible method anyway, for a number of reasons. It
looked like a method that tested whether a name was a 'directory' name
or not, and it did, but it also had some side effects where it would
convert it *to* a directory name in some cases as well. Thankfully,
nothing was using it any more, and as it was private we can lose it
safely. Gone.
2022-01-22 07:38:18 +00:00
Robert Haines e2e0e23763 Remove `File::add_buffer` from the API.
Its functionality is now replicated in `File::open_buffer` but in a more
secure way.
2022-01-22 07:34:00 +00:00
Robert Haines 044759f502 Fix `OutputStream#put_next_entry` to preserve `StreamableStream`s.
When passing an `Entry` type to `File#get_output_stream` the entry is
used to create a `StreamableStream`, which preserves all the info in the
entry, such as timestamp, etc. But then in `put_next_entry` all that is
lost due to the test for `kind_of?(Entry)` which a `StreamableStream` is
not. See #503 for details.

This change tests for `StreamableStream`s in `put_next_entry` and uses
them directly. Some set-up within `Entry` needed to be made more robust
to cope with this, but otherwise it's a low impact change, which does
fix the problem.

The reason this case was being missed before is that the tests weren't
testing `get_output_stream` with an `Entry` object, so I have also added
that test too.

Fixes #503.
2022-01-20 19:29:40 +00:00
Robert Haines 4cf801c5f3 Tidy up `EntrySet` accessors.
`entry_order` is no longer a member, so remove it. `entry_set` should
not be public, but needs to be protected for use in `==`.
2022-01-18 20:09:34 +00:00
Robert Haines 8489ab07d1 `OutputStream`: use a `CentralDirectory` object internally.
Now `CentralDirectory` is a bit cleaner it actually makes sense to use
it here instead of an `EntrySet` and comment separately.
2022-01-17 22:32:56 +00:00
Robert Haines 60f8fffbc2 Reorder methods in `CentralDirectory` with private at the end. 2022-01-17 22:04:45 +00:00
Robert Haines bdbd573290 Remove unnecessary static method from `CentralDirectory`.
`CentralDirectory` shouldn't be in the public API for rubyzip and
there's nothing that `CentralDirectory::read_from_stream` did that
couldn't be done by just initializing an object first. Keeping it around
risked things getting out of date as we streamline and fix other things.
2022-01-17 18:10:17 +00:00
Robert Haines 75503df682 Round out the max comment size tests.
Just sanity check the comment size and the number of entries once the
file has been initialized.
2022-01-17 18:03:04 +00:00
Robert Haines 9c3f8254c7 Fix reading zip64 files with max length file comment.
If a zip file has a comment that is 65,535 characters long - which is a
valid length and the maximum allowable length - the initial read of the
archive fails to find the Zip64 End of Central Directory Locator and
therefore cannot read the rest of the file.

This commit fixes this by making sure that we look far enough back into
the file from the end to find this locator, and then use the
information in it to find the Zip64 End of Central Directory Record.

Test added to catch regressions.

Fixes #509.
2022-01-17 18:02:39 +00:00
Robert Haines 1d6bfb7e69 Expose the `EntrySet` more cleanly through `CentralDirectory`.
There is now no direct access to the set of entries in a central
directory. This makes the interface cleaner because we now, for example,
add/delete things directly to/from the central directory, rather than
to/from the entry set contained within the central directory.
2022-01-16 11:53:40 +00:00
Robert Haines 34731b1885 `Zip::File` no longer subclasses `Zip::CentralDirectory`.
It has bothered me for years that the central directory is exposed in
this way. A zip file should *have* a central directory, but it should
not *be* one.

This commit starts us down the path of properly separating the two.
2022-01-15 13:10:54 +00:00
Robert Haines f8b9d07022 Round out EOCD data size constants in CDir. 2022-01-12 09:13:15 +00:00
Robert Haines b1b82bbd9e Tidy up updating notes in README. 2022-01-11 22:26:50 +00:00
Robert Haines 7f7c4ca194 Update README after the move to Ruby >= 2.5. 2022-01-11 22:26:50 +00:00
Robert Haines cf258bbb71 Move to ruby 2.5 as the earliest supported version.
2.4 is nearly two years beyond EOL now.

Closes #484.
2022-01-11 22:26:09 +00:00
Robert Haines 90728d7109 Add `-v` switch to ruby for all tests. 2022-01-11 13:24:35 +00:00
Robert Haines 20952ef38f Update version numbers in the README test matrix. 2022-01-11 13:19:31 +00:00
Robert Haines 3fbc5e62f9 Add the YJIT tests to the README matrix. 2022-01-11 13:15:48 +00:00
Robert Haines 9bed9d0539 Expand the YJIT tests into a mini matrix.
Test 3.1 and head on Ubuntu and MacOS.
2022-01-11 13:02:00 +00:00
Robert Haines 099d379c82 Add an extra test for YJIT in ruby 3.1.
I tried adding this to the matrix, but I couldn't work out how to do
this *and* keep a vanilla 3.1 test in the mix as well. It seems you
can't add different `env`s with `include`, but maybe I missed something.
2022-01-10 18:16:00 +00:00
Robert Haines e04c9cdbd8 Update compatibility matrix in the README. 2022-01-02 13:57:09 +00:00
Robert Haines 8f743d7f68 Make ruby versions list in the CI consistent.
Ruby version `3.0` must be quoted otherwise it's interpreted as `3`.
Might as well make the rest in the list consistent.
2022-01-02 13:09:06 +00:00
Taichi Ishitani 1c33f2dd90 add Ruby 3.1 to CI 2022-01-02 13:06:07 +00:00
Robert Haines 14b63f68db Ensure `File.open_buffer` doesn't rewrite unchanged data. 2021-11-30 22:22:37 +00:00
Robert Haines f5e19db273 Add a 100,000 file zip to test `count_entries`. 2021-11-20 20:02:47 +00:00
Robert Haines 22e47641e6 Add `File::count_entries`.
This method provides a short cut to finding out how many entries are in
an archive by reading this number directly from the central directory,
and not iterating through the entire set of entries.
2021-11-20 10:53:00 +00:00
Robert Haines 3db1eff1e3 Add `CentralDirectory#count_entries`.
This method gets the number of entries from a zip archive without
loading all of the individual entries.
2021-11-20 10:50:55 +00:00
Robert Haines 6a516fb0b1 Factor out reading EOCD records.
This allows for reading the EOCD records without then automatically
reading all of the entry data as well, so that we can do other things
faster, like provide the number of entries in an archive.
2021-11-20 10:36:32 +00:00
Robert Haines 765cb316f1 Fix reading unknown extra fields.
When loading extra fields from both the central directory and local headers,
unknown fields were not merged correctly. They were being appended, which
means that we end up with the two versions stuck together - in some
cases duplicating the field completely.

This broke all kinds of things (like calculating the size of a local
header) in subtle ways.

This commit fixes this by implementing a new `Unknown` extra field type,
and making sure that when reading local and central extra fields they
are stored and preserved correctly. We cannot assume the unknown fields
use the same data in the local and central headers.

Fixes #505.
2021-11-19 19:53:38 +00:00
Robert Haines f7cd692e15 Fix reading zip files with max length file comment.
If a zip file has a comment that is 65,535 characters long - which is a
valid length and the maximum allowable length - the initial read of the
archive fails to find the End of Central Directory Record and therefore
cannot read the rest of the file.

This commit fixes this by making sure that we look far enough back into
the file from the end to find the EoCDR. Test added to catch
regressions.

Fixes #508.
2021-11-19 19:35:36 +00:00
Robert Haines bc6523ec43 Unpick changes from v2.3.1. 2021-07-05 22:22:01 +01:00
Robert Haines c3b1e5d693 Pick changes from v2.3.1. 2021-07-03 13:45:22 +01:00
Robert Haines 54b7762c8f Don't silently alter zip files opened with `Zip::sort_entries`.
Fixes #329.
2021-06-30 23:18:59 +01:00
Yo Yehudi 322955c6b4 add research notice
so that contributors will be aware metrics are being gathered on this repo over the  next year.  If there's a better place to put this please let me know. :)
2021-06-28 13:18:45 +01:00
Robert Haines 66527ae10d Fix minor typo in `GPFBit3Error` message in `InputStream`. 2021-06-27 21:55:15 +01:00
Robert Haines 19e5f4a8ce Detect and raise GPFBit3Error in `InputStream.get_next_entry`.
We were previously trying to work out where the next entry would be,
even with GP bit 3 set, but the logic was flaky and cannot really be
correct given the data available. It's not expected behaviour, so raise
the error instead.

This means that we get rid of the incorrect `Entry.data_descriptor_size`
which was doing more harm than good.
2021-06-27 21:43:03 +01:00
Robert Haines 8071290ce6 Update and tidy up encryption tests. 2021-06-27 15:56:39 +01:00
Robert Haines 50dddca0be Update encrypted fixtures to remove data descriptors. 2021-06-27 15:54:08 +01:00
Robert Haines bb237aaa08 Update authors in gemspec to reflect current maintainers. 2021-06-27 11:47:31 +01:00
Robert Haines f005ca2864 Update the list of files packaged in the gemspec. 2021-06-27 11:30:09 +01:00
Robert Haines 81d95ad0a3 Minor gemspec formatting changes for space/readability. 2021-06-27 11:29:55 +01:00
Robert Haines 1fb74bd82f Don't mess with the library path in the gemspec. 2021-06-27 11:04:53 +01:00
Robert Haines 9fc12bf97b Add notes to the README and Changelog about the new API. 2021-06-27 10:20:11 +01:00
Robert Haines aa646ef827 Use named params for `InputStream`. 2021-06-27 10:20:11 +01:00
Robert Haines f75eb61578 Use named parameters for `File#get_output_stream`. 2021-06-27 10:20:11 +01:00
Robert Haines debc9fda91 Use named parameters for `File::split`. 2021-06-27 10:20:11 +01:00
Robert Haines f033ae760d Use named parameters for `File::new`.
This is a breaking change, but now is the time to do this as we've
already done the same for `Entry::new`.
2021-06-27 10:20:11 +01:00
Robert Haines e1e1cab39c Fix some non-writable `StringIO`s. 2021-06-27 10:20:11 +01:00
Robert Haines 659db85bff `open` and `write_buffer` in `OutputStream` use named params. 2021-06-27 10:20:11 +01:00
Robert Haines 7ae90be63e Fix Style/OptionalBooleanParameter in `OutputStream`. 2021-06-27 10:20:11 +01:00
Robert Haines e7f0aba5ff Fix Style/OptionalBooleanParameter cop in `Entry`.
Just an internal API so safe, and makes things a lot neater.
2021-06-27 10:20:11 +01:00
Yuya.Nishida f76cef90f7 Remove newer duplicated line 2021-06-26 22:53:37 +01:00
Robert Haines 8699e356d4 Improve documentation for `File.glob`.
Closes #338.
2021-06-26 20:04:17 +01:00
Robert Haines a301d68eeb Raise an error if entry names exceed 65,535 characters.
Fixes #247.
2021-06-26 19:21:07 +01:00
Robert Haines 49e313629e Remove the `ZipXError` v1 legacy classes. 2021-06-26 17:39:25 +01:00
Robert Haines e000552deb Raise an error on reading a split archive with `InputStream`.
Fixes #349.
2021-06-26 12:39:08 +01:00
Robert Haines 193507b15a Adjust Layout/LineLength cop to 100 characters.
We'll get the line length down in stages...
2021-06-25 22:31:34 +01:00
Robert Haines de6ec15610 Update Changelog. 2021-06-25 17:54:30 +01:00
Robert Haines 84b3e8c644 Ensure `InputStream` raises `GPFBit3Error` for OSX Archive files.
Fixes #493.
2021-06-25 17:53:18 +01:00
Robert Haines 78565db40c Simplify `InputStream.open_entry`.
Also ensure `@complete_entry` is initialized!
2021-06-25 17:53:18 +01:00
Robert Haines c29297c0b8 Add a test to ensure `InputStream` raises `GPFBit3Error`. 2021-06-25 17:53:18 +01:00
Robert Haines ac053bd787 Improve documentation and error messages for `InputStream`.
Closes #196.
2021-06-25 16:58:01 +01:00
Robert Haines 1183607ea1 Flush buffered `OutputStream` on close.
Fixes #265.
2021-06-23 22:24:44 +01:00
Robert Haines a2a14c2cd2 Fix Style/RedundantRegexpEscape cop. 2021-06-18 16:31:23 +01:00
Robert Haines f1e8c2fc9d Fix Style/StringConcatenation cop. 2021-06-18 16:10:57 +01:00
Robert Haines f66a15a85d Update rubocop config. 2021-06-18 16:00:57 +01:00
Robert Haines 71f2c90b20 Test that a corrupted cdir entry is caught. 2021-06-18 12:08:31 +01:00
Robert Haines 75386f8db6 Remove now redundant `IOizeString` module. 2021-06-18 11:50:07 +01:00
Robert Haines afe1892208 Fix a mis-firing CentralDirectory test.
`test_read_from_truncated_zip_file` was not testing what it thought it
was. It was testing whether we caught an out-of-bounds cdir offset, not
whether we caught a corrupted cdir entry.

This commit embraces the actual behaviour and tests that we catch an
out-of-bounds error for both standard `IO`s and `StringIO`s.
2021-06-18 11:44:58 +01:00
Robert Haines bf3ae2ad76 Improve some entry header tests.
Use `StringIO` instead of the custom `IOizeString` code in the
test_helper.

Also test both versions (class and instance) of the `Entry` APIs.
2021-06-18 11:11:11 +01:00
Robert Haines 750c474610 Update Changelog. 2021-06-12 16:31:50 +01:00
Robert Haines 21ba82c67c Move the split signature to the constants file. 2021-06-12 16:29:25 +01:00
Robert Haines 80382135e5 Tidy up some of the file split code. 2021-06-12 16:29:25 +01:00
Robert Haines bd2f15e4bb Extract the `Zip::File::split` code into its own module.
This code is rarely used and may not even be correct according to the
standard. Also this de-clutters the `File` class.
2021-06-12 16:29:06 +01:00
Robert Haines 7df623fb0e Read EOCD record for Zip64 files.
Means we actually read in the file-level comment now!

Fixes #492.
2021-06-11 23:23:34 +01:00
Robert Haines be1c5b7c03 Turn on FULL_ZIP64_TEST in CI. 2021-06-11 17:28:52 +01:00
Robert Haines 3fbb48de31 Refactor the full 64bit tests. 2021-06-11 17:28:52 +01:00
Robert Haines d8111826bf Remove the now redundant `read_zip_*` methods.
We're unpacking headers in chunks now, using `unpack`.
2021-06-11 13:51:40 +01:00
Robert Haines dc27c99eb1 Refactor unpacking the Zip64 eocd record. 2021-06-11 13:50:09 +01:00
Robert Haines 7e254dc581 Refactor unpacking the eocd record.
The old version used some really obfuscated code to perform what is an
essentially fairly simple job.
2021-06-10 22:44:51 +01:00
Robert Haines cd9a3fcad1 Move all the `read_zip_*` methods out of `Entry`.
They were only ever used in `CentralDirectory` anyway.
2021-06-10 17:29:00 +01:00
Robert Haines c0f20321ae Merge branch 'fix_depreciation_warning' of https://github.com/bbuchalter/rubyzip into bbuchalter-fix_depreciation_warning
* 'fix_depreciation_warning' of https://github.com/bbuchalter/rubyzip:
  Use default ruby behavior for Array.join
  Remove OUTPUT_FIELD_SEPARATOR-related test behaviors
  Set OUTPUT_FIELD_SEPARATOR to nil in test
  Prefer OUTPUT_RECORD_SEPARATOR to $\
  Prefer OUTPUT_FIELD_SEPARATOR to $,
2021-06-07 20:02:15 +01:00
Robert Haines 51c6c10e7a Add a compatibility table to the README.
Fixes #455.
2021-06-06 20:06:59 +01:00
Robert Haines 4fe6bc8983 Update README to remove link to Travis. 2021-06-06 18:11:41 +01:00
Jan-Joost Spanjers 4a01537f32 Fix restore permissons test on Windows 2021-06-06 16:17:22 +01:00
Jan-Joost Spanjers 52bcfc72f9 Revert "REVERT ME: This disables a test fixed by #486"
This reverts commit 6ac2cb207d.
2021-06-06 16:17:22 +01:00
Robert Haines 369056ff30 Update Changelog. 2021-06-06 16:17:22 +01:00
Robert Haines 2410f2889e Restore file timestamps on all platforms.
Was only being done on Unix-type filesystems for some reason. Moved code
so that it is run for all files, whatever the underlying platform.
2021-06-06 16:17:22 +01:00
Robert Haines 3260e4e666 Add some tests to ensure the default behaviour sticks. 2021-06-06 16:17:22 +01:00
Robert Haines a6c6345084 Set restoring permissions and times as the default. 2021-06-06 16:17:22 +01:00
Robert Haines 684b69f330 Move the restore options to the top level.
This will ensure consistency between `File` and `Entry`.
2021-06-06 16:17:22 +01:00
Robert Haines 098bce399a Update Changelog. 2021-06-06 15:45:04 +01:00
Robert Haines a4e51f15fc Use constants instead of literals for some `fstype` calls. 2021-06-06 15:02:49 +01:00
Robert Haines 26b7f98c08 Use octal for more obvious definition of file-modes. 2021-06-06 15:02:49 +01:00
Robert Haines 9d8fc05c43 Refactor `get_entry` in `FileSystem::File(::Stat)`.
Rename it to `find_entry` because that is ultimately what is called on
the underlying zip file. Make `FileSystem::File#find_entry` public as it
need to be called from `FileSystem::File::Stat`, so now we can avoid
`__send__`. Neither class is documented anyway, so no harm done there.
2021-06-06 15:02:49 +01:00
Robert Haines 64a162ced4 Refactor `FileSystem::File::Stat.delegate_to_fs_file`.
Now uses `class_exec` instead of `class_eval`.
2021-06-06 15:02:49 +01:00
Robert Haines 99ecf3638f Remove spurious empty line at start of module. 2021-06-06 15:02:49 +01:00
Robert Haines 7b2e9c7970 Extract `FileSystem::File::Stat` from `FileSystem::File`. 2021-06-06 15:02:49 +01:00
Robert Haines d1329299c3 Extract `FileSystem::File` from the main filesystem file. 2021-06-06 15:02:49 +01:00
Robert Haines a1c9b63e61 Extract `FileSystem::Dir` from the main filesystem file. 2021-06-06 15:02:49 +01:00
Robert Haines 239baef845 Extract `DirectoryIterator` from the main filesystem file. 2021-06-06 15:02:49 +01:00
Robert Haines 204d084fdf Extract `ZipFileNameMapper` from the main filesystem file. 2021-06-06 15:02:49 +01:00
Robert Haines 64c54cc61b Update Changelog. 2021-06-06 14:56:29 +01:00
Jan-Joost Spanjers 6ac2cb207d REVERT ME: This disables a test fixed by #486 2021-06-06 14:44:20 +01:00
Jan-Joost Spanjers 8a24bff1b2 Disable recover file permissions test on Windows 2021-06-06 14:44:20 +01:00
Jan-Joost Spanjers c3443c06ea Make recover file permissions test better understandable 2021-06-06 14:44:20 +01:00
Jan-Joost Spanjers cdef4a5187 Prevent directory not empty error when running file_test on Windows
Fixed error:

ZipFileTest#test_open_buffer_no_op_does_not_change_file:
Errno::ENOTEMPTY: Directory not empty @ dir_s_rmdir - D:/a/_temp/d20210605-6612-1yi35sp
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1335:in `rmdir'
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1335:in `block in remove_dir1'
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1349:in `platform_support'
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1334:in `remove_dir1'
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1327:in `remove'
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:689:in `block in remove_entry'
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1384:in `ensure in postorder_traverse'
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:1384:in `postorder_traverse'
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/fileutils.rb:687:in `remove_entry'
    C:/hostedtoolcache/windows/Ruby/2.4.10/x64/lib/ruby/2.4.0/tmpdir.rb:101:in `mktmpdir'
    D:/a/rubyzip/rubyzip/test/file_test.rb:136:in `test_open_buffer_no_op_does_not_change_file'

Rationale:

File#dup does not behave like what you would expect from #dup on Ruby.
File#dup calls dup(2), which has OS dependant behavoir.

On Windows, calling File#dup seems to cause an extra reference
to an open file, which prevents deleting that file later.

With this commit, we leave out the call to File#dup on Windows.
It is not clear to me that removing this call has no undesired
consequences, but all other existing tests still succeed.
2021-06-06 14:44:20 +01:00
Jan-Joost Spanjers 8a5fef8074 Fix FileSystem::ZipFileNameMapper#expand_path on Windows
Fixes regression introduced by 0e4dc676a0.
2021-06-06 14:44:20 +01:00
Jan-Joost Spanjers c7fe2a47cb Do not hardcode /tmp in entry_test 2021-06-06 14:44:20 +01:00
Jan-Joost Spanjers 5c9b850729 Prevent adding a newline to comments for generated test files on Windows 2021-06-06 14:44:20 +01:00
Jan-Joost Spanjers 0051d5bb1f Read/write test files in binay mode (for Windows compatibility) 2021-06-06 14:44:20 +01:00
Jan-Joost Spanjers cf22ff1b92 Preserve eol linefeed for text test data files
This prevents converting lf to crlf on Windows.
2021-06-06 14:44:20 +01:00
Robert Haines b705085b09 `Entry#name_safe?` now allows Windows drive mappings. 2021-06-06 14:44:20 +01:00
Robert Haines 22a54853e6 Reinstate normalising pathname separators to `/`.
But only do it after we have set filename encoding appropriately to
avoid breaking multibyte characters with `\`s in them.

Fixes #324.
2021-06-04 16:22:45 +01:00
Robert Haines 1777a3ff53 Make sure `::Zip.force_entry_names_encoding` is reset.
It was the one option left out of `::Zip.reset!` for some reason.
2021-06-01 22:38:43 +01:00
Robert Haines ca51c7ce9b Remove Travis config as its community offering is closing.
Our GitHub Actions workflows are working well now and integrated with
Coveralls, so Travis is no longer needed anyway.
2021-05-30 11:25:43 +01:00
Ariel Zelivansky f54e3b7f56 Fix improvement & fix NTFS 2021-05-30 10:28:27 +01:00
Ariel Zelivansky 01acd0488a Quick fix to prevent crash when mtime is nil 2021-05-30 10:28:27 +01:00
Robert Haines e70e1d3080 Add `InputStream#size`.
This will enable `InputStream` to be used with external APIs that expect
to be able to query the expected size of data they will receive, such as
S3.

Fixes #451.
2021-05-26 13:35:16 +01:00
Robert Haines dea45613bb Test non-block version of File#get_output_stream. 2021-05-25 22:23:39 +01:00
Robert Haines 922afbf5bb Fix gentestfiles: remove redundant she-bang. 2021-05-25 22:04:03 +01:00
Robert Haines aa3a2cba98 Fix gentestfiles: fix some overly long lines. 2021-05-25 22:01:16 +01:00
Robert Haines 3011c4a705 Fix gentestfiles: remove unused constants. 2021-05-25 22:01:16 +01:00
Robert Haines 667328cc93 Fix gentestfiles: create binary files with bytes other than zero. 2021-05-25 21:57:56 +01:00
Robert Haines 15f3a98ff5 Fix gentestfiles: create ASCII files with wider char range. 2021-05-25 21:57:56 +01:00
Robert Haines c438defe73 Remove stale .cvsignore file. 2021-05-25 21:57:56 +01:00
Robert Haines e15f80718d Remove stale task from the Rakefile. 2021-05-25 21:57:56 +01:00
Robert Haines ce08405c1a Fix (most) Style/MutableConstant cop errors.
The last one, in `ExtraField` needs a sizeable refactor to fix.
2021-05-25 21:50:06 +01:00
Robert Haines ca516df01e Remove stale Style/DocumentDynamicEvalDefinition cop. 2021-05-25 21:24:50 +01:00
Robert Haines 86758175f0 Fix Performance/RegexpMatch cop. 2021-05-25 21:24:50 +01:00
Robert Haines 7af12ca887 Fix Performance/FixedSize cop. 2021-05-25 21:24:50 +01:00
Robert Haines ed21f9cf17 Fix Performance/StringInclude cop. 2021-05-25 21:24:50 +01:00
Robert Haines 530afe5d0c Fix Performance/BlockGivenWithExplicitBlock cop. 2021-05-25 21:24:50 +01:00
Robert Haines 255480f22f Add rubocop-performance. 2021-05-25 21:24:50 +01:00
Robert Haines f6cebc6514 Add rubocop-rake. 2021-05-25 21:24:50 +01:00
Robert Haines 984d86ce4b Configure Style/ModuleFunction cop. 2021-05-25 21:24:50 +01:00
Robert Haines 55ed74c20e Fix/configure Style/AccessorGrouping cop. 2021-05-25 21:24:50 +01:00
Robert Haines 3131e6a4aa Fix/configure Naming/VariableNumber cop. 2021-05-25 21:24:50 +01:00
Robert Haines fe998a5aec Fix Layout/EmptyLinesAroundAttributeAccessor cop. 2021-05-25 21:24:50 +01:00
Robert Haines deabe02798 Fix Layout/FirstArrayElementIndentation cop. 2021-05-25 21:24:50 +01:00
Robert Haines 57fa5013c0 Turn off Lint/EmptyClass cop. 2021-05-25 21:24:50 +01:00
Robert Haines e64132f4fc Fix Style/NegatedIfElseCondition cop. 2021-05-25 21:24:50 +01:00
Robert Haines 2b04cc26fa Fix Style/RedundantFileExtensionInRequire cop. 2021-05-25 21:24:50 +01:00
Robert Haines efa23a84ba Fix Style/RedundantBegin cop. 2021-05-25 21:24:50 +01:00
Robert Haines 1b3f4bb7b8 Fix Style/HashConversion cop. 2021-05-25 21:24:50 +01:00
Robert Haines 8fa35de528 Fix Style/FormatStringToken cop. 2021-05-25 21:24:50 +01:00
Robert Haines e2c16991e5 Fix Style/Dir cop. 2021-05-25 21:24:50 +01:00
Robert Haines deac4fa313 Fix Style/CommentAnnotation cop. 2021-05-25 21:24:50 +01:00
Robert Haines 606b5ffbb2 Fix Lint/EmptyBlock cop. 2021-05-25 21:24:50 +01:00
Robert Haines e10badf68e Fix Style/FrozenStringLiteralComment cop. 2021-05-25 21:24:50 +01:00
Taichi Ishitani 0e4dc676a0 fix frozen string literal error 2021-05-25 21:24:50 +01:00
Robert Haines 6f929b603f Configure Layout/EmptyLineBetweenDefs cop. 2021-05-25 21:24:50 +01:00
Robert Haines 3d33e4a8e0 Update Rubocop version.
Now using as late a version as we can for Ruby 2.4.
2021-05-25 21:24:50 +01:00
Robert Haines f1e73b047e Tidy up dependencies in gemspec. 2021-05-25 21:24:50 +01:00
Robert Haines cb69bd520f Update Changelog. 2021-05-25 19:58:08 +01:00
Robert Haines 3b3b932f2d No longer need to turn on `objectspace` in JRuby. 2021-05-25 19:53:42 +01:00
Benoit Daloze af716bef32 Refactor assert_forwarded so it does not need ObjectSpace._id2ref or eval 2021-05-25 19:24:11 +01:00
Robert Haines f0b50d3c6c Add JRUBY_OPTS=--debug to the CI environment. 2021-05-19 22:23:54 +01:00
Robert Haines db3ce93027 Add coveralls integration to the GitHub CI Action.
Fixes #480.
2021-05-19 21:39:53 +01:00
Robert Haines 3958039497 Name the steps in the linter Action.
To match those of the tests Action.
2021-05-18 23:12:51 +01:00
Robert Haines 43d9984044 Install zip for the Windows CI test Action.
And remove hardcoded paths for zip in the tests.
2021-05-18 23:05:35 +01:00
Robert Haines 8702876e55 Set the default `Entry` time to the file's mtime on Windows.
For some reason this was being skipped on Windows, but not Linux or
MacOS.
2021-05-18 21:59:54 +01:00
Robert Haines df9d39730e Add macos and windows tests to Actions.
Just one run of each for now should be enough. Allow windows tests to
fail for now as our tests are broken there at the moment.
2021-05-18 21:33:42 +01:00
Robert Haines bae056efb4 Optimise the GitHub Actions tests workflow. 2021-05-18 21:12:49 +01:00
Robert Haines 34237efc00 Ensure that `Entry#time=` sets times as `DOSTime` objects.
Fixes #481.
2021-05-18 19:57:03 +01:00
Robert Haines 25795c7b0e Update Changelog with some recent merged PRs. 2021-05-18 19:24:47 +01:00
Robert Haines 2b2e0ee568 Bump version to 3.0.0.
There are breaking changes in the recent PRs that have been merged.
2021-05-18 19:23:56 +01:00
Robert Haines 1f5aa84738 Update maintainer information in the README. 2021-05-17 20:21:24 +01:00
Robert Haines 5c8bb1c4a0 Update CI information in the README. 2021-05-17 20:21:00 +01:00
Robert Haines 4ed35cae94 DosTime#<=> should return `nil` if other is not comparable. 2021-05-17 19:57:16 +01:00
Robert Haines 6e9f2976d1 Add temporary fix for JRuby to workaround Time cmp bug.
Workaround jruby/jruby#6668 until fix is released.

Version 9.2.18.0 is hopefully the version that will fix this, but we can
adjust the version accordingly if not.
2021-05-17 19:55:13 +01:00
Robert Haines 65886ac875 Add GitHub actions badges to the README. 2021-05-17 12:49:49 +01:00
Robert Haines 6536b96458 Turn off fail-fast for the CI tests action. 2021-05-17 12:39:18 +01:00
Robert Haines e24f191222 Add some head versions to CI action and allow errors. 2021-05-16 23:24:50 +01:00
Robert Haines 25ce623d13 Add a GitHub action for CI tests. 2021-05-16 21:36:04 +01:00
Robert Haines fd510e07eb Add a GitHub action for linting. 2021-05-16 19:43:27 +01:00
Oleksandr Simonov 7f3bb29487
Merge pull request #464 from hainesr/remove_dosequals
Replace and deprecate `Zip::DOSTime#dos_equals`.
2021-05-03 10:22:13 +03:00
Oleksandr Simonov ef16746cd3
Merge pull request #473 from hainesr/fix-comp-tests
Fix the compression level tests to compare relative sizes.
2021-05-03 10:21:07 +03:00
Oleksandr Simonov f17335459a
Merge pull request #474 from taichi-ishitani/add_ruby30_to_ci
add Ruby 3.0 to CI
2021-05-03 10:20:31 +03:00
Taichi Ishitani 6b656d3277 add Ruby 3.0 to CI 2021-03-06 00:11:47 +09:00
Robert Haines 9da6be98d8 Fix the compression level tests to be relative.
Made little sense to use hardcoded bytes sizes; the tests end up too
brittle.
2021-02-16 13:21:24 +00:00
Oleksandr Simonov 67339da9d1
Merge pull request #470 from bbuchalter/simplify_assertions_in_basic_zip_file_test
Simplify assertions in basic_zip_file_test
2021-02-14 14:29:21 +02:00
Oleksandr Simonov b6eae4f9d8
Merge pull request #468 from bbuchalter/remove_compare_enumerables
Remove compare_enumerables from test_helper.rb
2021-02-14 14:28:19 +02:00
Oleksandr Simonov bdef2137d9
Merge pull request #448 from hainesr/compression_level
Set compression level on a per-zipfile basis.
2021-02-14 14:26:27 +02:00
Oleksandr Simonov a0345420d8
Merge branch 'master' into compression_level 2021-02-14 14:26:12 +02:00
Oleksandr Simonov e397af3e0d
Merge pull request #447 from jlahtinen/fix_zlib_deflate_buffer_growth
Fix zlib deflate buffer growth
2021-02-14 14:24:36 +02:00
Oleksandr Simonov 9f29d09e02
Merge pull request #462 from hainesr/fix-zis-partial-read
Fix input stream partial read error.
2021-02-14 14:23:13 +02:00
Oleksandr Simonov 410daad1ac
Merge pull request #459 from hainesr/fix-extra-fields
Fix loading extra fields
2021-02-14 14:22:38 +02:00
Oleksandr Simonov 1440947a13
Merge pull request #458 from kenhys/fix-spdx-id
Use correct SPDX license identifier
2021-02-14 14:21:10 +02:00
Brian Buchalter 2c4de67d9f Simplify assertions in basic_zip_file_test
We can get the same strength of assertions with less code. Also, by
using assert_equal instead of assert, we get better feedback when
assertion does not meet expectations.
2021-01-26 07:54:23 -07:00
Brian Buchalter 72cedd7ce4 Remove compare_enumerables from test_helper.rb
This change has several benefits:
* When errors occur, the test provides useful feedback, showing you
  expected vs. actual.
* We no longer need to open and modify the Enumerable module.
* The test is more readable.
2021-01-26 06:19:26 -07:00
Brian Buchalter ab9f546557 Use default ruby behavior for Array.join
Since we are using the default behavior of OUTPUT_FIELD_SEPERATOR
anyway, let's allow Ruby to maange that default for us, so if it should
change in the future, we don't have to change.
2021-01-26 04:43:26 -07:00
Brian Buchalter 68b9ed4cfe Remove OUTPUT_FIELD_SEPARATOR-related test behaviors
Since modifying OUTPUT_FIELD_SEPARATOR is deprecated, there's no need
for us to do this in our test.
2021-01-26 04:41:51 -07:00
Brian Buchalter 0585e4e36b Set OUTPUT_FIELD_SEPARATOR to nil in test
As discussed in https://bugs.ruby-lang.org/issues/14240 and implemented
in 6298ec2875
setting OUTPUT_FIELD_SEPERATOR to a non-nil value is now depricated.
2021-01-26 04:36:13 -07:00
Brian Buchalter adfd15a1c6 Prefer OUTPUT_RECORD_SEPARATOR to $\ 2021-01-26 04:35:21 -07:00
Brian Buchalter 4dc308caed Prefer OUTPUT_FIELD_SEPARATOR to $, 2021-01-26 04:34:45 -07:00
Robert Haines 5a4d1d8b6b Replace and deprecate `Zip::DOSTime#dos_equals`.
Having a specific 'does this instance equal another instance' method is
kind of annoying and breaks a number of things. Most obviously it breaks
comparing to `nil`: `nil.dos_equals(other)` will fail where
`nil == other` does not.

So this commit overrides `<=>` in `Zip::DOSTime` and deprecates
`dos_equals`.
2020-11-28 21:19:58 +00:00
Robert Haines 0a6037b0ad Update Changelog.md. 2020-11-08 17:51:57 +00:00
Robert Haines 2ea805c951 Check `number_of_bytes` before comparison in read.
If an input stream has been read from, and left some data in the
internal buffer, then a subsequent `read`, with no amount of bytes to be
read having been specified, will raise an error when comparing to `nil`.
This fix checks that the number of bytes specified in the `read` is not
`nil` before comparing with the size of the internal buffer.

Fixes #461.
2020-11-08 17:20:53 +00:00
Robert Haines ac89366902 Failing test to catch error on read after readline. 2020-11-08 17:19:49 +00:00
Robert Haines 0235e76bae Test packing the NTFS extra field. 2020-10-03 18:27:20 +01:00
Robert Haines 8bafcbbc4d Remove dead code in extra_field/generic.rb (`==`).
From what I can tell this was erroneously copied out of extra_field.rb
during a refactor. It attempts to compare a non-existent Hash that used
to be inherited before the refactor. If this code had been left within
ExtraField it would make more sense, but as it's not needed there either
let's just remove it.

See 20d79feb99 for the refactor.
2020-10-03 18:27:20 +01:00
Robert Haines a668fd14d2 Test reading an extra field with a bad header ID. 2020-10-03 18:27:20 +01:00
Robert Haines c2b9aa2893 Correctly read extra fields when opening a zip file.
Previously, only the extra fields stored in the central directory were
being read in. In reality it is often the case that the extra field in
the central directory is just a marker, and the full data is in the
local header. So we need to read both in and merge the two into the
final correct extra field. This merging infrastructure was already
implemented in the extra field code but we never actually read the
local extra fields in until now.

Reading the central directory headers and local entry headers seems
rather fragile, so we can't just read one over the other and hope to end
up with a correctly merged set of extra fields because this breaks other
things. So we need to specifically read the local extra field data and
merge just those bits.

This commit also fixes a couple of tests that were 'broken' by us now
reading extra fields in correctly!
2020-10-03 18:27:20 +01:00
Kentaro Hayashi a9628ef9d5 Use correct SPDX license identifier
The valid SPDX license is "BSD-2-Clause" instead of "BSD 2-Clause".

ref. https://spdx.org/licenses/
2020-09-25 16:18:33 +09:00
Robert Haines f742994cf2 Abstract out reading extra fields in Entry.
Remove some (almost) duplicated code and get ready for the real fix.
2020-09-20 18:55:39 +01:00
Robert Haines fe1d3c8da0 Fix reading Ux extra field.
As previously implemented the `uid` and `gid` fields could only ever be
read as 0, because they were being initialized to zero and then
memoization (`@uid ||= uid`) was used to 'save' the new value. Using `nil`
as the initial value for either of these fields breaks so many tests, so I
have fixed this by not using memoization instead. This is safe because it
is only the local extra field that holds these values for this type of
extra field.
2020-09-20 18:54:23 +01:00
Robert Haines b00c47a047 Add a failing test for reading local extra fields. 2020-09-20 14:41:24 +01:00
Robert Haines f0714137f6 Update Changelog.md with an entry for this PR. 2020-09-13 17:17:05 +01:00
Robert Haines 834ff70c4d Updated and cleaned-up authors in the README. 2020-09-13 17:13:02 +01:00
Robert Haines 8816279fe0 Update the 'Developing' instructions in the README. 2020-09-13 16:58:06 +01:00
Robert Haines 2ff313da26 Update README instructions for setting compression_level. 2020-09-13 16:45:10 +01:00
Robert Haines f1dd724a3a Use constants for the compression level gp flags. 2020-08-31 17:48:08 +01:00
Robert Haines cf3f4339f6 Make sure that compression method is STORE for level 0.
Whatever the compression method that is set by the user, if the
compression level is set to 0 (no compression), then the entry should be
STORED. This mimics commandline tool behaviour and matches user
expectations.
2020-08-31 17:48:08 +01:00
Robert Haines 5201cd2ea3 Make sure tests that change Zip defaults reset properly. 2020-08-31 17:48:08 +01:00
Robert Haines 0620fba13d Don't use raw numbers for Entry compression types.
Constants for Store and Deflate are already available, so use them. It
might be sensible to remove these local versions, but they do have their
uses as a shortened form.
2020-08-31 17:48:08 +01:00
Robert Haines 156b0f3dee Tidy up accessors in `Entry`.
Remove a load of accessors from the 'public' API by not documenting
them, and remove access to `header_signature` completely.

Also, `Entry#extra` has been created to allow extra fields to be set
correctly after initialization.
2020-08-31 17:48:08 +01:00
Robert Haines e4ceedaa58 Use keyword arguments for the `Entry` initializer.
This greatly simplifies the creation of `Entry` objects when only a
couple of fields are not set to their defaults, while at the same time
allowing an `Entry` to be fully configured at creation time if
appropriate.

This fundamentally changes the `Entry` API and  means that some
convenience methods in `OutputStream` and `File` have needed to be
refactored.
2020-08-31 17:48:08 +01:00
Robert Haines 2775f529b4 Set the compression level general purpose flags. 2020-08-31 17:48:08 +01:00
Robert Haines 072fa27e78 Refactor `Entry#compression_method` access.
As per the conversation here [1], make `compression_method` a method and
enforce the correct type of compression for directories by default.

[1] https://github.com/rubyzip/rubyzip/pull/448#discussion_r436268506
2020-08-31 17:48:08 +01:00
Robert Haines d4bc24dcb3 Clean up `OutputStream` internals.
There was some fairly odd stuff going on in `put_next_entry` that
allowed for data within an `Entry` to be overridden and prevented an
`Entry` from being a single point of truth. Fixing this also simplifies
the code within `File` and still passes all tests.

Also, fixing the above means we can stop passing the compression level
around as a parameter and use the value stored in each `Entry` directly.

Let's keep `compression_level` out of the `Entry` public API though as
it only makes sense when writing an `Entry`: there doesn't seem to be an
obvious way to read what level of compression was used when reading an
`Entry` from a zip file.
2020-08-31 17:48:08 +01:00
Robert Haines 14451e63e7 Add setting a compression level to the File options.
It looks like it needs to be surfaced in `add` and `get_output_stream`.
The compression level defaults to whatever the global default is unless
it is overridden on opening the Zip::File.

Also needed to reorder some of the requires in the top-level module file
now that we are using defaults in the File class.
2020-08-31 17:48:08 +01:00
Robert Haines ef520b4b94 Add `compression_level` to the Entry API.
Allow an Entry to specify a compression level and pass this down to the
underlying OutputStream infrastructure. OutputStream has been able to
specify a compression level for a while but this has, up until now, only
ever been set to the default.

This fundamentally changes the API so will need a major version bump.
2020-08-31 17:48:08 +01:00
Robert Haines 0790695e8c Clean up OutputStream#init_next_entry.
There's no need for this private method to specify a default.
2020-08-31 17:48:08 +01:00
John Lees-Miller e5e3f97ec8
Merge pull request #450 from gogainda/patch-2
enable truffle ruby in Travis CI
2020-08-24 15:34:11 +01:00
Igor Victor e4f6d5ec56
Allow failures for truffleruby-head and truffleruby 2020-08-24 13:19:53 +02:00
Igor Victor cf73152560
enable truffle ruby in Travis CI
@eregon enabling truffle ruby support for rubyzip
2020-08-13 17:37:09 +01:00
Joni Lahtinen 0fee529de5 Use guard instead of if else construct 2020-04-29 17:06:29 +03:00
Joni Lahtinen 99d8f59eaf Change encryption_test less implementation specific
Seems like zlib deflate compress differently when Zlib::SYNC_FLUSH is
used.
2020-04-29 16:41:21 +03:00
Joni Lahtinen 5af76cecb5 Use Zlib::SYNC_FLUSH so buffer does not grow until finished
With JRuby implementation deflate would always return empty string
without Zlib::SYNC_FLUSH. That can cause memory problems when large
files are in deflate buffer as whole and red there with finish call at
once.
2020-04-29 16:38:33 +03:00
John Lees-Miller b653d57635 Update changelog for #445 2020-04-11 10:37:05 +01:00
John Lees-Miller 214f75a4d8
Merge pull request #445 from lucaskanashiro/fix-test-in-s390x
test/file_extract_test.rb: fix test_extract_incorrect_size in s390x arch
2020-04-11 10:33:22 +01:00
Lucas Kanashiro 262ba001a4 test/file_extract_test.rb: fix test_extract_incorrect_size in s390x arch
Using the current pack directives makes test_extract_incorrect_size fail
in s390x architecture because of the endian probably. This is the output
of the command executed by the test in amd64:

irb(main):001:0> [501, 500000, 1].pack('LLS')
=> "\xF5\x01\x00\x00 \xA1\a\x00\x01\x00"

And the output of the same command in s390x:

irb(main):001:0> [501, 500000, 1].pack('LLS')
=> "\x00\x00\x01\xF5\x00\a\xA1 \x00\x01"

Changing to 'VVv' pack directives like is used in
lib/zib/entry.rb fixes the test in s390x and does not add a
regression in amd64.
2020-04-04 11:44:27 -03:00
John Lees-Miller 382cc84915 Update changelog for #444 2020-04-04 10:11:06 +01:00
John Lees-Miller 51a3b21c75
Merge pull request #444 from hainesr/travis-rubocop
Add rubocop to travis CI
2020-04-04 09:54:11 +01:00
Robert Haines 2f311541da Update the auto-config for Rubocop 0.80. 2020-03-15 12:25:46 +00:00
Robert Haines 0c34cc1814 Explicitly set Rubocop ruby version to 2.4. 2020-03-15 12:25:46 +00:00
Robert Haines ee06a8038a Bump Rubocop version and lock it down at patch level.
Now we're using Rubocop in CI we need to prevent CI from dragging in a
newer version than that which we are expecting. This will avoid
unexpected fails from the linting stage.
2020-03-15 12:24:47 +00:00
Robert Haines f37e679e7c Add a rubocop job to the Travis config. 2020-03-14 17:42:36 +00:00
129 changed files with 4195 additions and 2273 deletions

19
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Linter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout rubyzip code
uses: actions/checkout@v4
- name: Install and set up ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: Rubocop
run: bundle exec rubocop

78
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,78 @@
name: Tests
on: [push, pull_request]
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu]
ruby: ['3.0', '3.1', '3.2', '3.3', '3.4', head, jruby, jruby-head, truffleruby, truffleruby-head]
include:
- { os: macos , ruby: '3.0' }
- { os: windows, ruby: '3.0' }
# head builds
- { os: windows, ruby: ucrt }
- { os: windows, ruby: mswin }
runs-on: ${{ matrix.os }}-latest
continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.os == 'windows' }}
steps:
- name: Checkout rubyzip code
uses: actions/checkout@v4
- name: Install and set up ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
rubygems: latest
bundler-cache: true
- name: Run the tests
env:
RUBYOPT: -v
JRUBY_OPTS: --debug
FULL_ZIP64_TEST: 1
run: bundle exec rake
- name: Coveralls
if: matrix.os == 'ubuntu' && !endsWith(matrix.ruby, 'head')
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.github_token }}
flag-name: ${{ matrix.ruby }}
parallel: true
test-yjit:
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos]
ruby: ['3.1', '3.2', '3.3', '3.4', head]
runs-on: ${{ matrix.os }}-latest
continue-on-error: true
steps:
- name: Checkout rubyzip code
uses: actions/checkout@v4
- name: Install and set up ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run the tests
env:
RUBYOPT: --enable-yjit -v
FULL_ZIP64_TEST: 1
run: bundle exec rake
finish:
needs: test
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true

2
.gitignore vendored
View File

@ -4,7 +4,9 @@
Gemfile.lock
+samples/*.zip
+samples/*.zip.*
samples/zipdialogui.rb
coverage
html/
pkg/
.ruby-gemset
.ruby-version

View File

@ -1,9 +1,20 @@
require:
- rubocop-performance
- rubocop-rake
inherit_from: .rubocop_todo.yml
# Set this to the minimum supported ruby in the gemspec. Otherwise
# we get errors if our ruby version doesn't match.
AllCops:
TargetRubyVersion: 2.4
SuggestExtensions: false
TargetRubyVersion: 3.0
NewCops: enable
# Allow this in this file because adding the extra lines is pointless.
Layout/EmptyLineBetweenDefs:
Exclude:
- 'lib/zip/errors.rb'
Layout/HashAlignment:
EnforcedHashRocketStyle: table
@ -12,10 +23,13 @@ Layout/HashAlignment:
# Set a workable line length, given the current state of the code,
# and turn off for the tests.
Layout/LineLength:
Max: 135
Max: 100
Exclude:
- 'test/**/*.rb'
Lint/EmptyClass:
Enabled: false
# In some cases we just need to catch an exception, rather than
# actually handle it. Allow the tests to make use of this shortcut.
Lint/SuppressedException:
@ -24,7 +38,7 @@ Lint/SuppressedException:
- 'test/**/*.rb'
# Allow this "useless" test, as we are testing <=> here.
Lint/UselessComparison:
Lint/BinaryOperatorWithIdenticalOperands:
Exclude:
- 'test/entry_test.rb'
@ -35,9 +49,10 @@ Metrics/AbcSize:
Exclude:
- 'test/**/*.rb'
# Turn block length metrics off for the tests.
# Turn block length metrics off for the tests and gemspec.
Metrics/BlockLength:
Exclude:
- 'rubyzip.gemspec'
- 'test/**/*.rb'
# Turn class length metrics off for the tests.
@ -50,10 +65,31 @@ Metrics/MethodLength:
Exclude:
- 'test/**/*.rb'
# These tests are just better with snake_case numbers.
Naming/VariableNumber:
Exclude:
- 'test/file_permissions_test.rb'
# Need to allow accessors in Entry to be separated for doc purposes.
Style/AccessorGrouping:
Exclude:
- 'lib/zip/entry.rb'
# Set a consistent way of checking types.
Style/ClassCheck:
EnforcedStyle: kind_of?
Style/HashEachMethods:
Enabled: true
Style/HashTransformValues:
Enabled: true
# Allow non-default behaviour for Zip.
Style/ModuleFunction:
Exclude:
- 'lib/zip.rb'
# Allow this multi-line block chain as it actually reads better
# than the alternatives.
Style/MultilineBlockChain:

View File

@ -1,54 +1,61 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2020-02-08 14:58:51 +0000 using RuboCop version 0.79.0.
# on 2021-06-18 14:28:03 UTC using RuboCop version 1.12.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 15
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 580
Gemspec/DevelopmentDependencies:
Enabled: false
# Offense count: 26
# Offense count: 7
Lint/MissingSuper:
Exclude:
- 'lib/zip/extra_field.rb'
- 'lib/zip/extra_field/ntfs.rb'
- 'lib/zip/extra_field/old_unix.rb'
- 'lib/zip/extra_field/universal_time.rb'
- 'lib/zip/extra_field/unix.rb'
- 'lib/zip/extra_field/zip64.rb'
- 'lib/zip/extra_field/zip64_placeholder.rb'
# Offense count: 5
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 650
# Offense count: 21
# Configuration parameters: IgnoredMethods.
Metrics/CyclomaticComplexity:
Max: 14
# Offense count: 120
# Configuration parameters: CountComments, ExcludedMethods.
# Offense count: 47
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
Metrics/MethodLength:
Max: 30
Max: 34
# Offense count: 2
# Offense count: 5
# Configuration parameters: CountKeywordArgs.
Metrics/ParameterLists:
Max: 10
Max: 11
MaxOptionalParameters: 9
# Offense count: 21
# Offense count: 14
# Configuration parameters: IgnoredMethods.
Metrics/PerceivedComplexity:
Max: 15
# Offense count: 9
# Offense count: 7
Naming/AccessorMethodName:
Exclude:
- 'lib/zip/entry.rb'
- 'lib/zip/filesystem.rb'
- 'lib/zip/input_stream.rb'
- 'lib/zip/streamable_stream.rb'
# Offense count: 7
# Configuration parameters: EnforcedStyle.
# SupportedStyles: inline, group
Style/AccessModifierDeclarations:
Exclude:
- 'lib/zip/central_directory.rb'
- 'lib/zip/extra_field/zip64.rb'
- 'lib/zip/filesystem.rb'
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: nested, compact
Style/ClassAndModuleChildren:
Exclude:
@ -57,71 +64,49 @@ Style/ClassAndModuleChildren:
- 'lib/zip/extra_field/old_unix.rb'
- 'lib/zip/extra_field/universal_time.rb'
- 'lib/zip/extra_field/unix.rb'
- 'lib/zip/extra_field/unknown.rb'
- 'lib/zip/extra_field/zip64.rb'
- 'lib/zip/extra_field/zip64_placeholder.rb'
# Offense count: 26
# Offense count: 22
# Configuration parameters: AllowedConstants.
Style/Documentation:
Enabled: false
# Offense count: 3
# Configuration parameters: .
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
EnforcedStyle: unannotated
# Offense count: 95
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, never
Style/FrozenStringLiteralComment:
Enabled: false
# Offense count: 17
# Offense count: 13
# Cop supports --auto-correct.
Style/IfUnlessModifier:
Exclude:
- 'lib/zip/entry.rb'
- 'lib/zip/extra_field/generic.rb'
- 'lib/zip/file.rb'
- 'lib/zip/filesystem.rb'
- 'lib/zip/input_stream.rb'
- 'lib/zip/file_split.rb'
- 'lib/zip/filesystem/dir.rb'
- 'lib/zip/filesystem/file.rb'
- 'lib/zip/pass_thru_decompressor.rb'
- 'lib/zip/streamable_stream.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, Autocorrect.
# SupportedStyles: module_function, extend_self
Style/ModuleFunction:
Exclude:
- 'lib/zip.rb'
# Offense count: 56
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: literals, strict
Style/MutableConstant:
Enabled: false
Exclude:
- 'lib/zip/extra_field.rb'
# Offense count: 23
# Offense count: 21
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
# Configuration parameters: EnforcedStyle, IgnoredMethods.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/zip/entry.rb'
- 'lib/zip/extra_field/old_unix.rb'
- 'lib/zip/extra_field/universal_time.rb'
- 'lib/zip/extra_field/unix.rb'
- 'lib/zip/file.rb'
- 'lib/zip/filesystem.rb'
- 'lib/zip/filesystem/file.rb'
- 'lib/zip/input_stream.rb'
- 'lib/zip/ioextras.rb'
- 'lib/zip/ioextras/abstract_input_stream.rb'
- 'test/file_split_test.rb'
- 'test/test_helper.rb'
# Offense count: 17
# Cop supports --auto-correct.

View File

@ -1,9 +1,22 @@
require 'coveralls'
# frozen_string_literal: true
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
SimpleCov::Formatter::HTMLFormatter,
Coveralls::SimpleCov::Formatter
])
SimpleCov.start do
add_filter '/test'
require 'simplecov-lcov'
SimpleCov::Formatter::LcovFormatter.config do |c|
c.output_directory = 'coverage'
c.lcov_file_name = 'lcov.info'
c.report_with_single_file = true
c.single_report_path = 'coverage/lcov.info'
end
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
[
SimpleCov::Formatter::HTMLFormatter,
SimpleCov::Formatter::LcovFormatter
]
)
SimpleCov.start do
enable_coverage :branch
add_filter ['/test/', '/samples/']
end

View File

@ -1,34 +0,0 @@
language: ruby
dist: xenial
cache: bundler
rvm:
- 2.4
- 2.5
- 2.6
- 2.7
- ruby-head
matrix:
fast_finish: true
include:
- rvm: jruby-9.2
jdk: openjdk8
- rvm: jruby-9.2
jdk: openjdk11
- rvm: jruby-head
jdk: openjdk11
- rvm: rbx-4
allow_failures:
- rvm: ruby-head
- rvm: rbx-4
- rvm: jruby-head
before_install:
- gem --version
before_script:
- echo `whereis zip`
- echo `whereis unzip`
env:
global:
- JRUBY_OPTS="--debug"
- COVERALLS_PARALLEL=true
notifications:
webhooks: https://coveralls.io/webhook

View File

@ -1,4 +1,103 @@
# X.X.X (Next)
# 3.0.0 (Next)
- Fix de facto regression for input streams.
- Fix `File#write_buffer` to always return the given `io`.
- Add `Entry#absolute_time?` and `DOSTime#absolute_time?` methods.
- Use explicit named parameters for `File` methods.
- Ensure that entries can be extracted safely without path traversal. [#540](https://github.com/rubyzip/rubyzip/issues/540)
- Enable Zip64 by default.
- Rename `GPFBit3Error` to `StreamingError`.
- Ensure that `Entry.ftype` is correct via `InputStream`. [#533](https://github.com/rubyzip/rubyzip/issues/533)
- Add `Entry#zip64?` as a better way detect Zip64 entries.
- Implement `Zip::FileSystem::ZipFsFile#symlink?`.
- Remove `File::add_buffer` from the API.
- Fix `OutputStream#put_next_entry` to preserve `StreamableStream`s. [#503](https://github.com/rubyzip/rubyzip/issues/503)
- Ensure `File.open_buffer` doesn't rewrite unchanged data.
- Add `CentralDirectory#count_entries` and `File::count_entries`.
- Fix reading unknown extra fields. [#505](https://github.com/rubyzip/rubyzip/issues/505)
- Fix reading zip files with max length file comment. [#508](https://github.com/rubyzip/rubyzip/issues/508)
- Fix reading zip64 files with max length file comment. [#509](https://github.com/rubyzip/rubyzip/issues/509)
- Don't silently alter zip files opened with `Zip::sort_entries`. [#329](https://github.com/rubyzip/rubyzip/issues/329)
- Use named parameters for optional arguments in the public API.
- Raise an error if entry names exceed 65,535 characters. [#247](https://github.com/rubyzip/rubyzip/issues/247)
- Remove the `ZipXError` v1 legacy classes.
- Raise an error on reading a split archive with `InputStream`. [#349](https://github.com/rubyzip/rubyzip/issues/349)
- Ensure `InputStream` raises `GPFBit3Error` for OSX Archive files. [#493](https://github.com/rubyzip/rubyzip/issues/493)
- Improve documentation and error messages for `InputStream`. [#196](https://github.com/rubyzip/rubyzip/issues/196)
- Fix zip file-level comment is not read from zip64 files. [#492](https://github.com/rubyzip/rubyzip/issues/492)
- Fix `Zip::OutputStream.write_buffer` doesn't work with Tempfiles. [#265](https://github.com/rubyzip/rubyzip/issues/265)
- Reinstate normalising pathname separators to /. [#487](https://github.com/rubyzip/rubyzip/pull/487)
- Fix restore options consistency. [#486](https://github.com/rubyzip/rubyzip/pull/486)
- View and/or preserve original date created, date modified? (Windows). [#336](https://github.com/rubyzip/rubyzip/issues/336)
- Fix frozen string literal error. [#475](https://github.com/rubyzip/rubyzip/pull/475)
- Set the default `Entry` time to the file's mtime on Windows. [#465](https://github.com/rubyzip/rubyzip/issues/465)
- Ensure that `Entry#time=` sets times as `DOSTime` objects. [#481](https://github.com/rubyzip/rubyzip/issues/481)
- Replace and deprecate `Zip::DOSTime#dos_equals`. [#464](https://github.com/rubyzip/rubyzip/pull/464)
- Fix loading extra fields. [#459](https://github.com/rubyzip/rubyzip/pull/459)
- Set compression level on a per-zipfile basis. [#448](https://github.com/rubyzip/rubyzip/pull/448)
- Fix input stream partial read error. [#462](https://github.com/rubyzip/rubyzip/pull/462)
- Fix zlib deflate buffer growth. [#447](https://github.com/rubyzip/rubyzip/pull/447)
Tooling/internal:
- Add a test to ensure correct version number format.
- Update the README with new Ruby version compatability information.
- Fix various issues with JRuby tests.
- Update gem dependency versions.
- Add Ruby 3.4 to the CI.
- Fix mispelled variable names in the crypto classes.
- Only use the Zip64 CDIR end locator if needed.
- Prevent unnecessary Zip64 data being stored.
- Abstract marking various things as 'dirty' into `Dirtyable` for reuse.
- Properly test `File#mkdir`.
- Remove unused private method `File#directory?`.
- Expose the `EntrySet` more cleanly through `CentralDirectory`.
- `Zip::File` no longer subclasses `Zip::CentralDirectory`.
- Configure Coveralls to not report a failure on minor decreases of test coverage. [#491](https://github.com/rubyzip/rubyzip/issues/491)
- Extract the file splitting code out into its own module.
- Refactor, and tidy up, the `Zip::Filesystem` classes for improved maintainability.
- Fix Windows tests. [#489](https://github.com/rubyzip/rubyzip/pull/489)
- Refactor `assert_forwarded` so it does not need `ObjectSpace._id2ref` or `eval`. [#483](https://github.com/rubyzip/rubyzip/pull/483)
- Add GitHub Actions CI infrastructure. [#469](https://github.com/rubyzip/rubyzip/issues/469)
- Add Ruby 3.0 to CI. [#474](https://github.com/rubyzip/rubyzip/pull/474)
- Fix the compression level tests to compare relative sizes. [#473](https://github.com/rubyzip/rubyzip/pull/473)
- Simplify assertions in basic_zip_file_test. [#470](https://github.com/rubyzip/rubyzip/pull/470)
- Remove compare_enumerables from test_helper.rb. [#468](https://github.com/rubyzip/rubyzip/pull/468)
- Use correct SPDX license identifier. [#458](https://github.com/rubyzip/rubyzip/pull/458)
- Enable truffle ruby in Travis CI. [#450](https://github.com/rubyzip/rubyzip/pull/450)
- Update rubocop again and run it in CI. [#444](https://github.com/rubyzip/rubyzip/pull/444)
- Fix a test that was incorrect on big-endian architectures. [#445](https://github.com/rubyzip/rubyzip/pull/445)
# 2.4.1 (2025-01-05)
*This is a re-release of version 2.4 with a full version number string. We need to move to version 2.4.1 due to the canonical version number 2.4 now being taken in Rubygems.*
Tooling:
- Opt-in for MFA requirement explicitly on 2.4 branch.
# 2.4 (2025-01-04) - Yanked
*Yanked due to incorrect version number format (2.4 vs 2.4.0).*
- Ensure compatibility with `--enable-frozen-string-literal`.
- Ensure `File.open_buffer` doesn't rewrite unchanged data. This is a backport of the fix on the 3.x branch.
- Enable use of the version 3 calling style (mainly named parameters) wherever possible, while retaining version 2.x compatibility.
- Add (switchable) warning messages to methods that are changed or removed in version 3.x.
Tooling:
- Switch to using GitHub Actions (from Travis).
- Update Rubocop versions and configuration.
- Update actions with latest rubies.
# 2.3.2 (2021-07-05)
- A "dummy" release to warn about breaking changes coming in version 3.0. This updated version uses the Gem `post_install_message` instead of printing to `STDERR`.
# 2.3.1 (2021-07-03)
- A "dummy" release to warn about breaking changes coming in version 3.0.
# 2.3.0 (2020-03-14)

View File

@ -1,3 +1,12 @@
# frozen_string_literal: true
source 'https://rubygems.org'
gemspec
# TODO: remove when JRuby 9.4.10.0 will be released and available on CI
# Ref: https://github.com/jruby/jruby/issues/7262
if RUBY_PLATFORM.include?('java')
gem 'jar-dependencies', '0.4.1'
gem 'ruby-maven', '3.3.13'
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
guard :minitest do
# with Minitest::Unit
watch(%r{^test/(.*)\/?(.*)_test\.rb$})
watch(%r{^test/(.*)/?(.*)_test\.rb$})
watch(%r{^lib/zip/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
watch(%r{^test/test_helper\.rb$}) { 'test' }
end

24
LICENSE.md Normal file
View File

@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2002-2025, The Rubyzip Developers
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

190
README.md
View File

@ -1,26 +1,32 @@
# rubyzip
[![Gem Version](https://badge.fury.io/rb/rubyzip.svg)](http://badge.fury.io/rb/rubyzip)
[![Build Status](https://secure.travis-ci.org/rubyzip/rubyzip.svg)](http://travis-ci.org/rubyzip/rubyzip)
[![Tests](https://github.com/rubyzip/rubyzip/actions/workflows/tests.yml/badge.svg)](https://github.com/rubyzip/rubyzip/actions/workflows/tests.yml)
[![Linter](https://github.com/rubyzip/rubyzip/actions/workflows/lint.yml/badge.svg)](https://github.com/rubyzip/rubyzip/actions/workflows/lint.yml)
[![Code Climate](https://codeclimate.com/github/rubyzip/rubyzip.svg)](https://codeclimate.com/github/rubyzip/rubyzip)
[![Coverage Status](https://img.shields.io/coveralls/rubyzip/rubyzip.svg)](https://coveralls.io/r/rubyzip/rubyzip?branch=master)
Rubyzip is a ruby library for reading and writing zip files.
## Important note
## Important notes
The Rubyzip interface has changed!!! No need to do `require "zip/zip"` and `Zip` prefix in class names removed.
### Updating to version 3.0
If you have issues with any third-party gems that require an old version of rubyzip, you can use this workaround:
The public API of some classes has been modernized to use named parameters for optional arguments. Please check your usage of the following Rubyzip classes:
* `File`
* `Entry`
* `InputStream`
* `OutputStream`
```ruby
gem 'rubyzip', '>= 1.0.0' # will load new rubyzip version
gem 'zip-zip' # will load compatibility for old rubyzip API.
```
**Please see [Updating to version 3.x](https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x) in the wiki for details.**
## Requirements
- Ruby 2.4 or greater (for rubyzip 2.0; use 1.x for older rubies)
Version 3.x requires at least Ruby 3.0.
Version 2.x requires at least Ruby 2.4, and is known to work on Ruby 3.x.
It is not recommended to use any versions of Rubyzip earlier than 2.3 due to security issues.
## Installation
@ -49,7 +55,7 @@ input_filenames = ['image.jpg', 'description.txt', 'stats.csv']
zipfile_name = "/Users/me/Desktop/archive.zip"
Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
Zip::File.open(zipfile_name, create: true) do |zipfile|
input_filenames.each do |filename|
# Two arguments:
# - The name of the file as it will appear in the archive
@ -88,7 +94,7 @@ class ZipFileGenerator
def write
entries = Dir.entries(@input_dir) - %w[. ..]
::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
::Zip::File.open(@output_file, create: true) do |zipfile|
write_entries entries, '', zipfile
end
end
@ -121,9 +127,9 @@ class ZipFileGenerator
end
```
### Save zip archive entries in sorted by name state
### Save zip archive entries sorted by name
To save zip archives in sorted order like below, you need to set `::Zip.sort_entries` to `true`
To save zip archives with their entries sorted by name (see below), set `::Zip.sort_entries` to `true`
```
Vegetable/
@ -137,7 +143,7 @@ fruit/mango
fruit/orange
```
After this, entries in the zip archive will be saved in ordered state.
Opening an existing zip file with this option set will not change the order of the entries automatically. Altering the zip file - adding an entry, renaming an entry, adding or changing the archive comment, etc - will cause the ordering to be applied when closing the file.
### Default permissions of zip archives
@ -173,28 +179,71 @@ Zip::File.open('foo.zip') do |zip_file|
end
```
#### Notice about ::Zip::InputStream
### Notes on `Zip::InputStream`
`::Zip::InputStream` usable for fast reading zip file content because it not read Central directory.
`Zip::InputStream` can be used for faster reading of zip file content because it does not read the Central directory up front.
But there is one exception when it is not working - General Purpose Flag Bit 3.
There is one exception where it can not work however, and this is if the file does not contain enough information in the local entry headers to extract an entry. This is indicated in an entry by the General Purpose Flag bit 3 being set.
> If bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written. The fields in the local header are filled with zero, and the CRC-32 and size are appended in a 12-byte structure (optionally preceded by a 4-byte signature) immediately after the compressed data
> If bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written. The fields in the local header are filled with zero, and the CRC-32 and size are appended in a 12-byte structure (optionally preceded by a 4-byte signature) immediately after the compressed data.
If `::Zip::InputStream` finds such entry in the zip archive it will raise an exception.
If `Zip::InputStream` finds such an entry in the zip archive it will raise an exception (`Zip::StreamingError`).
`Zip::InputStream` is not designed to be used for random access in a zip file. When performing any operations on an entry that you are accessing via `Zip::InputStream.get_next_entry` then you should complete any such operations before the next call to `get_next_entry`.
```ruby
zip_stream = Zip::InputStream.new(File.open('file.zip'))
while entry = zip_stream.get_next_entry
# All required operations on `entry` go here.
end
```
Any attempt to move about in a zip file opened with `Zip::InputStream` could result in the incorrect entry being accessed and/or Zlib buffer errors. If you need random access in a zip file, use `Zip::File`.
### Password Protection (Experimental)
Rubyzip supports reading/writing zip files with traditional zip encryption (a.k.a. "ZipCrypto"). AES encryption is not yet supported. It can be used with buffer streams, e.g.:
#### Version 2.x
```ruby
Zip::OutputStream.write_buffer(::StringIO.new(''), Zip::TraditionalEncrypter.new('password')) do |out|
out.put_next_entry("my_file.txt")
out.write my_data
end.string
# Writing.
enc = Zip::TraditionalEncrypter.new('password')
buffer = Zip::OutputStream.write_buffer(::StringIO.new(''), enc) do |output|
output.put_next_entry("my_file.txt")
output.write my_data
end
# Reading.
dec = Zip::TraditionalDecrypter.new('password')
Zip::InputStream.open(buffer, 0, dec) do |input|
entry = input.get_next_entry
puts "Contents of '#{entry.name}':"
puts input.read
end
```
This is an experimental feature and the interface for encryption may change in future versions.
#### Version 3.x
```ruby
# Writing.
enc = Zip::TraditionalEncrypter.new('password')
buffer = Zip::OutputStream.write_buffer(encrypter: enc) do |output|
output.put_next_entry("my_file.txt")
output.write my_data
end
# Reading.
dec = Zip::TraditionalDecrypter.new('password')
Zip::InputStream.open(buffer, decrypter: dec) do |input|
entry = input.get_next_entry
puts "Contents of '#{entry.name}':"
puts input.read
end
```
_This is an experimental feature and the interface for encryption may change in future versions._
## Known issues
@ -208,7 +257,7 @@ buffer = Zip::OutputStream.write_buffer do |out|
unless [DOCUMENT_FILE_PATH, RELS_FILE_PATH].include?(e.name)
out.put_next_entry(e.name)
out.write e.get_input_stream.read
end
end
end
out.put_next_entry(DOCUMENT_FILE_PATH)
@ -288,25 +337,37 @@ Zip.validate_entry_sizes = false
Note that if you use the lower level `Zip::InputStream` interface, `rubyzip` does *not* check the entry `size`s. In this case, the caller is responsible for making sure it does not read more data than expected from the input stream.
### Default Compression
### Compression level
You can set the default compression level like so:
When adding entries to a zip archive you can set the compression level to trade-off compressed size against compression speed. By default this is set to the same as the underlying Zlib library's default (`Zlib::DEFAULT_COMPRESSION`), which is somewhere in the middle.
You can configure the default compression level with:
```ruby
Zip.default_compression = Zlib::DEFAULT_COMPRESSION
Zip.default_compression = X
```
It defaults to `Zlib::DEFAULT_COMPRESSION`. Possible values are `Zlib::BEST_COMPRESSION`, `Zlib::DEFAULT_COMPRESSION` and `Zlib::NO_COMPRESSION`
Where X is an integer between 0 and 9, inclusive. If this option is set to 0 (`Zlib::NO_COMPRESSION`) then entries will be stored in the zip archive uncompressed. A value of 1 (`Zlib::BEST_SPEED`) gives the fastest compression and 9 (`Zlib::BEST_COMPRESSION`) gives the smallest compressed file size.
This can also be set for each archive as an option to `Zip::File`:
```ruby
Zip::File.open('foo.zip', create:true, compression_level: 9) do |zip|
zip.add ...
end
```
### Zip64 Support
By default, Zip64 support is disabled for writing. To enable it do this:
Since version 3.0, Zip64 support is enabled for writing by default. To disable it do this:
```ruby
Zip.write_zip64_support = true
Zip.write_zip64_support = false
```
_NOTE_: If you will enable Zip64 writing then you will need zip extractor with Zip64 support to extract archive.
Prior to version 3.0, Zip64 support is disabled for writing by default.
_NOTE_: If Zip64 write support is enabled then any extractor subsequently used may also require Zip64 support to read from the resultant archive.
### Block Form
@ -321,15 +382,50 @@ You can set multiple settings at the same time by using a block:
end
```
## Compatibility
Rubyzip is known to run on a number of platforms and under a number of different Ruby versions.
### Version 2.3.x
Rubyzip 2.3 is known to work on MRI 2.4 to 3.4 on Linux and Mac, and JRuby and Truffleruby on Linux. There are known issues with Windows which have been fixed on the development branch. Please [let us know](https://github.com/rubyzip/rubyzip/pulls) if you know Rubyzip 2.3 works on a platform/Ruby combination not listed here, or [raise an issue](https://github.com/rubyzip/rubyzip/issues) if you see a failure where we think it should work.
### Next (version 3.0.0)
Please see the table below for what we think the current situation is. Note: an empty cell means "unknown", not "does not work".
| OS/Ruby | 3.0 | 3.1 | 3.2 | 3.3 | 3.4 | Head | JRuby 9.4.9.0 | JRuby Head | Truffleruby 24.1.1 | Truffleruby Head |
|---------|-----|-----|-----|-----|-----|------|---------------|------------|--------------------|------------------|
|Ubuntu 22.04| CI | CI | CI | CI | CI | ci | CI | ci | CI | ci |
|Mac OS 14.7.2| CI | CI | CI | CI | CI | ci | x | | x | |
|Windows Server 2022| CI | | | | CI&nbsp;mswin</br>CI&nbsp;ucrt | | | | | |
Key: `CI` - tested in CI, should work; `ci` - tested in CI, might fail; `x` - known working; `o` - known failing.
Rubies 3.1+ are also tested separately with YJIT turned on (Ubuntu and Mac OS).
See [the Actions tab](https://github.com/rubyzip/rubyzip/actions) in GitHub for full details.
Please [raise a PR](https://github.com/rubyzip/rubyzip/pulls) if you know Rubyzip works on a platform/Ruby combination not listed here, or [raise an issue](https://github.com/rubyzip/rubyzip/issues) if you see a failure where we think it should work.
## Developing
To run the test you need to do this:
Install the dependencies:
```
```shell
bundle install
```
Run the tests with `rake`:
```shell
rake
```
Please also run `rubocop` over your changes.
Our CI runs on [GitHub Actions](https://github.com/rubyzip/rubyzip/actions). Please note that `rubocop` is run as part of the CI configuration and will fail a build if errors are found.
## Website and Project Home
http://github.com/rubyzip/rubyzip
@ -338,17 +434,29 @@ http://rdoc.info/github/rubyzip/rubyzip/master/frames
## Authors
Alexander Simonov ( alex at simonov.me)
See https://github.com/rubyzip/rubyzip/graphs/contributors for a comprehensive list.
Alan Harper ( alan at aussiegeek.net)
### Current maintainers
Thomas Sondergaard (thomas at sondergaard.cc)
* Robert Haines (@hainesr)
* John Lees-Miller (@jdleesmiller)
* Oleksandr Simonov (@simonoff)
Technorama Ltd. (oss-ruby-zip at technorama.net)
### Original author
extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org)
* Thomas Sondergaard
## License
Rubyzip is distributed under the same license as ruby. See
http://www.ruby-lang.org/en/LICENSE.txt
Rubyzip is distributed under the same license as Ruby. In practice this means you can use it under the terms of the Ruby License or the 2-Clause BSD License. See https://www.ruby-lang.org/en/about/license.txt and LICENSE.md for details.
## Research notice
Please note that this repository is participating in a study into sustainability
of open source projects. Data will be gathered about this repository for
approximately the next 12 months, starting from June 2021.
Data collected will include number of contributors, number of PRs, time taken to
close/merge these PRs, and issues closed.
For more information, please visit
[our informational page](https://sustainable-open-science-and-software.github.io/) or download our [participant information sheet](https://sustainable-open-science-and-software.github.io/assets/PIS_sustainable_software.pdf).

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
require 'bundler/gem_tasks'
require 'rake/testtask'
require 'rdoc/task'
require 'rubocop/rake_task'
task default: :test
@ -11,11 +14,12 @@ Rake::TestTask.new(:test) do |test|
test.verbose = true
end
RuboCop::RakeTask.new
RDoc::Task.new do |rdoc|
rdoc.main = 'README.md'
rdoc.rdoc_files.include('README.md', 'lib/**/*.rb')
rdoc.options << '--markup=markdown'
rdoc.options << '--tab-width=2'
rdoc.options << "-t Rubyzip version #{Zip::VERSION}"
end
# Rake::TestTask.new(:zip64_full_test) do |test|
# test.libs << File.join(File.dirname(__FILE__), 'lib')
# test.libs << File.join(File.dirname(__FILE__), 'test')
# test.pattern = File.join(File.dirname(__FILE__), 'test/zip64_full_test.rb')
# test.verbose = true
# end
RuboCop::RakeTask.new

15
TODO
View File

@ -1,15 +0,0 @@
* ZipInputStream: Support zip-files with trailing data descriptors
* Adjust rdoc stylesheet to advertise inherited methods if possible
* Suggestion: Add ZipFile/ZipInputStream example that demonstrates extracting all entries.
* Suggestion: ZipFile#extract destination should default to "."
* Suggestion: ZipEntry should have extract(), get_input_stream() methods etc
* (is buffering used anywhere with write?)
* Inflater.sysread should pass the buffer to produce_input.
* Implement ZipFsDir.glob
* ZipFile.checkIntegrity method
* non-MSDOS permission attributes
** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2"
* Packager version, required unpacker version in zip headers
** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2"
* implement storing attributes and ownership information

View File

@ -1,4 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/setup'
require 'zip'

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'English'
require 'delegate'
require 'singleton'
@ -33,6 +35,11 @@ require 'zip/streamable_stream'
require 'zip/streamable_directory'
require 'zip/errors'
# Rubyzip is a ruby module for reading and writing zip files.
#
# The main entry points are File, InputStream and OutputStream. For a
# file/directory interface in the style of the standard ruby ::File and
# ::Dir APIs then `require 'zip/filesystem'` and see FileSystem.
module Zip
extend self
attr_accessor :unicode_names,
@ -46,19 +53,27 @@ module Zip
:force_entry_names_encoding,
:validate_entry_sizes
def reset!
DEFAULT_RESTORE_OPTIONS = {
restore_ownership: false,
restore_permissions: true,
restore_times: true
}.freeze # :nodoc:
def reset! # :nodoc:
@_ran_once = false
@unicode_names = false
@on_exists_proc = false
@continue_on_exists_proc = false
@sort_entries = false
@default_compression = ::Zlib::DEFAULT_COMPRESSION
@write_zip64_support = false
@default_compression = Zlib::DEFAULT_COMPRESSION
@write_zip64_support = true
@warn_invalid_date = true
@case_insensitive_match = false
@force_entry_names_encoding = nil
@validate_entry_sizes = true
end
# Set options for RubyZip in one block.
def setup
yield self unless @_ran_once
@_ran_once = true

View File

@ -1,45 +1,77 @@
# frozen_string_literal: true
require 'forwardable'
require_relative 'dirtyable'
module Zip
class CentralDirectory
include Enumerable
class CentralDirectory # :nodoc:
extend Forwardable
include Dirtyable
END_OF_CD_SIG = 0x06054b50
ZIP64_END_OF_CD_SIG = 0x06064b50
ZIP64_EOCD_LOCATOR_SIG = 0x07064b50
END_OF_CDS = 0x06054b50
ZIP64_END_OF_CDS = 0x06064b50
ZIP64_EOCD_LOCATOR = 0x07064b50
MAX_END_OF_CDS_SIZE = 65_536 + 18
STATIC_EOCD_SIZE = 22
ZIP64_STATIC_EOCD_SIZE = 56
ZIP64_EOCD_LOC_SIZE = 20
MAX_FILE_COMMENT_SIZE = (1 << 16) - 1
MAX_END_OF_CD_SIZE =
MAX_FILE_COMMENT_SIZE + STATIC_EOCD_SIZE + ZIP64_EOCD_LOC_SIZE
attr_reader :comment
attr_accessor :comment
# Returns an Enumerable containing the entries.
def entries
@entry_set.entries
end
def_delegators :@entry_set,
:<<, :delete, :each, :entries, :find_entry, :glob,
:include?, :size
def initialize(entries = EntrySet.new, comment = '') #:nodoc:
super()
mark_dirty :<<, :comment=, :delete
def initialize(entries = EntrySet.new, comment = '') # :nodoc:
super(dirty_on_create: false)
@entry_set = entries.kind_of?(EntrySet) ? entries : EntrySet.new(entries)
@comment = comment
end
def write_to_stream(io) #:nodoc:
def read_from_stream(io)
read_eocds(io)
read_central_directory_entries(io)
end
def write_to_stream(io) # :nodoc:
cdir_offset = io.tell
@entry_set.each { |entry| entry.write_c_dir_entry(io) }
eocd_offset = io.tell
cdir_size = eocd_offset - cdir_offset
if ::Zip.write_zip64_support
need_zip64_eocd = cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF
need_zip64_eocd ||= @entry_set.any? { |entry| entry.extra['Zip64'] }
if need_zip64_eocd
write_64_e_o_c_d(io, cdir_offset, cdir_size)
write_64_eocd_locator(io, eocd_offset)
end
if Zip.write_zip64_support &&
(cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF)
write_64_e_o_c_d(io, cdir_offset, cdir_size)
write_64_eocd_locator(io, eocd_offset)
end
write_e_o_c_d(io, cdir_offset, cdir_size)
end
def write_e_o_c_d(io, offset, cdir_size) #:nodoc:
# Reads the End of Central Directory Record (and the Zip64 equivalent if
# needs be) and returns the number of entries in the archive. This is a
# convenience method that avoids reading in all of the entry data to get a
# very quick entry count.
def count_entries(io)
read_eocds(io)
@size
end
def ==(other) # :nodoc:
return false unless other.kind_of?(CentralDirectory)
@entry_set.entries.sort == other.entries.sort && comment == other.comment
end
private
def write_e_o_c_d(io, offset, cdir_size) # :nodoc:
tmp = [
END_OF_CDS,
END_OF_CD_SIG,
0, # @numberOfThisDisk
0, # @numberOfDiskWithStartOfCDir
@entry_set ? [@entry_set.size, 0xFFFF].min : 0,
@ -52,11 +84,9 @@ module Zip
io << @comment
end
private :write_e_o_c_d
def write_64_e_o_c_d(io, offset, cdir_size) #:nodoc:
def write_64_e_o_c_d(io, offset, cdir_size) # :nodoc:
tmp = [
ZIP64_END_OF_CDS,
ZIP64_END_OF_CD_SIG,
44, # size of zip64 end of central directory record (excludes signature and field itself)
VERSION_MADE_BY,
VERSION_NEEDED_TO_EXTRACT_ZIP64,
@ -70,11 +100,9 @@ module Zip
io << tmp.pack('VQ<vvVVQ<Q<Q<Q<')
end
private :write_64_e_o_c_d
def write_64_eocd_locator(io, zip64_eocd_offset)
tmp = [
ZIP64_EOCD_LOCATOR,
ZIP64_EOCD_LOCATOR_SIG,
0, # number of disk containing the start of zip64 eocd record
zip64_eocd_offset, # offset of the start of zip64 eocd record in its disk
1 # total number of disks
@ -82,127 +110,145 @@ module Zip
io << tmp.pack('VVQ<V')
end
private :write_64_eocd_locator
def unpack_64_e_o_c_d(buffer) # :nodoc:
_, # ZIP64_END_OF_CD_SIG. We know we have this at this point.
@size_of_zip64_e_o_c_d,
@version_made_by,
@version_needed_for_extract,
@number_of_this_disk,
@number_of_disk_with_start_of_cdir,
@total_number_of_entries_in_cdir_on_this_disk,
@size,
@size_in_bytes,
@cdir_offset = buffer.unpack('VQ<vvVVQ<Q<Q<Q<')
def read_64_e_o_c_d(buf) #:nodoc:
buf = get_64_e_o_c_d(buf)
@size_of_zip64_e_o_c_d = Entry.read_zip_64_long(buf)
@version_made_by = Entry.read_zip_short(buf)
@version_needed_for_extract = Entry.read_zip_short(buf)
@number_of_this_disk = Entry.read_zip_long(buf)
@number_of_disk_with_start_of_cdir = Entry.read_zip_long(buf)
@total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_64_long(buf)
@size = Entry.read_zip_64_long(buf)
@size_in_bytes = Entry.read_zip_64_long(buf)
@cdir_offset = Entry.read_zip_64_long(buf)
@zip_64_extensible = buf.slice!(0, buf.bytesize)
raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
zip64_extensible_data_size =
@size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
@zip64_extensible_data = if zip64_extensible_data_size.zero?
''
else
buffer.slice(
ZIP64_STATIC_EOCD_SIZE,
zip64_extensible_data_size
)
end
end
def read_e_o_c_d(buf) #:nodoc:
buf = get_e_o_c_d(buf)
@number_of_this_disk = Entry.read_zip_short(buf)
@number_of_disk_with_start_of_cdir = Entry.read_zip_short(buf)
@total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_short(buf)
@size = Entry.read_zip_short(buf)
@size_in_bytes = Entry.read_zip_long(buf)
@cdir_offset = Entry.read_zip_long(buf)
comment_length = Entry.read_zip_short(buf)
@comment = if comment_length.to_i <= 0
buf.slice!(0, buf.size)
else
buf.read(comment_length)
end
raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
def unpack_64_eocd_locator(buffer) # :nodoc:
_, # ZIP64_EOCD_LOCATOR_SIG. We know we have this at this point.
_, zip64_eocd_offset, = buffer.unpack('VVQ<V')
zip64_eocd_offset
end
def read_central_directory_entries(io) #:nodoc:
def unpack_e_o_c_d(buffer) # :nodoc:
_, # END_OF_CD_SIG. We know we have this at this point.
num_disk,
num_disk_cdir,
num_cdir_disk,
num_entries,
size_in_bytes,
cdir_offset,
comment_length = buffer.unpack('VvvvvVVv')
@number_of_this_disk = num_disk unless num_disk == 0xFFFF
@number_of_disk_with_start_of_cdir = num_disk_cdir unless num_disk_cdir == 0xFFFF
@total_number_of_entries_in_cdir_on_this_disk = num_cdir_disk unless num_cdir_disk == 0xFFFF
@size = num_entries unless num_entries == 0xFFFF
@size_in_bytes = size_in_bytes unless size_in_bytes == 0xFFFFFFFF
@cdir_offset = cdir_offset unless cdir_offset == 0xFFFFFFFF
@comment = if comment_length.positive?
buffer.slice(STATIC_EOCD_SIZE, comment_length)
else
''
end
end
def read_central_directory_entries(io) # :nodoc:
# `StringIO` doesn't raise `EINVAL` if you seek beyond the current end,
# so we need to catch that *and* query `io#eof?` here.
eof = false
begin
io.seek(@cdir_offset, IO::SEEK_SET)
rescue Errno::EINVAL
raise Error, 'Zip consistency problem while reading central directory entry'
eof = true
end
raise Error, 'Zip consistency problem while reading central directory entry' if eof || io.eof?
@entry_set = EntrySet.new
@size.times do
@entry_set << Entry.read_c_dir_entry(io)
entry = Entry.read_c_dir_entry(io)
next unless entry
offset = if entry.zip64?
entry.extra['Zip64'].relative_header_offset
else
entry.local_header_offset
end
unless offset.nil?
io_save = io.tell
io.seek(offset, IO::SEEK_SET)
entry.read_extra_field(read_local_extra_field(io), local: true)
io.seek(io_save, IO::SEEK_SET)
end
@entry_set << entry
end
end
def read_from_stream(io) #:nodoc:
buf = start_buf(io)
if zip64_file?(buf)
read_64_e_o_c_d(buf)
else
read_e_o_c_d(buf)
end
read_central_directory_entries(io)
def read_local_extra_field(io)
buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
return '' unless buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
head, _, _, _, _, _, _, _, _, _, n_len, e_len = buf.unpack('VCCvvvvVVVvv')
return '' unless head == ::Zip::LOCAL_ENTRY_SIGNATURE
io.seek(n_len, IO::SEEK_CUR) # Skip over the entry name.
io.read(e_len)
end
def get_e_o_c_d(buf) #:nodoc:
sig_index = buf.rindex([END_OF_CDS].pack('V'))
raise Error, 'Zip end of central directory signature not found' unless sig_index
def read_eocds(io) # :nodoc:
base_location, data = eocd_data(io)
buf = buf.slice!((sig_index + 4)..(buf.bytesize))
eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
raise Error, 'Zip end of central directory signature not found' unless eocd_location
def buf.read(count)
slice!(0, count)
zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'))
if zip64_eocd_locator
zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'))
zip64_eocd_data =
if zip64_eocd_location
data.slice(zip64_eocd_location..zip64_eocd_locator)
else
zip64_eocd_location = unpack_64_eocd_locator(
data.slice(zip64_eocd_locator..eocd_location)
)
unless zip64_eocd_location
raise Error, 'Zip64 end of central directory signature not found'
end
io.seek(zip64_eocd_location, IO::SEEK_SET)
io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
end
unpack_64_e_o_c_d(zip64_eocd_data)
end
buf
unpack_e_o_c_d(data.slice(eocd_location..-1))
end
def zip64_file?(buf)
buf.rindex([ZIP64_END_OF_CDS].pack('V')) && buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
end
def start_buf(io)
def eocd_data(io)
begin
io.seek(-MAX_END_OF_CDS_SIZE, IO::SEEK_END)
io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
rescue Errno::EINVAL
io.seek(0, IO::SEEK_SET)
end
io.read
end
def get_64_e_o_c_d(buf) #:nodoc:
zip_64_start = buf.rindex([ZIP64_END_OF_CDS].pack('V'))
raise Error, 'Zip64 end of central directory signature not found' unless zip_64_start
zip_64_locator = buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
raise Error, 'Zip64 end of central directory signature locator not found' unless zip_64_locator
buf = buf.slice!((zip_64_start + 4)..zip_64_locator)
def buf.read(count)
slice!(0, count)
end
buf
end
# For iterating over the entries.
def each(&a_proc)
@entry_set.each(&a_proc)
end
# Returns the number of entries in the central directory (and
# consequently in the zip archive).
def size
@entry_set.size
end
def self.read_from_stream(io) #:nodoc:
cdir = new
cdir.read_from_stream(io)
cdir
rescue Error
nil
end
def ==(other) #:nodoc:
return false unless other.kind_of?(CentralDirectory)
@entry_set.entries.sort == other.entries.sort && comment == other.comment
[io.tell, io.read]
end
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
class Compressor #:nodoc:all
class Compressor # :nodoc:all
def finish; end
end
end

View File

@ -1,4 +1,8 @@
# frozen_string_literal: true
module Zip
# :stopdoc:
RUNNING_ON_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/i
CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
@ -11,6 +15,8 @@ module Zip
VERSION_NEEDED_TO_EXTRACT = 20
VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45
SPLIT_FILE_SIGNATURE = 0x08074b50
FILE_TYPE_FILE = 0o10
FILE_TYPE_DIR = 0o04
FILE_TYPE_SYMLINK = 0o12
@ -38,27 +44,27 @@ module Zip
FSTYPE_ATHEOS = 30
FSTYPES = {
FSTYPE_FAT => 'FAT'.freeze,
FSTYPE_AMIGA => 'Amiga'.freeze,
FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze,
FSTYPE_UNIX => 'Unix'.freeze,
FSTYPE_VM_CMS => 'VM/CMS'.freeze,
FSTYPE_ATARI => 'Atari ST'.freeze,
FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze,
FSTYPE_MAC => 'Macintosh'.freeze,
FSTYPE_Z_SYSTEM => 'Z-System'.freeze,
FSTYPE_CPM => 'CP/M'.freeze,
FSTYPE_TOPS20 => 'TOPS-20'.freeze,
FSTYPE_NTFS => 'NTFS'.freeze,
FSTYPE_QDOS => 'SMS/QDOS'.freeze,
FSTYPE_ACORN => 'Acorn RISC OS'.freeze,
FSTYPE_VFAT => 'Win32 VFAT'.freeze,
FSTYPE_MVS => 'MVS'.freeze,
FSTYPE_BEOS => 'BeOS'.freeze,
FSTYPE_TANDEM => 'Tandem NSK'.freeze,
FSTYPE_THEOS => 'Theos'.freeze,
FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze,
FSTYPE_ATHEOS => 'AtheOS'.freeze
FSTYPE_FAT => 'FAT',
FSTYPE_AMIGA => 'Amiga',
FSTYPE_VMS => 'VMS (Vax or Alpha AXP)',
FSTYPE_UNIX => 'Unix',
FSTYPE_VM_CMS => 'VM/CMS',
FSTYPE_ATARI => 'Atari ST',
FSTYPE_HPFS => 'OS/2 or NT HPFS',
FSTYPE_MAC => 'Macintosh',
FSTYPE_Z_SYSTEM => 'Z-System',
FSTYPE_CPM => 'CP/M',
FSTYPE_TOPS20 => 'TOPS-20',
FSTYPE_NTFS => 'NTFS',
FSTYPE_QDOS => 'SMS/QDOS',
FSTYPE_ACORN => 'Acorn RISC OS',
FSTYPE_VFAT => 'Win32 VFAT',
FSTYPE_MVS => 'MVS',
FSTYPE_BEOS => 'BeOS',
FSTYPE_TANDEM => 'Tandem NSK',
FSTYPE_THEOS => 'Theos',
FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)',
FSTYPE_ATHEOS => 'AtheOS'
}.freeze
COMPRESSION_METHOD_STORE = 0
@ -112,4 +118,6 @@ module Zip
COMPRESSION_METHOD_PPMD => 'PPMd version I, Rev 1',
COMPRESSION_METHOD_AES => 'AES encryption'
}.freeze
# :startdoc:
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
class DecryptedIo #:nodoc:all
class DecryptedIo # :nodoc:all
CHUNK_SIZE = 32_768
def initialize(io, decrypter)
@ -16,7 +18,7 @@ module Zip
buffer << produce_input
end
outbuf.replace(buffer.slice!(0...(length || output_buffer.bytesize)))
outbuf.replace(buffer.slice!(0...(length || buffer.bytesize)))
end
private

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
module Zip
class Encrypter #:nodoc:all
class Encrypter # :nodoc:all
end
class Decrypter
class Decrypter # :nodoc:all
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
module NullEncryption
module NullEncryption # :nodoc:
def header_bytesize
0
end
@ -9,7 +11,7 @@ module Zip
end
end
class NullEncrypter < Encrypter
class NullEncrypter < Encrypter # :nodoc:
include NullEncryption
def header(_mtime)
@ -20,14 +22,14 @@ module Zip
data
end
def data_descriptor(_crc32, _compressed_size, _uncomprssed_size)
def data_descriptor(_crc32, _compressed_size, _uncompressed_size)
''
end
def reset!; end
end
class NullDecrypter < Decrypter
class NullDecrypter < Decrypter # :nodoc:
include NullEncryption
def decrypt(data)

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
module TraditionalEncryption
module TraditionalEncryption # :nodoc:
def initialize(password)
@password = password
reset_keys!
@ -26,7 +28,7 @@ module Zip
def update_keys(num)
@key0 = ~Zlib.crc32(num, ~@key0)
@key1 = ((@key1 + (@key0 & 0xff)) * 134_775_813 + 1) & 0xffffffff
@key1 = (((@key1 + (@key0 & 0xff)) * 134_775_813) + 1) & 0xffffffff
@key2 = ~Zlib.crc32((@key1 >> 24).chr, ~@key2)
end
@ -36,7 +38,7 @@ module Zip
end
end
class TraditionalEncrypter < Encrypter
class TraditionalEncrypter < Encrypter # :nodoc:
include TraditionalEncryption
def header(mtime)
@ -53,8 +55,8 @@ module Zip
data.unpack('C*').map { |x| encode x }.pack('C*')
end
def data_descriptor(crc32, compressed_size, uncomprssed_size)
[0x08074b50, crc32, compressed_size, uncomprssed_size].pack('VVVV')
def data_descriptor(crc32, compressed_size, uncompressed_size)
[0x08074b50, crc32, compressed_size, uncompressed_size].pack('VVVV')
end
def reset!
@ -70,7 +72,7 @@ module Zip
end
end
class TraditionalDecrypter < Decrypter
class TraditionalDecrypter < Decrypter # :nodoc:
include TraditionalEncryption
def decrypt(data)

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
class Decompressor #:nodoc:all
class Decompressor # :nodoc:all
CHUNK_SIZE = 32_768
def self.decompressor_classes
@ -14,8 +16,7 @@ module Zip
decompressor_classes[compression_method]
end
attr_reader :input_stream
attr_reader :decompressed_size
attr_reader :decompressed_size, :input_stream
def initialize(input_stream, decompressed_size = nil)
super()

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
class Deflater < Compressor #:nodoc:all
class Deflater < Compressor # :nodoc:all
def initialize(output_stream, level = Zip.default_compression, encrypter = NullEncrypter.new)
super()
@output_stream = output_stream
@ -13,16 +15,16 @@ module Zip
val = data.to_s
@crc = Zlib.crc32(val, @crc)
@size += val.bytesize
buffer = @zlib_deflater.deflate(data)
if buffer.empty?
@output_stream
else
@output_stream << @encrypter.encrypt(buffer)
end
buffer = @zlib_deflater.deflate(data, Zlib::SYNC_FLUSH)
return @output_stream if buffer.empty?
@output_stream << @encrypter.encrypt(buffer)
end
def finish
@output_stream << @encrypter.encrypt(@zlib_deflater.finish) until @zlib_deflater.finished?
buffer = @zlib_deflater.finish
@output_stream << @encrypter.encrypt(buffer) unless buffer.empty?
@zlib_deflater.close
end
attr_reader :size, :crc

32
lib/zip/dirtyable.rb Normal file
View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module Zip
module Dirtyable # :nodoc:all
def initialize(dirty_on_create: true)
@dirty = dirty_on_create
end
def dirty?
@dirty
end
module ClassMethods # :nodoc:
def mark_dirty(*symbols) # :nodoc:
# Move the original method and call it after we've set the dirty flag.
symbols.each do |symbol|
orig_name = "orig_#{symbol}"
alias_method orig_name, symbol
define_method(symbol) do |param|
@dirty = true
send(orig_name, param)
end
end
end
end
def self.included(base)
base.extend(ClassMethods)
end
end
end

View File

@ -1,5 +1,9 @@
# frozen_string_literal: true
require 'rubygems'
module Zip
class DOSTime < Time #:nodoc:all
class DOSTime < Time # :nodoc:all
# MS-DOS File Date and Time format as used in Interrupt 21H Function 57H:
# Register CX, the Time:
@ -12,6 +16,14 @@ module Zip
# bits 5-8 month (1-12)
# bits 9-15 year (four digit year minus 1980)
attr_writer :absolute_time # :nodoc:
def absolute_time?
# If absolute time is not set, we can assume it is an absolute time
# because times do have timezone information by default.
@absolute_time.nil? ? true : @absolute_time
end
def to_binary_dos_time
(sec / 2) +
(min << 5) +
@ -24,9 +36,16 @@ module Zip
((year - 1980) << 9)
end
# Dos time is only stored with two seconds accuracy
def dos_equals(other)
to_i / 2 == other.to_i / 2
warn 'Zip::DOSTime#dos_equals is deprecated. Use `==` instead.'
self == other
end
# Dos time is only stored with two seconds accuracy.
def <=>(other)
return unless other.kind_of?(Time)
(to_i / 2) <=> (other.to_i / 2)
end
# Create a DOSTime instance from a vanilla Time instance.
@ -41,9 +60,36 @@ module Zip
day = (0b11111 & bin_dos_date)
month = (0b111100000 & bin_dos_date) >> 5
year = ((0b1111111000000000 & bin_dos_date) >> 9) + 1980
begin
local(year, month, day, hour, minute, second)
time = local(year, month, day, hour, minute, second)
time.absolute_time = false
time
end
if defined? JRUBY_VERSION && Gem::Version.new(JRUBY_VERSION) < '9.2.18.0'
module JRubyCMP # :nodoc:
def ==(other)
(self <=> other).zero?
end
def <(other)
(self <=> other).negative?
end
def <=(other)
(self <=> other) <= 0
end
def >(other)
(self <=> other).positive?
end
def >=(other)
(self <=> other) >= 0
end
end
include JRubyCMP
end
end
end

View File

@ -1,21 +1,44 @@
# frozen_string_literal: true
require 'pathname'
require_relative 'dirtyable'
module Zip
# Zip::Entry represents an entry in a Zip archive.
class Entry
STORED = 0
DEFLATED = 8
include Dirtyable
# Constant used to specify that the entry is stored (i.e., not compressed).
STORED = ::Zip::COMPRESSION_METHOD_STORE
# Constant used to specify that the entry is deflated (i.e., compressed).
DEFLATED = ::Zip::COMPRESSION_METHOD_DEFLATE
# Language encoding flag (EFS) bit
EFS = 0b100000000000
EFS = 0b100000000000 # :nodoc:
attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
:name, :size, :local_header_offset, :zipfile, :fstype, :external_file_attributes,
:internal_file_attributes,
:gp_flags, :header_signature, :follow_symlinks,
:restore_times, :restore_permissions, :restore_ownership,
:unix_uid, :unix_gid, :unix_perms,
:dirty
attr_reader :ftype, :filepath # :nodoc:
# Compression level flags (used as part of the gp flags).
COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110 # :nodoc:
COMPRESSION_LEVEL_FAST_GPFLAG = 0b100 # :nodoc:
COMPRESSION_LEVEL_MAX_GPFLAG = 0b010 # :nodoc:
def set_default_vars_values
attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
:restore_ownership, :restore_permissions, :restore_times,
:unix_gid, :unix_perms, :unix_uid
attr_accessor :crc, :external_file_attributes, :fstype, :gp_flags,
:internal_file_attributes, :local_header_offset # :nodoc:
attr_reader :extra, :compression_level, :filepath # :nodoc:
attr_writer :size # :nodoc:
mark_dirty :comment=, :compressed_size=, :external_file_attributes=,
:fstype=, :gp_flags=, :name=, :size=,
:unix_gid=, :unix_perms=, :unix_uid=
def set_default_vars_values # :nodoc:
@local_header_offset = 0
@local_header_size = nil # not known until local entry is created or read
@internal_file_attributes = 1
@ -34,178 +57,252 @@ module Zip
end
@follow_symlinks = false
@restore_times = false
@restore_permissions = false
@restore_ownership = false
@restore_times = DEFAULT_RESTORE_OPTIONS[:restore_times]
@restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
@restore_ownership = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
# BUG: need an extra field to support uid/gid's
@unix_uid = nil
@unix_gid = nil
@unix_perms = nil
# @posix_acl = nil
# @ntfs_acl = nil
@dirty = false
end
def check_name(name)
return unless name.start_with?('/')
raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
def check_name(name) # :nodoc:
raise EntryNameError, name if name.start_with?('/')
raise EntryNameError if name.length > 65_535
end
def initialize(*args)
name = args[1] || ''
check_name(name)
# Create a new Zip::Entry.
def initialize(
zipfile = '', name = '',
comment: '', size: nil, compressed_size: 0, crc: 0,
compression_method: DEFLATED,
compression_level: ::Zip.default_compression,
time: ::Zip::DOSTime.now, extra: ::Zip::ExtraField.new
)
super()
@name = name
check_name(@name)
set_default_vars_values
@fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
@zipfile = args[0] || ''
@name = name
@comment = args[2] || ''
@extra = args[3] || ''
@compressed_size = args[4] || 0
@crc = args[5] || 0
@compression_method = args[6] || ::Zip::Entry::DEFLATED
@size = args[7] || 0
@time = args[8] || ::Zip::DOSTime.now
@zipfile = zipfile
@comment = comment || ''
@compression_method = compression_method || DEFLATED
@compression_level = compression_level || ::Zip.default_compression
@compressed_size = compressed_size || 0
@crc = crc || 0
@size = size
@time = case time
when ::Zip::DOSTime
time
when Time
::Zip::DOSTime.from_time(time)
else
::Zip::DOSTime.now
end
@extra =
extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
@ftype = name_is_directory? ? :directory : :file
@extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.kind_of?(::Zip::ExtraField)
set_compression_level_flags
end
# Is this entry encrypted?
def encrypted?
gp_flags & 1 == 1
end
def incomplete?
gp_flags & 8 == 8
def incomplete? # :nodoc:
(gp_flags & 8 == 8) && (crc == 0 || size == 0 || compressed_size == 0)
end
def time
if @extra['UniversalTime']
@extra['UniversalTime'].mtime
elsif @extra['NTFS']
@extra['NTFS'].mtime
else
# Standard time field in central directory has local time
# under archive creator. Then, we can't get timezone.
@time
end
# The uncompressed size of the entry.
def size
@size || 0
end
# Get a timestamp component of this entry.
#
# Returns modification time by default.
def time(component: :mtime)
time =
if @extra['UniversalTime']
@extra['UniversalTime'].send(component)
elsif @extra['NTFS']
@extra['NTFS'].send(component)
end
# Standard time field in central directory has local time
# under archive creator. Then, we can't get timezone.
time || (@time if component == :mtime)
end
alias mtime time
def time=(value)
# Get the last access time of this entry, if available.
def atime
time(component: :atime)
end
# Get the creation time of this entry, if available.
def ctime
time(component: :ctime)
end
# Set a timestamp component of this entry.
#
# Sets modification time by default.
def time=(value, component: :mtime)
@dirty = true
unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
@extra.create('UniversalTime')
end
(@extra['UniversalTime'] || @extra['NTFS']).mtime = value
@time = value
value = DOSTime.from_time(value)
comp = "#{component}=" unless component.to_s.end_with?('=')
(@extra['UniversalTime'] || @extra['NTFS']).send(comp, value)
@time = value if component == :mtime
end
def file_type_is?(type)
raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
alias mtime= time=
@ftype == type
# Set the last access time of this entry.
def atime=(value)
send(:time=, value, component: :atime)
end
# Set the creation time of this entry.
def ctime=(value)
send(:time=, value, component: :ctime)
end
# Does this entry return time fields with accurate timezone information?
def absolute_time?
@extra.member?('UniversalTime') || @extra.member?('NTFS')
end
# Return the compression method for this entry.
#
# Returns STORED if the entry is a directory or if the compression
# level is 0.
def compression_method
return STORED if ftype == :directory || @compression_level == 0
@compression_method
end
# Set the compression method for this entry.
def compression_method=(method)
@dirty = true
@compression_method = (ftype == :directory ? STORED : method)
end
# Does this entry use the ZIP64 extensions?
def zip64?
!@extra['Zip64'].nil?
end
def file_type_is?(type) # :nodoc:
ftype == type
end
def ftype # :nodoc:
@ftype ||= name_is_directory? ? :directory : :file
end
# Dynamic checkers
%w[directory file symlink].each do |k|
define_method "#{k}?" do
define_method :"#{k}?" do
file_type_is?(k.to_sym)
end
end
def name_is_directory? #:nodoc:all
def name_is_directory? # :nodoc:
@name.end_with?('/')
end
# Is the name a relative path, free of `..` patterns that could lead to
# path traversal attacks? This does NOT handle symlinks; if the path
# contains symlinks, this check is NOT enough to guarantee safety.
def name_safe?
def name_safe? # :nodoc:
cleanpath = Pathname.new(@name).cleanpath
return false unless cleanpath.relative?
root = ::File::SEPARATOR
naive_expanded_path = ::File.join(root, cleanpath.to_s)
::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
naive = ::File.join(root, cleanpath.to_s)
# Allow for Windows drive mappings at the root.
::File.absolute_path(cleanpath.to_s, root).match?(/([A-Z]:)?#{naive}/i)
end
def local_entry_offset #:nodoc:all
def local_entry_offset # :nodoc:
local_header_offset + @local_header_size
end
def name_size
def name_size # :nodoc:
@name ? @name.bytesize : 0
end
def extra_size
def extra_size # :nodoc:
@extra ? @extra.local_size : 0
end
def comment_size
def comment_size # :nodoc:
@comment ? @comment.bytesize : 0
end
def calculate_local_header_size #:nodoc:all
def calculate_local_header_size # :nodoc:
LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
end
# check before rewriting an entry (after file sizes are known)
# that we didn't change the header size (and thus clobber file data or something)
def verify_local_header_size!
def verify_local_header_size! # :nodoc:
return if @local_header_size.nil?
new_size = calculate_local_header_size
raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
return unless @local_header_size != new_size
raise Error,
"Local header size changed (#{@local_header_size} -> #{new_size})"
end
def cdir_header_size #:nodoc:all
def cdir_header_size # :nodoc:
CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
(@extra ? @extra.c_dir_size : 0) + comment_size
end
def next_header_offset #:nodoc:all
local_entry_offset + compressed_size + data_descriptor_size
def next_header_offset # :nodoc:
local_entry_offset + compressed_size
end
# Extracts entry to file dest_path (defaults to @name).
# NB: The caller is responsible for making sure dest_path is safe, if it
# is passed.
def extract(dest_path = nil, &block)
if dest_path.nil? && !name_safe?
warn "WARNING: skipped '#{@name}' as unsafe."
# Extracts this entry to a file at `entry_path`, with
# `destination_directory` as the base location in the filesystem.
#
# NB: The caller is responsible for making sure `destination_directory` is
# safe, if it is passed.
def extract(entry_path = @name, destination_directory: '.', &block)
dest_dir = ::File.absolute_path(destination_directory || '.')
extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
unless extract_path.start_with?(dest_dir)
warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
return self
end
dest_path ||= @name
block ||= proc { ::Zip.on_exists_proc }
raise "unknown file type #{inspect}" unless directory? || file? || symlink?
__send__("create_#{@ftype}", dest_path, &block)
__send__(:"create_#{ftype}", extract_path, &block)
self
end
def to_s
def to_s # :nodoc:
@name
end
class << self
def read_zip_short(io) # :nodoc:
io.read(2).unpack1('v')
end
def read_zip_long(io) # :nodoc:
io.read(4).unpack1('V')
end
def read_zip_64_long(io) # :nodoc:
io.read(8).unpack1('Q<')
end
def read_c_dir_entry(io) #:nodoc:all
def read_c_dir_entry(io) # :nodoc:
path = if io.respond_to?(:path)
io.path
else
@ -218,16 +315,18 @@ module Zip
nil
end
def read_local_entry(io)
def read_local_entry(io) # :nodoc:
entry = new(io)
entry.read_local_entry(io)
entry
rescue SplitArchiveError
raise
rescue Error
nil
end
end
def unpack_local_entry(buf)
def unpack_local_entry(buf) # :nodoc:
@header_signature,
@version,
@fstype,
@ -242,62 +341,66 @@ module Zip
@extra_length = buf.unpack('VCCvvvvVVVvv')
end
def read_local_entry(io) #:nodoc:all
@local_header_offset = io.tell
def read_local_entry(io) # :nodoc:
@dirty = false # No changes at this point.
current_offset = io.tell
static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
read_local_header_fields(io)
unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
raise Error, 'Premature end of file. Not enough data for zip entry local header'
if @header_signature == SPLIT_FILE_SIGNATURE
raise SplitArchiveError if current_offset.zero?
# Rewind, skipping the data descriptor, then try to read the local header again.
current_offset += 16
io.seek(current_offset)
read_local_header_fields(io)
end
unpack_local_entry(static_sized_fields_buf)
unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
unless @header_signature == LOCAL_ENTRY_SIGNATURE
raise Error, "Zip local header magic not found at location '#{current_offset}'"
end
@local_header_offset = current_offset
set_time(@last_mod_date, @last_mod_time)
@name = io.read(@name_length)
extra = io.read(@extra_length)
@name.tr!('\\', '/')
if ::Zip.force_entry_names_encoding
@name.force_encoding(::Zip.force_entry_names_encoding)
end
@name.tr!('\\', '/') # Normalise filepath separators after encoding set.
# We need to do this here because `initialize` has so many side-effects.
# :-(
@ftype = name_is_directory? ? :directory : :file
extra = io.read(@extra_length)
if extra && extra.bytesize != @extra_length
raise ::Zip::Error, 'Truncated local zip entry header'
end
if @extra.kind_of?(::Zip::ExtraField)
@extra.merge(extra) if extra
else
@extra = ::Zip::ExtraField.new(extra)
end
read_extra_field(extra, local: true)
parse_zip64_extra(true)
@local_header_size = calculate_local_header_size
end
def pack_local_entry
def pack_local_entry # :nodoc:
zip64 = @extra['Zip64']
[::Zip::LOCAL_ENTRY_SIGNATURE,
@version_needed_to_extract, # version needed to extract
@gp_flags, # @gp_flags
@compression_method,
compression_method,
@time.to_binary_dos_time, # @last_mod_time
@time.to_binary_dos_date, # @last_mod_date
@crc,
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
name_size,
@extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
end
def write_local_entry(io, rewrite = false) #:nodoc:all
prep_zip64_extra(true)
def write_local_entry(io, rewrite: false) # :nodoc:
prep_local_zip64_extra
verify_local_header_size! if rewrite
@local_header_offset = io.tell
@ -308,7 +411,7 @@ module Zip
@local_header_size = io.tell - @local_header_offset
end
def unpack_c_dir_entry(buf)
def unpack_c_dir_entry(buf) # :nodoc:
@header_signature,
@version, # version of encoding software
@fstype, # filesystem type
@ -332,7 +435,7 @@ module Zip
@comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
end
def set_ftype_from_c_dir_entry
def set_ftype_from_c_dir_entry # :nodoc:
@ftype = case @fstype
when ::Zip::FSTYPE_UNIX
@unix_perms = (@external_file_attributes >> 16) & 0o7777
@ -344,8 +447,9 @@ module Zip
when ::Zip::FILE_TYPE_SYMLINK
:symlink
else
# best case guess for whether it is a file or not
# Otherwise this would be set to unknown and that entry would never be able to extracted
# Best case guess for whether it is a file or not.
# Otherwise this would be set to unknown and that
# entry would never be able to be extracted.
if name_is_directory?
:directory
else
@ -361,43 +465,47 @@ module Zip
end
end
def check_c_dir_entry_static_header_length(buf)
return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
def check_c_dir_entry_static_header_length(buf) # :nodoc:
return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
end
def check_c_dir_entry_signature
return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
def check_c_dir_entry_signature # :nodoc:
return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
end
def check_c_dir_entry_comment_size
def check_c_dir_entry_comment_size # :nodoc:
return if @comment && @comment.bytesize == @comment_length
raise ::Zip::Error, 'Truncated cdir zip entry header'
end
def read_c_dir_extra_field(io)
def read_extra_field(buf, local: false) # :nodoc:
if @extra.kind_of?(::Zip::ExtraField)
@extra.merge(io.read(@extra_length))
@extra.merge(buf, local: local) if buf
else
@extra = ::Zip::ExtraField.new(io.read(@extra_length))
@extra = ::Zip::ExtraField.new(buf, local: local)
end
end
def read_c_dir_entry(io) #:nodoc:all
def read_c_dir_entry(io) # :nodoc:
@dirty = false # No changes at this point.
static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
check_c_dir_entry_static_header_length(static_sized_fields_buf)
unpack_c_dir_entry(static_sized_fields_buf)
check_c_dir_entry_signature
set_time(@last_mod_date, @last_mod_time)
@name = io.read(@name_length)
if ::Zip.force_entry_names_encoding
@name.force_encoding(::Zip.force_entry_names_encoding)
end
read_c_dir_extra_field(io)
@name.tr!('\\', '/') # Normalise filepath separators after encoding set.
read_extra_field(io.read(@extra_length))
@comment = io.read(@comment_length)
check_c_dir_entry_comment_size
set_ftype_from_c_dir_entry
@ -413,27 +521,27 @@ module Zip
end
def get_extra_attributes_from_path(path) # :nodoc:
return if Zip::RUNNING_ON_WINDOWS
stat = file_stat(path)
@time = DOSTime.from_time(stat.mtime)
return if ::Zip::RUNNING_ON_WINDOWS
stat = file_stat(path)
@unix_uid = stat.uid
@unix_gid = stat.gid
@unix_perms = stat.mode & 0o7777
@time = ::Zip::DOSTime.from_time(stat.mtime)
end
def set_unix_attributes_on_path(dest_path)
# ignore setuid/setgid bits by default. honor if @restore_ownership
unix_perms_mask = 0o1777
unix_perms_mask = 0o7777 if @restore_ownership
::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
# Restore the timestamp on a file. This will either have come from the
# original source file that was copied into the archive, or from the
# creation date of the archive if there was no original source file.
::FileUtils.touch(dest_path, mtime: time) if @restore_times
# rubocop:disable Style/GuardClause
def set_unix_attributes_on_path(dest_path) # :nodoc:
# Ignore setuid/setgid bits by default. Honour if @restore_ownership.
unix_perms_mask = (@restore_ownership ? 0o7777 : 0o1777)
if @restore_permissions && @unix_perms
::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path)
end
if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
::FileUtils.chown(@unix_uid, @unix_gid, dest_path)
end
end
# rubocop:enable Style/GuardClause
def set_extra_attributes_on_path(dest_path) # :nodoc:
return unless file? || directory?
@ -442,9 +550,14 @@ module Zip
when ::Zip::FSTYPE_UNIX
set_unix_attributes_on_path(dest_path)
end
# Restore the timestamp on a file. This will either have come from the
# original source file that was copied into the archive, or from the
# creation date of the archive if there was no original source file.
::FileUtils.touch(dest_path, mtime: time) if @restore_times
end
def pack_c_dir_entry
def pack_c_dir_entry # :nodoc:
zip64 = @extra['Zip64']
[
@header_signature,
@ -452,12 +565,12 @@ module Zip
@fstype, # filesystem type
@version_needed_to_extract, # @versionNeededToExtract
@gp_flags, # @gp_flags
@compression_method,
compression_method,
@time.to_binary_dos_time, # @last_mod_time
@time.to_binary_dos_date, # @last_mod_date
@crc,
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
name_size,
@extra ? @extra.c_dir_size : 0,
comment_size,
@ -471,11 +584,12 @@ module Zip
].pack('VCCvvvvvVVVvvvvvVV')
end
def write_c_dir_entry(io) #:nodoc:all
prep_zip64_extra(false)
def write_c_dir_entry(io) # :nodoc:
prep_cdir_zip64_extra
case @fstype
when ::Zip::FSTYPE_UNIX
ft = case @ftype
ft = case ftype
when :file
@unix_perms ||= 0o644
::Zip::FILE_TYPE_FILE
@ -488,7 +602,7 @@ module Zip
end
unless ft.nil?
@external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
@external_file_attributes = ((ft << 12) | (@unix_perms & 0o7777)) << 16
end
end
@ -499,43 +613,42 @@ module Zip
io << @comment
end
def ==(other)
def ==(other) # :nodoc:
return false unless other.class == self.class
# Compares contents of local entry and exposed fields
keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
%w[compression_method crc compressed_size size name extra filepath time].all? do |k|
other.__send__(k.to_sym) == __send__(k.to_sym)
end
keys_equal && time.dos_equals(other.time)
end
def <=>(other)
def <=>(other) # :nodoc:
to_s <=> other.to_s
end
# Returns an IO like object for the given ZipEntry.
# Warning: may behave weird with symlinks.
def get_input_stream(&block)
if @ftype == :directory
yield ::Zip::NullInputStream if block_given?
if ftype == :directory
yield ::Zip::NullInputStream if block
::Zip::NullInputStream
elsif @filepath
case @ftype
case ftype
when :file
::File.open(@filepath, 'rb', &block)
when :symlink
linkpath = ::File.readlink(@filepath)
stringio = ::StringIO.new(linkpath)
yield(stringio) if block_given?
yield(stringio) if block
stringio
else
raise "unknown @file_type #{@ftype}"
raise "unknown @file_type #{ftype}"
end
else
zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
zis.instance_variable_set(:@complete_entry, self)
zis.get_next_entry
if block_given?
if block
begin
yield(zis)
ensure
@ -554,7 +667,7 @@ module Zip
if name_is_directory?
raise ArgumentError,
"entry name '#{newEntry}' indicates directory entry, but " \
"'#{src_path}' is not a directory"
"'#{src_path}' is not a directory"
end
:file
when 'directory'
@ -564,7 +677,7 @@ module Zip
if name_is_directory?
raise ArgumentError,
"entry name '#{newEntry}' indicates directory entry, but " \
"'#{src_path}' is not a directory"
"'#{src_path}' is not a directory"
end
:symlink
else
@ -572,27 +685,30 @@ module Zip
end
@filepath = src_path
@size = stat.size
get_extra_attributes_from_path(@filepath)
end
def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
if @ftype == :directory
zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
def write_to_zip_output_stream(zip_output_stream) # :nodoc:
if ftype == :directory
zip_output_stream.put_next_entry(self)
elsif @filepath
zip_output_stream.put_next_entry(self, nil, nil, compression_method || ::Zip::Entry::DEFLATED)
get_input_stream { |is| ::Zip::IOExtras.copy_stream(zip_output_stream, is) }
zip_output_stream.put_next_entry(self)
get_input_stream do |is|
::Zip::IOExtras.copy_stream(zip_output_stream, is)
end
else
zip_output_stream.copy_raw_entry(self)
end
end
def parent_as_string
def parent_as_string # :nodoc:
entry_name = name.chomp('/')
slash_index = entry_name.rindex('/')
slash_index ? entry_name.slice(0, slash_index + 1) : nil
end
def get_raw_input_stream(&block)
def get_raw_input_stream(&block) # :nodoc:
if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
yield @zipfile
else
@ -600,12 +716,22 @@ module Zip
end
end
def clean_up
# By default, do nothing
def clean_up # :nodoc:
@dirty = false # Any changes are written at this point.
end
private
def read_local_header_fields(io) # :nodoc:
static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
raise Error, 'Premature end of file. Not enough data for zip entry local header'
end
unpack_local_entry(static_sized_fields_buf)
end
def set_time(binary_dos_date, binary_dos_time)
@time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
rescue ArgumentError
@ -614,9 +740,9 @@ module Zip
def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
if ::File.exist?(dest_path) && !yield(self, dest_path)
raise ::Zip::DestinationFileExistsError,
"Destination '#{dest_path}' already exists"
raise ::Zip::DestinationExistsError, dest_path
end
::File.open(dest_path, 'wb') do |os|
get_input_stream do |is|
bytes_written = 0
@ -627,10 +753,10 @@ module Zip
bytes_written += buf.bytesize
next unless bytes_written > size && !warned
message = "entry '#{name}' should be #{size}B, but is larger when inflated."
raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
error = ::Zip::EntrySizeError.new(self)
raise error if ::Zip.validate_entry_sizes
warn "WARNING: #{message}"
warn "WARNING: #{error.message}"
warned = true
end
end
@ -643,14 +769,11 @@ module Zip
return if ::File.directory?(dest_path)
if ::File.exist?(dest_path)
if block_given? && yield(self, dest_path)
::FileUtils.rm_f dest_path
else
raise ::Zip::DestinationFileExistsError,
"Cannot create directory '#{dest_path}'. " \
'A file already exists with that name'
end
raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
::FileUtils.rm_f dest_path
end
::FileUtils.mkdir_p(dest_path)
set_extra_attributes_on_path(dest_path)
end
@ -664,53 +787,71 @@ module Zip
# apply missing data from the zip64 extra information field, if present
# (required when file sizes exceed 2**32, but can be used for all files)
def parse_zip64_extra(for_local_header) #:nodoc:all
return if @extra['Zip64'].nil?
def parse_zip64_extra(for_local_header) # :nodoc:
return unless zip64?
if for_local_header
@size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
else
@size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(@size, @compressed_size, @local_header_offset)
@size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(
@size, @compressed_size, @local_header_offset
)
end
end
def data_descriptor_size
(@gp_flags & 0x0008) > 0 ? 16 : 0
# For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
# indicate compression level. This seems to be mainly cosmetic but they are
# generally set by other tools - including in docx files. It is these flags
# that are used by commandline tools (and elsewhere) to give an indication
# of how compressed a file is. See the PKWARE APPNOTE for more information:
# https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
#
# It's safe to simply OR these flags here as compression_level is read only.
def set_compression_level_flags
return unless compression_method == DEFLATED
case @compression_level
when 1
@gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
when 2
@gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
when 8, 9
@gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
end
end
# create a zip64 extra information field if we need one
def prep_zip64_extra(for_local_header) #:nodoc:all
# rubocop:disable Style/GuardClause
def prep_local_zip64_extra
return unless ::Zip.write_zip64_support
return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
# Might not know size here, so need ZIP64 just in case.
# If we already have a ZIP64 extra (placeholder) then we must fill it in.
if zip64? || @size.nil? || @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
@version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
zip64 = @extra['Zip64'] || @extra.create('Zip64')
# Local header always includes size and compressed size.
zip64.original_size = @size || 0
zip64.compressed_size = @compressed_size
end
end
def prep_cdir_zip64_extra
return unless ::Zip.write_zip64_support
need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
if need_zip64
if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
@local_header_offset >= 0xFFFFFFFF
@version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
@extra.delete('Zip64Placeholder')
zip64 = @extra.create('Zip64')
if for_local_header
# local header always includes size and compressed size
zip64.original_size = @size
zip64.compressed_size = @compressed_size
else
# central directory entry entries include whichever fields are necessary
zip64.original_size = @size if @size >= 0xFFFFFFFF
zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
end
else
@extra.delete('Zip64')
zip64 = @extra['Zip64'] || @extra.create('Zip64')
# if this is a local header entry, create a placeholder
# so we have room to write a zip64 extra field afterward
# (we won't know if it's needed until the file data is written)
if for_local_header
@extra.create('Zip64Placeholder')
else
@extra.delete('Zip64Placeholder')
end
# Central directory entry entries include whichever fields are necessary.
zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
end
end
# rubocop:enable Style/GuardClause
end
end

View File

@ -1,7 +1,11 @@
# frozen_string_literal: true
module Zip
class EntrySet #:nodoc:all
class EntrySet # :nodoc:all
include Enumerable
attr_accessor :entry_set, :entry_order
attr_reader :entry_set
protected :entry_set
def initialize(an_enumerable = [])
super()
@ -33,10 +37,8 @@ module Zip
entry if @entry_set.delete(to_key(entry))
end
def each
@entry_set = sorted_entries.dup.each do |_, value|
yield(value)
end
def each(&block)
entries.each(&block)
end
def entries
@ -59,18 +61,18 @@ module Zip
end
def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH | ::File::FNM_EXTGLOB)
entries.map do |entry|
entries.filter_map do |entry|
next nil unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
yield(entry) if block_given?
entry
end.compact
end
end
protected
def sorted_entries
::Zip.sort_entries ? Hash[@entry_set.sort] : @entry_set
::Zip.sort_entries ? @entry_set.sort.to_h : @entry_set
end
private

View File

@ -1,19 +1,139 @@
module Zip
class Error < StandardError; end
class EntryExistsError < Error; end
class DestinationFileExistsError < Error; end
class CompressionMethodError < Error; end
class EntryNameError < Error; end
class EntrySizeError < Error; end
class InternalError < Error; end
class GPFBit3Error < Error; end
class DecompressionError < Error; end
# frozen_string_literal: true
# Backwards compatibility with v1 (delete in v2)
ZipError = Error
ZipEntryExistsError = EntryExistsError
ZipDestinationFileExistsError = DestinationFileExistsError
ZipCompressionMethodError = CompressionMethodError
ZipEntryNameError = EntryNameError
ZipInternalError = InternalError
module Zip
# The superclass for all rubyzip error types. Simply rescue this one if
# you don't need to know what sort of error has been raised.
class Error < StandardError; end
# Error raised if an unsupported compression method is used.
class CompressionMethodError < Error
# The compression method that has caused this error.
attr_reader :compression_method
# Create a new CompressionMethodError with the specified incorrect
# compression method.
def initialize(method)
super()
@compression_method = method
end
# The message returned by this error.
def message
"Unsupported compression method: #{COMPRESSION_METHODS[@compression_method]}."
end
end
# Error raised if there is a problem while decompressing an archive entry.
class DecompressionError < Error
# The error from the underlying Zlib library that caused this error.
attr_reader :zlib_error
# Create a new DecompressionError with the specified underlying Zlib
# error.
def initialize(zlib_error)
super()
@zlib_error = zlib_error
end
# The message returned by this error.
def message
"Zlib error ('#{@zlib_error.message}') while inflating."
end
end
# Error raised when trying to extract an archive entry over an
# existing file.
class DestinationExistsError < Error
# Create a new DestinationExistsError with the clashing destination.
def initialize(destination)
super()
@destination = destination
end
# The message returned by this error.
def message
"Cannot create file or directory '#{@destination}'. " \
'A file already exists with that name.'
end
end
# Error raised when trying to add an entry to an archive where the
# entry name already exists.
class EntryExistsError < Error
# Create a new EntryExistsError with the specified source and name.
def initialize(source, name)
super()
@source = source
@name = name
end
# The message returned by this error.
def message
"'#{@source}' failed. Entry #{@name} already exists."
end
end
# Error raised when an entry name is invalid.
class EntryNameError < Error
# Create a new EntryNameError with the specified name.
def initialize(name = nil)
super()
@name = name
end
# The message returned by this error.
def message
if @name.nil?
'Illegal entry name. Names must have fewer than 65,536 characters.'
else
"Illegal entry name '#{@name}'. Names must not start with '/'."
end
end
end
# Error raised if an entry is larger on extraction than it is advertised
# to be.
class EntrySizeError < Error
# The entry that has caused this error.
attr_reader :entry
# Create a new EntrySizeError with the specified entry.
def initialize(entry)
super()
@entry = entry
end
# The message returned by this error.
def message
"Entry '#{@entry.name}' should be #{@entry.size}B, but is larger when inflated."
end
end
# Error raised if a split archive is read. Rubyzip does not support reading
# split archives.
class SplitArchiveError < Error
# The message returned by this error.
def message
'Rubyzip cannot extract from split archives at this time.'
end
end
# Error raised if there is not enough metadata for the entry to be streamed.
class StreamingError < Error
# The entry that has caused this error.
attr_reader :entry
# Create a new StreamingError with the specified entry.
def initialize(entry)
super()
@entry = entry
end
# The message returned by this error.
def message
"The local header of this entry ('#{@entry.name}') does not contain " \
'the correct metadata for `Zip::InputStream` to be able to ' \
'uncompress it. Please use `Zip::File` instead of `Zip::InputStream`.'
end
end
end

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
module Zip
class ExtraField < Hash
class ExtraField < Hash # :nodoc:all
ID_MAP = {}
def initialize(binstr = nil)
merge(binstr) if binstr
def initialize(binstr = nil, local: false)
merge(binstr, local: local) if binstr
end
def extra_field_type_exist(binstr, id, len, index)
@ -16,25 +18,18 @@ module Zip
end
end
def extra_field_type_unknown(binstr, len, index)
create_unknown_item unless self['Unknown']
if !len || len + 4 > binstr[index..-1].bytesize
self['Unknown'] << binstr[index..-1]
def extra_field_type_unknown(binstr, len, index, local)
self['Unknown'] ||= Unknown.new
if !len || len + 4 > binstr[index..].bytesize
self['Unknown'].merge(binstr[index..], local: local)
return
end
self['Unknown'] << binstr[index, len + 4]
self['Unknown'].merge(binstr[index, len + 4], local: local)
end
def create_unknown_item
s = +''
class << s
alias_method :to_c_dir_bin, :to_s
alias_method :to_local_bin, :to_s
end
self['Unknown'] = s
end
def merge(binstr)
def merge(binstr, local: false)
return if binstr.empty?
i = 0
@ -44,8 +39,7 @@ module Zip
if id && ID_MAP.member?(id)
extra_field_type_exist(binstr, id, len, i)
elsif id
create_unknown_item unless self['Unknown']
break unless extra_field_type_unknown(binstr, len, i)
break unless extra_field_type_unknown(binstr, len, i, local)
end
i += len + 4
end
@ -59,8 +53,8 @@ module Zip
self[name] = field_class.new
end
# place Unknown last, so "extra" data that is missing the proper signature/size
# does not prevent known fields from being read back in
# Place Unknown last, so "extra" data that is missing the proper
# signature/size does not prevent known fields from being read back in.
def ordered_values
result = []
each { |k, v| k == 'Unknown' ? result.push(v) : result.unshift(v) }
@ -90,12 +84,12 @@ module Zip
end
end
require 'zip/extra_field/unknown'
require 'zip/extra_field/generic'
require 'zip/extra_field/universal_time'
require 'zip/extra_field/old_unix'
require 'zip/extra_field/unix'
require 'zip/extra_field/zip64'
require 'zip/extra_field/zip64_placeholder'
require 'zip/extra_field/ntfs'
# Copyright (C) 2002, 2003 Thomas Sondergaard

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
class ExtraField::Generic
class ExtraField::Generic # :nodoc:
def self.register_map
return unless const_defined?(:HEADER_ID)
@ -19,26 +21,17 @@ module Zip
return false
end
[binstr[2, 2].unpack1('v'), binstr[4..-1]]
end
def ==(other)
return false if self.class != other.class
each do |k, v|
return false if v != other[k]
end
true
[binstr[2, 2].unpack1('v'), binstr[4..]]
end
def to_local_bin
s = pack_for_local
self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
(self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
end
def to_c_dir_bin
s = pack_for_c_dir
self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
(self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
end
end
end

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
module Zip
# PKWARE NTFS Extra Field (0x000a)
# Only Tag 0x0001 is supported
class ExtraField::NTFS < ExtraField::Generic
class ExtraField::NTFS < ExtraField::Generic # :nodoc:
HEADER_ID = [0x000A].pack('v')
register_map
@ -23,7 +25,7 @@ module Zip
size, content = initial_parse(binstr)
(size && content) || return
content = content[4..-1]
content = content[4..]
tags = parse_tags(content)
tag1 = tags[1]
@ -51,7 +53,7 @@ module Zip
# reserved 0 and tag 1
s = [0, 1].pack('Vv')
tag1 = ''.force_encoding(Encoding::BINARY)
tag1 = (+'').force_encoding(Encoding::BINARY)
if @mtime
tag1 << [to_ntfs_time(@mtime)].pack('Q<')
if @atime
@ -84,7 +86,7 @@ module Zip
end
def from_ntfs_time(ntfs_time)
::Zip::DOSTime.at(ntfs_time / WINDOWS_TICK - SEC_TO_UNIX_EPOCH)
::Zip::DOSTime.at((ntfs_time / WINDOWS_TICK) - SEC_TO_UNIX_EPOCH)
end
def to_ntfs_time(time)

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
module Zip
# Olf Info-ZIP Extra for UNIX uid/gid and file timestampes
class ExtraField::OldUnix < ExtraField::Generic
class ExtraField::OldUnix < ExtraField::Generic # :nodoc:
HEADER_ID = 'UX'
register_map

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
module Zip
# Info-ZIP Additional timestamp field
class ExtraField::UniversalTime < ExtraField::Generic
class ExtraField::UniversalTime < ExtraField::Generic # :nodoc:
HEADER_ID = 'UT'
register_map

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
module Zip
# Info-ZIP Extra for UNIX uid/gid
class ExtraField::IUnix < ExtraField::Generic
class ExtraField::IUnix < ExtraField::Generic # :nodoc:
HEADER_ID = 'Ux'
register_map
@ -20,8 +22,8 @@ module Zip
return if !size || size == 0
uid, gid = content.unpack('vv')
@uid ||= uid
@gid ||= gid # rubocop:disable Naming/MemoizedInstanceVariableName
@uid = uid
@gid = gid
end
def ==(other)

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Zip
# A class to hold unknown extra fields so that they are preserved.
class ExtraField::Unknown # :nodoc:
def initialize
@local_bin = +''
@cdir_bin = +''
end
def merge(binstr, local: false)
return if binstr.empty?
if local
@local_bin << binstr
else
@cdir_bin << binstr
end
end
def to_local_bin
@local_bin
end
def to_c_dir_bin
@cdir_bin
end
def ==(other)
@local_bin == other.to_local_bin && @cdir_bin == other.to_c_dir_bin
end
end
end

View File

@ -1,7 +1,11 @@
# frozen_string_literal: true
module Zip
# Info-ZIP Extra for Zip64 size
class ExtraField::Zip64 < ExtraField::Generic
attr_accessor :original_size, :compressed_size, :relative_header_offset, :disk_start_number
class ExtraField::Zip64 < ExtraField::Generic # :nodoc:
attr_accessor :compressed_size, :disk_start_number,
:original_size, :relative_header_offset
HEADER_ID = ['0100'].pack('H*')
register_map
@ -36,7 +40,9 @@ module Zip
def parse(original_size, compressed_size, relative_header_offset = nil, disk_start_number = nil)
@original_size = extract(8, 'Q<') if original_size == 0xFFFFFFFF
@compressed_size = extract(8, 'Q<') if compressed_size == 0xFFFFFFFF
@relative_header_offset = extract(8, 'Q<') if relative_header_offset && relative_header_offset == 0xFFFFFFFF
if relative_header_offset && relative_header_offset == 0xFFFFFFFF
@relative_header_offset = extract(8, 'Q<')
end
@disk_start_number = extract(4, 'V') if disk_start_number && disk_start_number == 0xFFFF
@content = nil
[@original_size || original_size,
@ -51,7 +57,8 @@ module Zip
private :extract
def pack_for_local
# local header entries must contain original size and compressed size; other fields do not apply
# Local header entries must contain original size and compressed size;
# other fields do not apply.
return '' unless @original_size && @compressed_size
[@original_size, @compressed_size].pack('Q<Q<')
@ -59,7 +66,7 @@ module Zip
def pack_for_c_dir
# central directory entries contain only fields that didn't fit in the main entry part
packed = ''.force_encoding('BINARY')
packed = (+'').force_encoding('BINARY')
packed << [@original_size].pack('Q<') if @original_size
packed << [@compressed_size].pack('Q<') if @compressed_size
packed << [@relative_header_offset].pack('Q<') if @relative_header_offset

View File

@ -1,15 +0,0 @@
module Zip
# placeholder to reserve space for a Zip64 extra information record, for the
# local file header only, that we won't know if we'll need until after
# we write the file data
class ExtraField::Zip64Placeholder < ExtraField::Generic
HEADER_ID = ['9999'].pack('H*') # this ID is used by other libraries such as .NET's Ionic.zip
register_map
def initialize(_binstr = nil); end
def pack_for_local
"\x00" * 16
end
end
end

View File

@ -1,124 +1,109 @@
# frozen_string_literal: true
require 'forwardable'
require_relative 'file_split'
module Zip
# ZipFile is modeled after java.util.zip.ZipFile from the Java SDK.
# The most important methods are those inherited from
# ZipCentralDirectory for accessing information about the entries in
# the archive and methods such as get_input_stream and
# get_output_stream for reading from and writing entries to the
# Zip::File is modeled after java.util.zip.ZipFile from the Java SDK.
# The most important methods are those for accessing information about
# the entries in
# the archive and methods such as `get_input_stream` and
# `get_output_stream` for reading from and writing entries to the
# archive. The class includes a few convenience methods such as
# #extract for extracting entries to the filesystem, and #remove,
# #replace, #rename and #mkdir for making simple modifications to
# `extract` for extracting entries to the filesystem, and `remove`,
# `replace`, `rename` and `mkdir` for making simple modifications to
# the archive.
#
# Modifications to a zip archive are not committed until #commit or
# #close is called. The method #open accepts a block following
# the pattern from File.open offering a simple way to
# Modifications to a zip archive are not committed until `commit` or
# `close` is called. The method `open` accepts a block following
# the pattern from ::File.open offering a simple way to
# automatically close the archive when the block returns.
#
# The following example opens zip archive <code>my.zip</code>
# The following example opens zip archive `my.zip`
# (creating it if it doesn't exist) and adds an entry
# <code>first.txt</code> and a directory entry <code>a_dir</code>
# `first.txt` and a directory entry `a_dir`
# to it.
#
# require 'zip'
# ```
# require 'zip'
#
# Zip::File.open("my.zip", Zip::File::CREATE) {
# |zipfile|
# zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" }
# zipfile.mkdir("a_dir")
# }
# Zip::File.open('my.zip', create: true) do |zipfile|
# zipfile.get_output_stream('first.txt') { |f| f.puts 'Hello from Zip::File' }
# zipfile.mkdir('a_dir')
# end
# ```
#
# The next example reopens <code>my.zip</code> writes the contents of
# <code>first.txt</code> to standard out and deletes the entry from
# The next example reopens `my.zip`, writes the contents of
# `first.txt` to standard out and deletes the entry from
# the archive.
#
# require 'zip'
# ```
# require 'zip'
#
# Zip::File.open("my.zip", Zip::File::CREATE) {
# |zipfile|
# puts zipfile.read("first.txt")
# zipfile.remove("first.txt")
# }
# Zip::File.open('my.zip', create: true) do |zipfile|
# puts zipfile.read('first.txt')
# zipfile.remove('first.txt')
# end
#
# ZipFileSystem offers an alternative API that emulates ruby's
# interface for accessing the filesystem, ie. the File and Dir classes.
# Zip::FileSystem offers an alternative API that emulates ruby's
# interface for accessing the filesystem, ie. the ::File and ::Dir classes.
class File
extend Forwardable
extend FileSplit
class File < CentralDirectory
CREATE = true
SPLIT_SIGNATURE = 0x08074b50
ZIP64_EOCD_SIGNATURE = 0x06064b50
MAX_SEGMENT_SIZE = 3_221_225_472
MIN_SEGMENT_SIZE = 65_536
DATA_BUFFER_SIZE = 8192
IO_METHODS = [:tell, :seek, :read, :eof, :close]
DEFAULT_OPTIONS = {
restore_ownership: false,
restore_permissions: false,
restore_times: false
}.freeze
IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze # :nodoc:
# The name of this zip archive.
attr_reader :name
# default -> false.
attr_accessor :restore_ownership
# default -> false, but will be set to true in a future version.
# default -> true.
attr_accessor :restore_permissions
# default -> false, but will be set to true in a future version.
# default -> true.
attr_accessor :restore_times
# Returns the zip files comment, if it has one
attr_accessor :comment
def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
# Opens a zip archive. Pass true as the second parameter to create
# Opens a zip archive. Pass create: true to create
# a new archive if it doesn't exist already.
def initialize(path_or_io, create = false, buffer = false, options = {})
def initialize(path_or_io, create: false, buffer: false,
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
compression_level: ::Zip.default_compression)
super()
options = DEFAULT_OPTIONS.merge(options)
@name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
@comment = ''
@create = create ? true : false # allow any truthy value to mean true
if ::File.size?(@name.to_s)
# There is a file, which exists, that is associated with this zip.
@create = false
@file_permissions = ::File.stat(@name).mode
initialize_cdir(path_or_io, buffer: buffer)
if buffer
read_from_stream(path_or_io)
else
::File.open(@name, 'rb') do |f|
read_from_stream(f)
end
end
elsif buffer && path_or_io.size > 0
# This zip is probably a non-empty StringIO.
read_from_stream(path_or_io)
elsif @create
# This zip is completely new/empty and is to be created.
@entry_set = EntrySet.new
elsif ::File.zero?(@name)
# A file exists, but it is empty.
raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
else
# Everything is wrong.
raise Error, "File #{@name} not found"
end
@stored_entries = @entry_set.dup
@stored_comment = @comment
@restore_ownership = options[:restore_ownership]
@restore_permissions = options[:restore_permissions]
@restore_times = options[:restore_times]
@restore_ownership = restore_ownership
@restore_permissions = restore_permissions
@restore_times = restore_times
@compression_level = compression_level
end
class << self
# Similar to ::new. If a block is passed the Zip::File object is passed
# to the block and is automatically closed afterwards, just as with
# ruby's builtin File::open method.
def open(file_name, create = false, options = {})
zf = ::Zip::File.new(file_name, create, false, options)
def open(file_name, create: false,
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
compression_level: ::Zip.default_compression)
zf = ::Zip::File.new(file_name, create: create,
restore_ownership: restore_ownership,
restore_permissions: restore_permissions,
restore_times: restore_times,
compression_level: compression_level)
return zf unless block_given?
begin
@ -128,29 +113,29 @@ module Zip
end
end
# Same as #open. But outputs data to a buffer instead of a file
def add_buffer
io = ::StringIO.new('')
zf = ::Zip::File.new(io, true, true)
yield zf
zf.write_buffer(io)
end
# Like #open, but reads zip archive contents from a String or open IO
# stream, and outputs data to a buffer.
# (This can be used to extract data from a
# downloaded zip archive without first saving it to disk.)
def open_buffer(io, options = {})
def open_buffer(io = ::StringIO.new, create: false,
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
compression_level: ::Zip.default_compression)
unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
raise "Zip::File.open_buffer expects a String or IO-like argument (responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
raise 'Zip::File.open_buffer expects a String or IO-like argument' \
"(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
end
io = ::StringIO.new(io) if io.kind_of?(::String)
# https://github.com/rubyzip/rubyzip/issues/119
io.binmode if io.respond_to?(:binmode)
zf = ::Zip::File.new(io, create: create, buffer: true,
restore_ownership: restore_ownership,
restore_permissions: restore_permissions,
restore_times: restore_times,
compression_level: compression_level)
zf = ::Zip::File.new(io, true, true, options)
return zf unless block_given?
yield zf
@ -174,82 +159,19 @@ module Zip
end
end
def get_segment_size_for_split(segment_size)
if MIN_SEGMENT_SIZE > segment_size
MIN_SEGMENT_SIZE
elsif MAX_SEGMENT_SIZE < segment_size
MAX_SEGMENT_SIZE
# Count the entries in a zip archive without reading the whole set of
# entry data into memory.
def count_entries(path_or_io)
cdir = ::Zip::CentralDirectory.new
if path_or_io.kind_of?(String)
::File.open(path_or_io, 'rb') do |f|
cdir.count_entries(f)
end
else
segment_size
cdir.count_entries(path_or_io)
end
end
def get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
unless partial_zip_file_name.nil?
partial_zip_file_name = zip_file_name.sub(/#{::File.basename(zip_file_name)}\z/,
partial_zip_file_name + ::File.extname(zip_file_name))
end
partial_zip_file_name ||= zip_file_name
partial_zip_file_name
end
def get_segment_count_for_split(zip_file_size, segment_size)
(zip_file_size / segment_size).to_i + (zip_file_size % segment_size == 0 ? 0 : 1)
end
def put_split_signature(szip_file, segment_size)
signature_packed = [SPLIT_SIGNATURE].pack('V')
szip_file << signature_packed
segment_size - signature_packed.size
end
#
# TODO: Make the code more understandable
#
def save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
ssegment_size = zip_file_size - zip_file.pos
ssegment_size = segment_size if ssegment_size > segment_size
szip_file_name = "#{partial_zip_file_name}.#{format('%03d', szip_file_index)}"
::File.open(szip_file_name, 'wb') do |szip_file|
if szip_file_index == 1
ssegment_size = put_split_signature(szip_file, segment_size)
end
chunk_bytes = 0
until ssegment_size == chunk_bytes || zip_file.eof?
segment_bytes_left = ssegment_size - chunk_bytes
buffer_size = segment_bytes_left < DATA_BUFFER_SIZE ? segment_bytes_left : DATA_BUFFER_SIZE
chunk = zip_file.read(buffer_size)
chunk_bytes += buffer_size
szip_file << chunk
# Info for track splitting
yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
end
end
end
# Splits an archive into parts with segment size
def split(zip_file_name, segment_size = MAX_SEGMENT_SIZE, delete_zip_file = true, partial_zip_file_name = nil)
raise Error, "File #{zip_file_name} not found" unless ::File.exist?(zip_file_name)
raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
zip_file_size = ::File.size(zip_file_name)
segment_size = get_segment_size_for_split(segment_size)
return if zip_file_size <= segment_size
segment_count = get_segment_count_for_split(zip_file_size, segment_size)
# Checking for correct zip structure
::Zip::File.open(zip_file_name) {}
partial_zip_file_name = get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
szip_file_index = 0
::File.open(zip_file_name, 'rb') do |zip_file|
until zip_file.eof?
szip_file_index += 1
save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
end
end
::File.delete(zip_file_name) if delete_zip_file
szip_file_index
end
end
# Returns an input stream to the specified entry. If a block is passed
@ -264,24 +186,29 @@ module Zip
# specified. If a block is passed the stream object is passed to the block and
# the stream is automatically closed afterwards just as with ruby's builtin
# File.open method.
def get_output_stream(entry, permission_int = nil, comment = nil,
extra = nil, compressed_size = nil, crc = nil,
compression_method = nil, size = nil, time = nil,
&a_proc)
def get_output_stream(entry, permissions: nil, comment: nil,
extra: nil, compressed_size: nil, crc: nil,
compression_method: nil, compression_level: nil,
size: nil, time: nil, &a_proc)
new_entry =
if entry.kind_of?(Entry)
entry
else
Entry.new(@name, entry.to_s, comment, extra, compressed_size, crc, compression_method, size, time)
Entry.new(
@name, entry.to_s, comment: comment, extra: extra,
compressed_size: compressed_size, crc: crc, size: size,
compression_method: compression_method,
compression_level: compression_level, time: time
)
end
if new_entry.directory?
raise ArgumentError,
"cannot open stream to directory entry - '#{new_entry}'"
end
new_entry.unix_perms = permission_int
new_entry.unix_perms = permissions
zip_streamable_entry = StreamableStream.new(new_entry)
@entry_set << zip_streamable_entry
@cdir << zip_streamable_entry
zip_streamable_entry.get_output_stream(&a_proc)
end
@ -299,31 +226,39 @@ module Zip
def add(entry, src_path, &continue_on_exists_proc)
continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
check_entry_exists(entry, continue_on_exists_proc, 'add')
new_entry = entry.kind_of?(::Zip::Entry) ? entry : ::Zip::Entry.new(@name, entry.to_s)
new_entry = if entry.kind_of?(::Zip::Entry)
entry
else
::Zip::Entry.new(
@name, entry.to_s,
compression_level: @compression_level
)
end
new_entry.gather_fileinfo_from_srcpath(src_path)
new_entry.dirty = true
@entry_set << new_entry
@cdir << new_entry
end
# Convenience method for adding the contents of a file to the archive
# in Stored format (uncompressed)
def add_stored(entry, src_path, &continue_on_exists_proc)
entry = ::Zip::Entry.new(@name, entry.to_s, nil, nil, nil, nil, ::Zip::Entry::STORED)
entry = ::Zip::Entry.new(
@name, entry.to_s, compression_method: ::Zip::Entry::STORED
)
add(entry, src_path, &continue_on_exists_proc)
end
# Removes the specified entry.
def remove(entry)
@entry_set.delete(get_entry(entry))
@cdir.delete(get_entry(entry))
end
# Renames the specified entry.
def rename(entry, new_name, &continue_on_exists_proc)
found_entry = get_entry(entry)
check_entry_exists(new_name, continue_on_exists_proc, 'rename')
@entry_set.delete(found_entry)
@cdir.delete(found_entry)
found_entry.name = new_name
@entry_set << found_entry
@cdir << found_entry
end
# Replaces the specified entry with the contents of src_path (from
@ -334,11 +269,16 @@ module Zip
add(entry, src_path)
end
# Extracts entry to file dest_path.
def extract(entry, dest_path, &block)
# Extracts `entry` to a file at `entry_path`, with `destination_directory`
# as the base location in the filesystem.
#
# NB: The caller is responsible for making sure `destination_directory` is
# safe, if it is passed.
def extract(entry, entry_path = nil, destination_directory: '.', &block)
block ||= proc { ::Zip.on_exists_proc }
found_entry = get_entry(entry)
found_entry.extract(dest_path, &block)
entry_path ||= found_entry.name
found_entry.extract(entry_path, destination_directory: destination_directory, &block)
end
# Commits changes that has been made since the previous commit to
@ -348,22 +288,23 @@ module Zip
on_success_replace do |tmp_file|
::Zip::OutputStream.open(tmp_file) do |zos|
@entry_set.each do |e|
@cdir.each do |e|
e.write_to_zip_output_stream(zos)
e.dirty = false
e.clean_up
end
zos.comment = comment
end
true
end
initialize(name)
initialize_cdir(@name)
end
# Write buffer write changes to buffer and return
def write_buffer(io = ::StringIO.new(''))
def write_buffer(io = ::StringIO.new)
return io unless commit_required?
::Zip::OutputStream.write_buffer(io) do |zos|
@entry_set.each { |e| e.write_to_zip_output_stream(zos) }
@cdir.each { |e| e.write_to_zip_output_stream(zos) }
zos.comment = comment
end
end
@ -376,16 +317,19 @@ module Zip
# Returns true if any changes has been made to this archive since
# the previous commit
def commit_required?
@entry_set.each do |e|
return true if e.dirty
return true if @create || @cdir.dirty?
@cdir.each do |e|
return true if e.dirty?
end
@comment != @stored_comment || @entry_set != @stored_entries || @create
false
end
# Searches for entry with the specified name. Returns nil if
# no entry is found. See also get_entry
def find_entry(entry_name)
selected_entry = @entry_set.find_entry(entry_name)
selected_entry = @cdir.find_entry(entry_name)
return if selected_entry.nil?
selected_entry.restore_ownership = @restore_ownership
@ -394,11 +338,6 @@ module Zip
selected_entry
end
# Searches for entries given a glob
def glob(*args, &block)
@entry_set.glob(*args, &block)
end
# Searches for an entry just as find_entry, but throws Errno::ENOENT
# if no entry is found.
def get_entry(entry)
@ -414,33 +353,50 @@ module Zip
entry_name = entry_name.dup.to_s
entry_name << '/' unless entry_name.end_with?('/')
@entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
@cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
end
private
def directory?(new_entry, src_path)
path_is_directory = ::File.directory?(src_path)
if new_entry.directory? && !path_is_directory
raise ArgumentError,
"entry name '#{new_entry}' indicates directory entry, but " \
"'#{src_path}' is not a directory"
elsif !new_entry.directory? && path_is_directory
new_entry.name += '/'
def initialize_cdir(path_or_io, buffer: false)
@cdir = ::Zip::CentralDirectory.new
if ::File.size?(@name.to_s)
# There is a file, which exists, that is associated with this zip.
@create = false
@file_permissions = ::File.stat(@name).mode
if buffer
# https://github.com/rubyzip/rubyzip/issues/119
path_or_io.binmode if path_or_io.respond_to?(:binmode)
@cdir.read_from_stream(path_or_io)
else
::File.open(@name, 'rb') do |f|
@cdir.read_from_stream(f)
end
end
elsif buffer && path_or_io.size > 0
# This zip is probably a non-empty StringIO.
@create = false
@cdir.read_from_stream(path_or_io)
elsif !@create && ::File.empty?(@name)
# A file exists, but it is empty, and we've said we're
# NOT creating a new zip.
raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
elsif !@create
# If we get here, and we're not creating a new zip, then
# everything is wrong.
raise Error, "File #{@name} not found"
end
new_entry.directory? && path_is_directory
end
def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
return unless @entry_set.include?(entry_name)
return unless @cdir.include?(entry_name)
if continue_on_exists_proc.call
remove get_entry(entry_name)
else
raise ::Zip::EntryExistsError,
proc_name + " failed. Entry #{entry_name} already exists"
end
continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
remove get_entry(entry_name)
end
def check_file(path)
@ -450,14 +406,12 @@ module Zip
def on_success_replace
dirname, basename = ::File.split(name)
::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
begin
if yield tmp_filename
::File.rename(tmp_filename, name)
::File.chmod(@file_permissions, name) unless @create
end
ensure
::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
if yield tmp_filename
::File.rename(tmp_filename, name)
::File.chmod(@file_permissions, name) unless @create
end
ensure
::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
end
end
end

91
lib/zip/file_split.rb Normal file
View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
module Zip
module FileSplit # :nodoc:
MAX_SEGMENT_SIZE = 3_221_225_472
MIN_SEGMENT_SIZE = 65_536
DATA_BUFFER_SIZE = 8192
def get_segment_size_for_split(segment_size)
segment_size.clamp(MIN_SEGMENT_SIZE, MAX_SEGMENT_SIZE)
end
def get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
unless partial_zip_file_name.nil?
partial_zip_file_name = zip_file_name.sub(
/#{::File.basename(zip_file_name)}\z/,
partial_zip_file_name + ::File.extname(zip_file_name)
)
end
partial_zip_file_name ||= zip_file_name
partial_zip_file_name
end
def get_segment_count_for_split(zip_file_size, segment_size)
(zip_file_size / segment_size).to_i +
((zip_file_size % segment_size).zero? ? 0 : 1)
end
def put_split_signature(szip_file, segment_size)
signature_packed = [SPLIT_FILE_SIGNATURE].pack('V')
szip_file << signature_packed
segment_size - signature_packed.size
end
#
# TODO: Make the code more understandable
#
def save_splited_part(
zip_file, partial_zip_file_name, zip_file_size,
szip_file_index, segment_size, segment_count
)
ssegment_size = zip_file_size - zip_file.pos
ssegment_size = segment_size if ssegment_size > segment_size
szip_file_name = "#{partial_zip_file_name}.#{format('%03d', szip_file_index)}"
::File.open(szip_file_name, 'wb') do |szip_file|
if szip_file_index == 1
ssegment_size = put_split_signature(szip_file, segment_size)
end
chunk_bytes = 0
until ssegment_size == chunk_bytes || zip_file.eof?
segment_bytes_left = ssegment_size - chunk_bytes
buffer_size = [segment_bytes_left, DATA_BUFFER_SIZE].min
chunk = zip_file.read(buffer_size)
chunk_bytes += buffer_size
szip_file << chunk
# Info for track splitting
yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
end
end
end
# Splits an archive into parts with segment size
def split(
zip_file_name, segment_size: MAX_SEGMENT_SIZE,
delete_original: true, partial_zip_file_name: nil
)
raise Error, "File #{zip_file_name} not found" unless ::File.exist?(zip_file_name)
raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
zip_file_size = ::File.size(zip_file_name)
segment_size = get_segment_size_for_split(segment_size)
return if zip_file_size <= segment_size
segment_count = get_segment_count_for_split(zip_file_size, segment_size)
::Zip::File.open(zip_file_name) {} # Check for correct zip structure.
partial_zip_file_name = get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
szip_file_index = 0
::File.open(zip_file_name, 'rb') do |zip_file|
until zip_file.eof?
szip_file_index += 1
save_splited_part(
zip_file, partial_zip_file_name, zip_file_size,
szip_file_index, segment_size, segment_count
)
end
end
::File.delete(zip_file_name) if delete_original
szip_file_index
end
end
end

View File

@ -1,4 +1,10 @@
# frozen_string_literal: true
require 'zip'
require_relative 'filesystem/zip_file_name_mapper'
require_relative 'filesystem/directory_iterator'
require_relative 'filesystem/dir'
require_relative 'filesystem/file'
module Zip
# The ZipFileSystem API provides an API for accessing entries in
@ -13,627 +19,52 @@ module Zip
# <code>first.txt</code>, a directory entry named <code>mydir</code>
# and finally another normal entry named <code>second.txt</code>
#
# require 'zip/filesystem'
# ```
# require 'zip/filesystem'
#
# Zip::File.open("my.zip", Zip::File::CREATE) {
# |zipfile|
# zipfile.file.open("first.txt", "w") { |f| f.puts "Hello world" }
# zipfile.dir.mkdir("mydir")
# zipfile.file.open("mydir/second.txt", "w") { |f| f.puts "Hello again" }
# }
# Zip::File.open('my.zip', create: true) do |zipfile|
# zipfile.file.open('first.txt', 'w') { |f| f.puts 'Hello world' }
# zipfile.dir.mkdir('mydir')
# zipfile.file.open('mydir/second.txt', 'w') { |f| f.puts 'Hello again' }
# end
# ```
#
# Reading is as easy as writing, as the following example shows. The
# example writes the contents of <code>first.txt</code> from zip archive
# <code>my.zip</code> to standard out.
#
# require 'zip/filesystem'
# ```
# require 'zip/filesystem'
#
# Zip::File.open("my.zip") {
# |zipfile|
# puts zipfile.file.read("first.txt")
# }
# Zip::File.open('my.zip') do |zipfile|
# puts zipfile.file.read('first.txt')
# end
# ```
module FileSystem
def initialize # :nodoc:
mapped_zip = ZipFileNameMapper.new(self)
@zip_fs_dir = ZipFsDir.new(mapped_zip)
@zip_fs_file = ZipFsFile.new(mapped_zip)
@zip_fs_dir = Dir.new(mapped_zip)
@zip_fs_file = File.new(mapped_zip)
@zip_fs_dir.file = @zip_fs_file
@zip_fs_file.dir = @zip_fs_dir
end
# Returns a ZipFsDir which is much like ruby's builtin Dir (class)
# object, except it works on the Zip::File on which this method is
# Returns a Zip::FileSystem::Dir which is much like ruby's builtin Dir
# (class) object, except it works on the Zip::File on which this method is
# invoked
def dir
@zip_fs_dir
end
# Returns a ZipFsFile which is much like ruby's builtin File (class)
# object, except it works on the Zip::File on which this method is
# Returns a Zip::FileSystem::File which is much like ruby's builtin File
# (class) object, except it works on the Zip::File on which this method is
# invoked
def file
@zip_fs_file
end
# Instances of this class are normally accessed via the accessor
# Zip::File::file. An instance of ZipFsFile behaves like ruby's
# builtin File (class) object, except it works on Zip::File entries.
#
# The individual methods are not documented due to their
# similarity with the methods in File
class ZipFsFile
attr_writer :dir
# protected :dir
class ZipFsStat
class << self
def delegate_to_fs_file(*methods)
methods.each do |method|
class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
def #{method} # def file?
@zip_fs_file.#{method}(@entry_name) # @zip_fs_file.file?(@entry_name)
end # end
END_EVAL
end
end
end
def initialize(zip_fs_file, entry_name)
@zip_fs_file = zip_fs_file
@entry_name = entry_name
end
def kind_of?(type)
super || type == ::File::Stat
end
delegate_to_fs_file :file?, :directory?, :pipe?, :chardev?, :symlink?,
:socket?, :blockdev?, :readable?, :readable_real?, :writable?, :ctime,
:writable_real?, :executable?, :executable_real?, :sticky?, :owned?,
:grpowned?, :setuid?, :setgid?, :zero?, :size, :size?, :mtime, :atime
def blocks
nil
end
def get_entry
@zip_fs_file.__send__(:get_entry, @entry_name)
end
private :get_entry
def gid
e = get_entry
if e.extra.member? 'IUnix'
e.extra['IUnix'].gid || 0
else
0
end
end
def uid
e = get_entry
if e.extra.member? 'IUnix'
e.extra['IUnix'].uid || 0
else
0
end
end
def ino
0
end
def dev
0
end
def rdev
0
end
def rdev_major
0
end
def rdev_minor
0
end
def ftype
if file?
'file'
elsif directory?
'directory'
else
raise StandardError, 'Unknown file type'
end
end
def nlink
1
end
def blksize
nil
end
def mode
e = get_entry
if e.fstype == 3
e.external_file_attributes >> 16
else
33_206 # 33206 is equivalent to -rw-rw-rw-
end
end
end
def initialize(mapped_zip)
@mapped_zip = mapped_zip
end
def get_entry(filename)
unless exists?(filename)
raise Errno::ENOENT, "No such file or directory - #{filename}"
end
@mapped_zip.find_entry(filename)
end
private :get_entry
def unix_mode_cmp(filename, mode)
e = get_entry(filename)
e.fstype == 3 && ((e.external_file_attributes >> 16) & mode) != 0
rescue Errno::ENOENT
false
end
private :unix_mode_cmp
def exists?(filename)
expand_path(filename) == '/' || !@mapped_zip.find_entry(filename).nil?
end
alias exist? exists?
# Permissions not implemented, so if the file exists it is accessible
alias owned? exists?
alias grpowned? exists?
def readable?(filename)
unix_mode_cmp(filename, 0o444)
end
alias readable_real? readable?
def writable?(filename)
unix_mode_cmp(filename, 0o222)
end
alias writable_real? writable?
def executable?(filename)
unix_mode_cmp(filename, 0o111)
end
alias executable_real? executable?
def setuid?(filename)
unix_mode_cmp(filename, 0o4000)
end
def setgid?(filename)
unix_mode_cmp(filename, 0o2000)
end
def sticky?(filename)
unix_mode_cmp(filename, 0o1000)
end
def umask(*args)
::File.umask(*args)
end
def truncate(_filename, _len)
raise StandardError, 'truncate not supported'
end
def directory?(filename)
entry = @mapped_zip.find_entry(filename)
expand_path(filename) == '/' || (!entry.nil? && entry.directory?)
end
def open(filename, mode = 'r', permissions = 0o644, &block)
mode.delete!('b') # ignore b option
case mode
when 'r'
@mapped_zip.get_input_stream(filename, &block)
when 'w'
@mapped_zip.get_output_stream(filename, permissions, &block)
else
raise StandardError, "openmode '#{mode} not supported" unless mode == 'r'
end
end
def new(filename, mode = 'r')
self.open(filename, mode)
end
def size(filename)
@mapped_zip.get_entry(filename).size
end
# Returns nil for not found and nil for directories
def size?(filename)
entry = @mapped_zip.find_entry(filename)
entry.nil? || entry.directory? ? nil : entry.size
end
def chown(owner, group, *filenames)
filenames.each do |filename|
e = get_entry(filename)
e.extra.create('IUnix') unless e.extra.member?('IUnix')
e.extra['IUnix'].uid = owner
e.extra['IUnix'].gid = group
end
filenames.size
end
def chmod(mode, *filenames)
filenames.each do |filename|
e = get_entry(filename)
e.fstype = 3 # force convertion filesystem type to unix
e.unix_perms = mode
e.external_file_attributes = mode << 16
e.dirty = true
end
filenames.size
end
def zero?(filename)
sz = size(filename)
sz.nil? || sz == 0
rescue Errno::ENOENT
false
end
def file?(filename)
entry = @mapped_zip.find_entry(filename)
!entry.nil? && entry.file?
end
def dirname(filename)
::File.dirname(filename)
end
def basename(filename)
::File.basename(filename)
end
def split(filename)
::File.split(filename)
end
def join(*fragments)
::File.join(*fragments)
end
def utime(modified_time, *filenames)
filenames.each do |filename|
get_entry(filename).time = modified_time
end
end
def mtime(filename)
@mapped_zip.get_entry(filename).mtime
end
def atime(filename)
e = get_entry(filename)
if e.extra.member? 'UniversalTime'
e.extra['UniversalTime'].atime
elsif e.extra.member? 'NTFS'
e.extra['NTFS'].atime
end
end
def ctime(filename)
e = get_entry(filename)
if e.extra.member? 'UniversalTime'
e.extra['UniversalTime'].ctime
elsif e.extra.member? 'NTFS'
e.extra['NTFS'].ctime
end
end
def pipe?(_filename)
false
end
def blockdev?(_filename)
false
end
def chardev?(_filename)
false
end
def symlink?(_filename)
false
end
def socket?(_filename)
false
end
def ftype(filename)
@mapped_zip.get_entry(filename).directory? ? 'directory' : 'file'
end
def readlink(_filename)
raise NotImplementedError, 'The readlink() function is not implemented'
end
def symlink(_filename, _symlink_name)
raise NotImplementedError, 'The symlink() function is not implemented'
end
def link(_filename, _symlink_name)
raise NotImplementedError, 'The link() function is not implemented'
end
def pipe
raise NotImplementedError, 'The pipe() function is not implemented'
end
def stat(filename)
raise Errno::ENOENT, filename unless exists?(filename)
ZipFsStat.new(self, filename)
end
alias lstat stat
def readlines(filename)
self.open(filename, &:readlines)
end
def read(filename)
@mapped_zip.read(filename)
end
def popen(*args, &a_proc)
::File.popen(*args, &a_proc)
end
def foreach(filename, sep = $INPUT_RECORD_SEPARATOR, &a_proc)
self.open(filename) { |is| is.each_line(sep, &a_proc) }
end
def delete(*args)
args.each do |filename|
if directory?(filename)
raise Errno::EISDIR, "Is a directory - \"#{filename}\""
end
@mapped_zip.remove(filename)
end
end
def rename(file_to_rename, new_name)
@mapped_zip.rename(file_to_rename, new_name) { true }
end
alias unlink delete
def expand_path(path)
@mapped_zip.expand_path(path)
end
end
# Instances of this class are normally accessed via the accessor
# ZipFile::dir. An instance of ZipFsDir behaves like ruby's
# builtin Dir (class) object, except it works on ZipFile entries.
#
# The individual methods are not documented due to their
# similarity with the methods in Dir
class ZipFsDir
def initialize(mapped_zip)
@mapped_zip = mapped_zip
end
attr_writer :file
def new(directory_name)
ZipFsDirIterator.new(entries(directory_name))
end
def open(directory_name)
dir_iter = new(directory_name)
if block_given?
begin
yield(dir_iter)
return nil
ensure
dir_iter.close
end
end
dir_iter
end
def pwd
@mapped_zip.pwd
end
alias getwd pwd
def chdir(directory_name)
unless @file.stat(directory_name).directory?
raise Errno::EINVAL, "Invalid argument - #{directory_name}"
end
@mapped_zip.pwd = @file.expand_path(directory_name)
end
def entries(directory_name)
entries = []
foreach(directory_name) { |e| entries << e }
entries
end
def glob(*args, &block)
@mapped_zip.glob(*args, &block)
end
def foreach(directory_name)
unless @file.stat(directory_name).directory?
raise Errno::ENOTDIR, directory_name
end
path = @file.expand_path(directory_name)
path << '/' unless path.end_with?('/')
path = Regexp.escape(path)
subdir_entry_regex = Regexp.new("^#{path}([^/]+)$")
@mapped_zip.each do |filename|
match = subdir_entry_regex.match(filename)
yield(match[1]) unless match.nil?
end
end
def delete(entry_name)
unless @file.stat(entry_name).directory?
raise Errno::EINVAL, "Invalid argument - #{entry_name}"
end
@mapped_zip.remove(entry_name)
end
alias rmdir delete
alias unlink delete
def mkdir(entry_name, permissions = 0o755)
@mapped_zip.mkdir(entry_name, permissions)
end
def chroot(*_args)
raise NotImplementedError, 'The chroot() function is not implemented'
end
end
class ZipFsDirIterator # :nodoc:all
include Enumerable
def initialize(filenames)
@filenames = filenames
@index = 0
end
def close
@filenames = nil
end
def each(&a_proc)
raise IOError, 'closed directory' if @filenames.nil?
@filenames.each(&a_proc)
end
def read
raise IOError, 'closed directory' if @filenames.nil?
@filenames[(@index += 1) - 1]
end
def rewind
raise IOError, 'closed directory' if @filenames.nil?
@index = 0
end
def seek(position)
raise IOError, 'closed directory' if @filenames.nil?
@index = position
end
def tell
raise IOError, 'closed directory' if @filenames.nil?
@index
end
end
# All access to Zip::File from ZipFsFile and ZipFsDir goes through a
# ZipFileNameMapper, which has one responsibility: ensure
class ZipFileNameMapper # :nodoc:all
include Enumerable
def initialize(zip_file)
@zip_file = zip_file
@pwd = '/'
end
attr_accessor :pwd
def find_entry(filename)
@zip_file.find_entry(expand_to_entry(filename))
end
def get_entry(filename)
@zip_file.get_entry(expand_to_entry(filename))
end
def get_input_stream(filename, &a_proc)
@zip_file.get_input_stream(expand_to_entry(filename), &a_proc)
end
def get_output_stream(filename, permissions = nil, &a_proc)
@zip_file.get_output_stream(
expand_to_entry(filename), permissions, &a_proc
)
end
def glob(pattern, *flags, &block)
@zip_file.glob(expand_to_entry(pattern), *flags, &block)
end
def read(filename)
@zip_file.read(expand_to_entry(filename))
end
def remove(filename)
@zip_file.remove(expand_to_entry(filename))
end
def rename(filename, new_name, &continue_on_exists_proc)
@zip_file.rename(
expand_to_entry(filename),
expand_to_entry(new_name),
&continue_on_exists_proc
)
end
def mkdir(filename, permissions = 0o755)
@zip_file.mkdir(expand_to_entry(filename), permissions)
end
# Turns entries into strings and adds leading /
# and removes trailing slash on directories
def each
@zip_file.each do |e|
yield('/' + e.to_s.chomp('/'))
end
end
def expand_path(path)
expanded = path.start_with?('/') ? path : ::File.join(@pwd, path)
expanded.gsub!(/\/\.(\/|$)/, '')
expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, '')
expanded.empty? ? '/' : expanded
end
private
def expand_to_entry(path)
expand_path(path)[1..-1]
end
end
end
class File
class File # :nodoc:
include FileSystem
end
end

86
lib/zip/filesystem/dir.rb Normal file
View File

@ -0,0 +1,86 @@
# frozen_string_literal: true
module Zip
module FileSystem
class Dir # :nodoc:all
def initialize(mapped_zip)
@mapped_zip = mapped_zip
end
attr_writer :file
def new(directory_name)
DirectoryIterator.new(entries(directory_name))
end
def open(directory_name)
dir_iter = new(directory_name)
if block_given?
begin
yield(dir_iter)
return nil
ensure
dir_iter.close
end
end
dir_iter
end
def pwd
@mapped_zip.pwd
end
alias getwd pwd
def chdir(directory_name)
unless @file.stat(directory_name).directory?
raise Errno::EINVAL, "Invalid argument - #{directory_name}"
end
@mapped_zip.pwd = @file.expand_path(directory_name)
end
def entries(directory_name)
entries = []
foreach(directory_name) { |e| entries << e }
entries
end
def glob(...)
@mapped_zip.glob(...)
end
def foreach(directory_name)
unless @file.stat(directory_name).directory?
raise Errno::ENOTDIR, directory_name
end
path = @file.expand_path(directory_name)
path << '/' unless path.end_with?('/')
path = Regexp.escape(path)
subdir_entry_regex = Regexp.new("^#{path}([^/]+)$")
@mapped_zip.each do |filename|
match = subdir_entry_regex.match(filename)
yield(match[1]) unless match.nil?
end
end
def delete(entry_name)
unless @file.stat(entry_name).directory?
raise Errno::EINVAL, "Invalid argument - #{entry_name}"
end
@mapped_zip.remove(entry_name)
end
alias rmdir delete
alias unlink delete
def mkdir(entry_name, permissions = 0o755)
@mapped_zip.mkdir(entry_name, permissions)
end
def chroot(*_args)
raise NotImplementedError, 'The chroot() function is not implemented'
end
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
module Zip
module FileSystem
class DirectoryIterator # :nodoc:all
include Enumerable
def initialize(filenames)
@filenames = filenames
@index = 0
end
def close
@filenames = nil
end
def each(&a_proc)
raise IOError, 'closed directory' if @filenames.nil?
@filenames.each(&a_proc)
end
def read
raise IOError, 'closed directory' if @filenames.nil?
@filenames[(@index += 1) - 1]
end
def rewind
raise IOError, 'closed directory' if @filenames.nil?
@index = 0
end
def seek(position)
raise IOError, 'closed directory' if @filenames.nil?
@index = position
end
def tell
raise IOError, 'closed directory' if @filenames.nil?
@index
end
end
end
end

262
lib/zip/filesystem/file.rb Normal file
View File

@ -0,0 +1,262 @@
# frozen_string_literal: true
require_relative 'file_stat'
module Zip
module FileSystem
# Instances of this class are normally accessed via the accessor
# Zip::File::file. An instance of File behaves like ruby's
# builtin File (class) object, except it works on Zip::File entries.
#
# The individual methods are not documented due to their
# similarity with the methods in File
class File # :nodoc:all
attr_writer :dir
def initialize(mapped_zip)
@mapped_zip = mapped_zip
end
def find_entry(filename)
unless exists?(filename)
raise Errno::ENOENT, "No such file or directory - #{filename}"
end
@mapped_zip.find_entry(filename)
end
def unix_mode_cmp(filename, mode)
e = find_entry(filename)
e.fstype == FSTYPE_UNIX && ((e.external_file_attributes >> 16) & mode) != 0
rescue Errno::ENOENT
false
end
private :unix_mode_cmp
def exists?(filename)
expand_path(filename) == '/' || !@mapped_zip.find_entry(filename).nil?
end
alias exist? exists?
# Permissions not implemented, so if the file exists it is accessible
alias owned? exists?
alias grpowned? exists?
def readable?(filename)
unix_mode_cmp(filename, 0o444)
end
alias readable_real? readable?
def writable?(filename)
unix_mode_cmp(filename, 0o222)
end
alias writable_real? writable?
def executable?(filename)
unix_mode_cmp(filename, 0o111)
end
alias executable_real? executable?
def setuid?(filename)
unix_mode_cmp(filename, 0o4000)
end
def setgid?(filename)
unix_mode_cmp(filename, 0o2000)
end
def sticky?(filename)
unix_mode_cmp(filename, 0o1000)
end
def umask(*args)
::File.umask(*args)
end
def truncate(_filename, _len)
raise StandardError, 'truncate not supported'
end
def directory?(filename)
entry = @mapped_zip.find_entry(filename)
expand_path(filename) == '/' || (!entry.nil? && entry.directory?)
end
def open(filename, mode = 'r', permissions = 0o644, &block)
mode = mode.tr('b', '') # ignore b option
case mode
when 'r'
@mapped_zip.get_input_stream(filename, &block)
when 'w'
@mapped_zip.get_output_stream(filename, permissions, &block)
else
raise StandardError, "openmode '#{mode} not supported" unless mode == 'r'
end
end
def new(filename, mode = 'r')
self.open(filename, mode)
end
def size(filename)
@mapped_zip.get_entry(filename).size
end
# Returns nil for not found and nil for directories
def size?(filename)
entry = @mapped_zip.find_entry(filename)
entry.nil? || entry.directory? ? nil : entry.size
end
def chown(owner, group, *filenames)
filenames.each do |filename|
e = find_entry(filename)
e.extra.create('IUnix') unless e.extra.member?('IUnix')
e.extra['IUnix'].uid = owner
e.extra['IUnix'].gid = group
end
filenames.size
end
def chmod(mode, *filenames)
filenames.each do |filename|
e = find_entry(filename)
e.fstype = FSTYPE_UNIX # Force conversion filesystem type to unix.
e.unix_perms = mode
e.external_file_attributes = mode << 16
end
filenames.size
end
def zero?(filename)
sz = size(filename)
sz.nil? || sz == 0
rescue Errno::ENOENT
false
end
def file?(filename)
entry = @mapped_zip.find_entry(filename)
!entry.nil? && entry.file?
end
def dirname(filename)
::File.dirname(filename)
end
def basename(filename)
::File.basename(filename)
end
def split(filename)
::File.split(filename)
end
def join(*fragments)
::File.join(*fragments)
end
def utime(modified_time, *filenames)
filenames.each do |filename|
find_entry(filename).time = modified_time
end
end
def mtime(filename)
@mapped_zip.get_entry(filename).mtime
end
def atime(filename)
@mapped_zip.get_entry(filename).atime
end
def ctime(filename)
@mapped_zip.get_entry(filename).ctime
end
def pipe?(_filename)
false
end
def blockdev?(_filename)
false
end
def chardev?(_filename)
false
end
def symlink?(filename)
@mapped_zip.get_entry(filename).symlink?
end
def socket?(_filename)
false
end
def ftype(filename)
@mapped_zip.get_entry(filename).directory? ? 'directory' : 'file'
end
def readlink(_filename)
raise NotImplementedError, 'The readlink() function is not implemented'
end
def symlink(_filename, _symlink_name)
raise NotImplementedError, 'The symlink() function is not implemented'
end
def link(_filename, _symlink_name)
raise NotImplementedError, 'The link() function is not implemented'
end
def pipe
raise NotImplementedError, 'The pipe() function is not implemented'
end
def stat(filename)
raise Errno::ENOENT, filename unless exists?(filename)
Stat.new(self, filename)
end
alias lstat stat
def readlines(filename)
self.open(filename, &:readlines)
end
def read(filename)
@mapped_zip.read(filename)
end
def popen(*args, &a_proc)
::File.popen(*args, &a_proc)
end
def foreach(filename, sep = $INPUT_RECORD_SEPARATOR, &a_proc)
self.open(filename) { |is| is.each_line(sep, &a_proc) }
end
def delete(*args)
args.each do |filename|
if directory?(filename)
raise Errno::EISDIR, "Is a directory - \"#{filename}\""
end
@mapped_zip.remove(filename)
end
end
def rename(file_to_rename, new_name)
@mapped_zip.rename(file_to_rename, new_name) { true }
end
alias unlink delete
def expand_path(path)
@mapped_zip.expand_path(path)
end
end
end
end

View File

@ -0,0 +1,110 @@
# frozen_string_literal: true
module Zip
module FileSystem
class File # :nodoc:all
class Stat # :nodoc:all
class << self
def delegate_to_fs_file(*methods)
methods.each do |method|
class_exec do
define_method(method) do
@zip_fs_file.__send__(method, @entry_name)
end
end
end
end
end
def initialize(zip_fs_file, entry_name)
@zip_fs_file = zip_fs_file
@entry_name = entry_name
end
def kind_of?(type)
super || type == ::File::Stat
end
delegate_to_fs_file :file?, :directory?, :pipe?, :chardev?, :symlink?,
:socket?, :blockdev?, :readable?, :readable_real?, :writable?, :ctime,
:writable_real?, :executable?, :executable_real?, :sticky?, :owned?,
:grpowned?, :setuid?, :setgid?, :zero?, :size, :size?, :mtime, :atime
def blocks
nil
end
def gid
e = find_entry
if e.extra.member? 'IUnix'
e.extra['IUnix'].gid || 0
else
0
end
end
def uid
e = find_entry
if e.extra.member? 'IUnix'
e.extra['IUnix'].uid || 0
else
0
end
end
def ino
0
end
def dev
0
end
def rdev
0
end
def rdev_major
0
end
def rdev_minor
0
end
def ftype
if file?
'file'
elsif directory?
'directory'
else
raise StandardError, 'Unknown file type'
end
end
def nlink
1
end
def blksize
nil
end
def mode
e = find_entry
if e.fstype == FSTYPE_UNIX
e.external_file_attributes >> 16
else
0o100_666 # Equivalent to -rw-rw-rw-.
end
end
private
def find_entry
@zip_fs_file.find_entry(@entry_name)
end
end
end
end
end

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
module Zip
module FileSystem
# All access to Zip::File from FileSystem::File and FileSystem::Dir
# goes through a ZipFileNameMapper, which has one responsibility: ensure
class ZipFileNameMapper # :nodoc:all
include Enumerable
def initialize(zip_file)
@zip_file = zip_file
@pwd = '/'
end
attr_accessor :pwd
def find_entry(filename)
@zip_file.find_entry(expand_to_entry(filename))
end
def get_entry(filename)
@zip_file.get_entry(expand_to_entry(filename))
end
def get_input_stream(filename, &a_proc)
@zip_file.get_input_stream(expand_to_entry(filename), &a_proc)
end
def get_output_stream(filename, permissions = nil, &a_proc)
@zip_file.get_output_stream(
expand_to_entry(filename), permissions: permissions, &a_proc
)
end
def glob(pattern, *flags, &block)
@zip_file.glob(expand_to_entry(pattern), *flags, &block)
end
def read(filename)
@zip_file.read(expand_to_entry(filename))
end
def remove(filename)
@zip_file.remove(expand_to_entry(filename))
end
def rename(filename, new_name, &continue_on_exists_proc)
@zip_file.rename(
expand_to_entry(filename),
expand_to_entry(new_name),
&continue_on_exists_proc
)
end
def mkdir(filename, permissions = 0o755)
@zip_file.mkdir(expand_to_entry(filename), permissions)
end
# Turns entries into strings and adds leading /
# and removes trailing slash on directories
def each
@zip_file.each do |e|
yield("/#{e.to_s.chomp('/')}")
end
end
def expand_path(path)
expanded = path.start_with?('/') ? path.dup : ::File.join(@pwd, path)
expanded.gsub!(/\/\.(\/|$)/, '')
expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, '')
expanded.empty? ? '/' : expanded
end
private
def expand_to_entry(path)
expand_path(path)[1..]
end
end
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
class Inflater < Decompressor #:nodoc:all
class Inflater < Decompressor # :nodoc:all
def initialize(*args)
super
@ -7,7 +9,7 @@ module Zip
@zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS)
end
def read(length = nil, outbuf = '')
def read(length = nil, outbuf = +'')
return (length.nil? || length.zero? ? '' : nil) if eof
while length.nil? || (@buffer.bytesize < length)
@ -37,8 +39,8 @@ module Zip
retried += 1
retry
end
rescue Zlib::Error
raise(::Zip::DecompressionError, 'zlib error while inflating')
rescue Zlib::Error => e
raise ::Zip::DecompressionError, e
end
def input_finished?

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
##
module Zip
# InputStream is the basic class for reading zip entries in a
# zip file. It is possible to create a InputStream object directly,
@ -37,9 +40,8 @@ module Zip
#
# java.util.zip.ZipInputStream is the original inspiration for this
# class.
class InputStream
CHUNK_SIZE = 32_768
CHUNK_SIZE = 32_768 # :nodoc:
include ::Zip::IOExtras::AbstractInputStream
@ -49,28 +51,35 @@ module Zip
#
# @param context [String||IO||StringIO] file path or IO/StringIO object
# @param offset [Integer] offset in the IO/StringIO
def initialize(context, offset = 0, decrypter = nil)
def initialize(context, offset: 0, decrypter: nil)
super()
@archive_io = get_io(context, offset)
@decompressor = ::Zip::NullDecompressor
@decrypter = decrypter || ::Zip::NullDecrypter.new
@archive_io = get_io(context, offset)
@decompressor = ::Zip::NullDecompressor
@decrypter = decrypter || ::Zip::NullDecrypter.new
@current_entry = nil
@complete_entry = nil
end
# Close this InputStream. All further IO will raise an IOError.
def close
@archive_io.close
end
# Returns a Entry object. It is necessary to call this
# method on a newly created InputStream before reading from
# the first entry in the archive. Returns nil when there are
# no more entries.
# Returns an Entry object and positions the stream at the beginning of
# the entry data. It is necessary to call this method on a newly created
# InputStream before reading from the first entry in the archive.
# Returns nil when there are no more entries.
def get_next_entry
@archive_io.seek(@current_entry.next_header_offset, IO::SEEK_SET) if @current_entry
unless @current_entry.nil?
raise StreamingError, @current_entry if @current_entry.incomplete?
@archive_io.seek(@current_entry.next_header_offset, IO::SEEK_SET)
end
open_entry
end
# Rewinds the stream to the beginning of the current entry
# Rewinds the stream to the beginning of the current entry.
def rewind
return if @current_entry.nil?
@ -85,12 +94,19 @@ module Zip
@decompressor.read(length, outbuf)
end
# Returns the size of the current entry, or `nil` if there isn't one.
def size
return if @current_entry.nil?
@current_entry.size
end
class << self
# Same as #initialize but if a block is passed the opened
# stream is passed to the block and closed when the block
# returns.
def open(filename_or_io, offset = 0, decrypter = nil)
zio = new(filename_or_io, offset, decrypter)
def open(filename_or_io, offset: 0, decrypter: nil)
zio = new(filename_or_io, offset: offset, decrypter: decrypter)
return zio unless block_given?
begin
@ -99,16 +115,11 @@ module Zip
zio.close if zio
end
end
def open_buffer(filename_or_io, offset = 0)
warn 'open_buffer is deprecated!!! Use open instead!'
::Zip::InputStream.open(filename_or_io, offset)
end
end
protected
def get_io(io_or_file, offset = 0)
def get_io(io_or_file, offset = 0) # :nodoc:
if io_or_file.respond_to?(:seek)
io = io_or_file.dup
io.seek(offset, ::IO::SEEK_SET)
@ -120,56 +131,58 @@ module Zip
end
end
def open_entry
def open_entry # :nodoc:
@current_entry = ::Zip::Entry.read_local_entry(@archive_io)
if @current_entry && @current_entry.encrypted? && @decrypter.kind_of?(NullEncrypter)
raise Error, 'password required to decode zip file'
return if @current_entry.nil?
if @current_entry.encrypted? && @decrypter.kind_of?(NullDecrypter)
raise Error,
'A password is required to decode this zip file'
end
if @current_entry && @current_entry.incomplete? && @current_entry.crc == 0 \
&& @current_entry.compressed_size == 0 \
&& @current_entry.size == 0 && !@complete_entry
raise GPFBit3Error,
'General purpose flag Bit 3 is set so not possible to get proper info from local header.' \
'Please use ::Zip::File instead of ::Zip::InputStream'
if @current_entry.incomplete? && @current_entry.compressed_size == 0 && !@complete_entry
raise StreamingError, @current_entry
end
@decrypted_io = get_decrypted_io
@decompressor = get_decompressor
flush
@current_entry
end
def get_decrypted_io
def get_decrypted_io # :nodoc:
header = @archive_io.read(@decrypter.header_bytesize)
@decrypter.reset!(header)
::Zip::DecryptedIo.new(@archive_io, @decrypter)
end
def get_decompressor
def get_decompressor # :nodoc:
return ::Zip::NullDecompressor if @current_entry.nil?
decompressed_size =
if @current_entry.incomplete? && @current_entry.crc == 0 && @current_entry.size == 0 && @complete_entry
if @current_entry.incomplete? && @current_entry.crc == 0 &&
@current_entry.size == 0 && @complete_entry
@complete_entry.size
else
@current_entry.size
end
decompressor_class = ::Zip::Decompressor.find_by_compression_method(@current_entry.compression_method)
decompressor_class = ::Zip::Decompressor.find_by_compression_method(
@current_entry.compression_method
)
if decompressor_class.nil?
raise ::Zip::CompressionMethodError,
"Unsupported compression method #{@current_entry.compression_method}"
raise ::Zip::CompressionMethodError, @current_entry.compression_method
end
decompressor_class.new(@decrypted_io, decompressed_size)
end
def produce_input
def produce_input # :nodoc:
@decompressor.read(CHUNK_SIZE)
end
def input_finished?
def input_finished? # :nodoc:
@decompressor.eof
end
end

View File

@ -1,26 +1,26 @@
module Zip
module IOExtras #:nodoc:
CHUNK_SIZE = 131_072
# frozen_string_literal: true
RANGE_ALL = 0..-1
module Zip
module IOExtras # :nodoc:
CHUNK_SIZE = 131_072
class << self
def copy_stream(ostream, istream)
ostream.write(istream.read(CHUNK_SIZE, '')) until istream.eof?
ostream.write(istream.read(CHUNK_SIZE, +'')) until istream.eof?
end
def copy_stream_n(ostream, istream, nbytes)
toread = nbytes
while toread > 0 && !istream.eof?
tr = toread > CHUNK_SIZE ? CHUNK_SIZE : toread
ostream.write(istream.read(tr, ''))
tr = [toread, CHUNK_SIZE].min
ostream.write(istream.read(tr, +''))
toread -= tr
end
end
end
# Implements kind_of? in order to pretend to be an IO object
module FakeIO
module FakeIO # :nodoc:
def kind_of?(object)
object == IO || super
end

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
module Zip
module IOExtras
module IOExtras # :nodoc:
# Implements many of the convenience methods of IO
# such as gets, getc, readline and readlines
# depends on: input_finished?, produce_input and read
module AbstractInputStream
module AbstractInputStream # :nodoc:
include Enumerable
include FakeIO
@ -11,15 +13,15 @@ module Zip
super
@lineno = 0
@pos = 0
@output_buffer = ''
@output_buffer = +''
end
attr_accessor :lineno
attr_reader :pos
def read(number_of_bytes = nil, buf = '')
def read(number_of_bytes = nil, buf = +'')
tbuf = if @output_buffer.bytesize > 0
if number_of_bytes <= @output_buffer.bytesize
if number_of_bytes && number_of_bytes <= @output_buffer.bytesize
@output_buffer.slice!(0, number_of_bytes)
else
number_of_bytes -= @output_buffer.bytesize if number_of_bytes
@ -34,7 +36,7 @@ module Zip
end
if tbuf.nil? || tbuf.empty?
return nil if number_of_bytes
return nil if number_of_bytes&.positive?
return ''
end
@ -74,15 +76,18 @@ module Zip
a_sep_string = "#{$INPUT_RECORD_SEPARATOR}#{$INPUT_RECORD_SEPARATOR}" if a_sep_string.empty?
buffer_index = 0
over_limit = (number_of_bytes && @output_buffer.bytesize >= number_of_bytes)
over_limit = number_of_bytes && @output_buffer.bytesize >= number_of_bytes
while (match_index = @output_buffer.index(a_sep_string, buffer_index)).nil? && !over_limit
buffer_index = [buffer_index, @output_buffer.bytesize - a_sep_string.bytesize].max
return @output_buffer.empty? ? nil : flush if input_finished?
@output_buffer << produce_input
over_limit = (number_of_bytes && @output_buffer.bytesize >= number_of_bytes)
over_limit = number_of_bytes && @output_buffer.bytesize >= number_of_bytes
end
sep_index = [match_index + a_sep_string.bytesize, number_of_bytes || @output_buffer.bytesize].min
sep_index = [
match_index + a_sep_string.bytesize,
number_of_bytes || @output_buffer.bytesize
].min
@pos += sep_index
@output_buffer.slice!(0...sep_index)
end
@ -93,7 +98,7 @@ module Zip
def flush
ret_val = @output_buffer
@output_buffer = ''
@output_buffer = +''
ret_val
end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
module Zip
module IOExtras
module IOExtras # :nodoc:
# Implements many of the output convenience methods of IO.
# relies on <<
module AbstractOutputStream
module AbstractOutputStream # :nodoc:
include FakeIO
def write(data)
@ -11,7 +13,7 @@ module Zip
end
def print(*params)
self << params.join($OUTPUT_FIELD_SEPARATOR) << $OUTPUT_RECORD_SEPARATOR.to_s
self << params.join << $OUTPUT_RECORD_SEPARATOR.to_s
end
def printf(a_format_string, *params)

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
class NullCompressor < Compressor #:nodoc:all
class NullCompressor < Compressor # :nodoc:all
include Singleton
def <<(_data)

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
module NullDecompressor #:nodoc:all
module NullDecompressor # :nodoc:all
module_function
def read(_length = nil, _outbuf = nil)

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
module NullInputStream #:nodoc:all
module NullInputStream # :nodoc:all
include ::Zip::NullDecompressor
include ::Zip::IOExtras::AbstractInputStream
end

View File

@ -1,3 +1,8 @@
# frozen_string_literal: true
require 'forwardable'
##
module Zip
# ZipOutputStream is the basic class for writing zip files. It is
# possible to create a ZipOutputStream object directly, passing
@ -16,50 +21,49 @@ module Zip
#
# java.util.zip.ZipOutputStream is the original inspiration for this
# class.
class OutputStream
extend Forwardable
include ::Zip::IOExtras::AbstractOutputStream
attr_accessor :comment
def_delegators :@cdir, :comment, :comment=
# Opens the indicated zip file. If a file with that name already
# exists it will be overwritten.
def initialize(file_name, stream = false, encrypter = nil)
def initialize(file_name, stream: false, encrypter: nil)
super()
@file_name = file_name
@output_stream = if stream
iostream = @file_name.dup
iostream = Zip::RUNNING_ON_WINDOWS ? @file_name : @file_name.dup
iostream.reopen(@file_name)
iostream.rewind
iostream
else
::File.new(@file_name, 'wb')
end
@entry_set = ::Zip::EntrySet.new
@cdir = ::Zip::CentralDirectory.new
@compressor = ::Zip::NullCompressor.instance
@encrypter = encrypter || ::Zip::NullEncrypter.new
@closed = false
@current_entry = nil
@comment = nil
end
# Same as #initialize but if a block is passed the opened
# stream is passed to the block and closed when the block
# returns.
class << self
def open(file_name, encrypter = nil)
# Same as #initialize but if a block is passed the opened
# stream is passed to the block and closed when the block
# returns.
def open(file_name, encrypter: nil)
return new(file_name) unless block_given?
zos = new(file_name, false, encrypter)
zos = new(file_name, stream: false, encrypter: encrypter)
yield zos
ensure
zos.close if zos
end
# Same as #open but writes to a filestream instead
def write_buffer(io = ::StringIO.new(''), encrypter = nil)
def write_buffer(io = ::StringIO.new, encrypter: nil)
io.binmode if io.respond_to?(:binmode)
zos = new(io, true, encrypter)
zos = new(io, stream: true, encrypter: encrypter)
yield zos
zos.close_buffer
end
@ -71,7 +75,7 @@ module Zip
finalize_current_entry
update_local_headers
write_central_directory
@cdir.write_to_stream(@output_stream)
@output_stream.close
@closed = true
end
@ -82,37 +86,41 @@ module Zip
finalize_current_entry
update_local_headers
write_central_directory
@cdir.write_to_stream(@output_stream)
@closed = true
@output_stream.flush
@output_stream
end
# Closes the current entry and opens a new for writing.
# +entry+ can be a ZipEntry object or a string.
def put_next_entry(entry_name, comment = nil, extra = nil, compression_method = Entry::DEFLATED, level = Zip.default_compression)
def put_next_entry(
entry_name, comment = '', extra = ExtraField.new,
compression_method = Entry::DEFLATED, level = Zip.default_compression
)
raise Error, 'zip stream is closed' if @closed
new_entry = if entry_name.kind_of?(Entry)
entry_name
else
Entry.new(@file_name, entry_name.to_s)
end
new_entry.comment = comment unless comment.nil?
unless extra.nil?
new_entry.extra = extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
end
new_entry.compression_method = compression_method unless compression_method.nil?
init_next_entry(new_entry, level)
new_entry =
if entry_name.kind_of?(Entry) || entry_name.kind_of?(StreamableStream)
entry_name
else
Entry.new(
@file_name, entry_name.to_s, comment: comment, extra: extra,
compression_method: compression_method, compression_level: level
)
end
init_next_entry(new_entry)
@current_entry = new_entry
end
def copy_raw_entry(entry)
def copy_raw_entry(entry) # :nodoc:
entry = entry.dup
raise Error, 'zip stream is closed' if @closed
raise Error, 'entry is not a ZipEntry' unless entry.kind_of?(Entry)
finalize_current_entry
@entry_set << entry
@cdir << entry
src_pos = entry.local_header_offset
entry.write_local_entry(@output_stream)
@compressor = NullCompressor.instance
@ -131,55 +139,53 @@ module Zip
return unless @current_entry
finish
@current_entry.compressed_size = @output_stream.tell - \
@current_entry.local_header_offset - \
@current_entry.compressed_size = @output_stream.tell -
@current_entry.local_header_offset -
@current_entry.calculate_local_header_size
@current_entry.size = @compressor.size
@current_entry.crc = @compressor.crc
@output_stream << @encrypter.data_descriptor(@current_entry.crc, @current_entry.compressed_size, @current_entry.size)
@output_stream << @encrypter.data_descriptor(
@current_entry.crc,
@current_entry.compressed_size,
@current_entry.size
)
@current_entry.gp_flags |= @encrypter.gp_flags
@current_entry = nil
@compressor = ::Zip::NullCompressor.instance
end
def init_next_entry(entry, level = Zip.default_compression)
def init_next_entry(entry)
finalize_current_entry
@entry_set << entry
@cdir << entry
entry.write_local_entry(@output_stream)
@encrypter.reset!
@output_stream << @encrypter.header(entry.mtime)
@compressor = get_compressor(entry, level)
@compressor = get_compressor(entry)
end
def get_compressor(entry, level)
def get_compressor(entry)
case entry.compression_method
when Entry::DEFLATED
::Zip::Deflater.new(@output_stream, level, @encrypter)
::Zip::Deflater.new(@output_stream, entry.compression_level, @encrypter)
when Entry::STORED
::Zip::PassThruCompressor.new(@output_stream)
else
raise ::Zip::CompressionMethodError,
"Invalid compression method: '#{entry.compression_method}'"
raise ::Zip::CompressionMethodError, entry.compression_method
end
end
def update_local_headers
pos = @output_stream.pos
@entry_set.each do |entry|
@cdir.each do |entry|
@output_stream.pos = entry.local_header_offset
entry.write_local_entry(@output_stream, true)
entry.write_local_entry(@output_stream, rewrite: true)
end
@output_stream.pos = pos
end
def write_central_directory
cdir = CentralDirectory.new(@entry_set, @comment)
cdir.write_to_stream(@output_stream)
end
protected
def finish
def finish # :nodoc:
@compressor.finish
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
class PassThruCompressor < Compressor #:nodoc:all
class PassThruCompressor < Compressor # :nodoc:all
def initialize(output_stream)
super()
@output_stream = output_stream

View File

@ -1,11 +1,13 @@
# frozen_string_literal: true
module Zip
class PassThruDecompressor < Decompressor #:nodoc:all
class PassThruDecompressor < Decompressor # :nodoc:all
def initialize(*args)
super
@read_so_far = 0
end
def read(length = nil, outbuf = '')
def read(length = nil, outbuf = +'')
return (length.nil? || length.zero? ? '' : nil) if eof
if length.nil? || (@read_so_far + length) > decompressed_size

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
module Zip
class StreamableDirectory < Entry
class StreamableDirectory < Entry # :nodoc:
def initialize(zipfile, entry, src_path = nil, permission = nil)
super(zipfile, entry)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Zip
class StreamableStream < DelegateClass(Entry) # :nodoc:all
def initialize(entry)
@ -42,6 +44,7 @@ module Zip
end
def clean_up
super
@temp_file.unlink
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Zip
VERSION = '2.3.0'
VERSION = '3.0.0.rc2' # :nodoc:
end

View File

@ -1,29 +1,39 @@
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'zip/version'
# frozen_string_literal: true
require_relative 'lib/zip/version'
Gem::Specification.new do |s|
s.name = 'rubyzip'
s.version = ::Zip::VERSION
s.authors = ['Alexander Simonov']
s.email = ['alex@simonov.me']
s.homepage = 'http://github.com/rubyzip/rubyzip'
s.platform = Gem::Platform::RUBY
s.summary = 'rubyzip is a ruby module for reading and writing zip files'
s.files = Dir.glob('{samples,lib}/**/*.rb') + %w[README.md TODO Rakefile]
s.require_paths = ['lib']
s.license = 'BSD 2-Clause'
s.metadata = {
'bug_tracker_uri' => 'https://github.com/rubyzip/rubyzip/issues',
'changelog_uri' => "https://github.com/rubyzip/rubyzip/blob/v#{s.version}/Changelog.md",
'documentation_uri' => "https://www.rubydoc.info/gems/rubyzip/#{s.version}",
'source_code_uri' => "https://github.com/rubyzip/rubyzip/tree/v#{s.version}",
'wiki_uri' => 'https://github.com/rubyzip/rubyzip/wiki'
s.name = 'rubyzip'
s.version = Zip::VERSION
s.authors = ['Robert Haines', 'John Lees-Miller', 'Alexander Simonov']
s.email = [
'hainesr@gmail.com', 'jdleesmiller@gmail.com', 'alex@simonov.me'
]
s.homepage = 'http://github.com/rubyzip/rubyzip'
s.platform = Gem::Platform::RUBY
s.summary = 'rubyzip is a ruby module for reading and writing zip files'
s.files = Dir.glob('{samples,lib}/**/*.rb') +
%w[LICENSE.md README.md Changelog.md Rakefile rubyzip.gemspec]
s.require_paths = ['lib']
s.license = 'BSD-2-Clause'
s.metadata = {
'bug_tracker_uri' => 'https://github.com/rubyzip/rubyzip/issues',
'changelog_uri' => "https://github.com/rubyzip/rubyzip/blob/v#{s.version}/Changelog.md",
'documentation_uri' => "https://www.rubydoc.info/gems/rubyzip/#{s.version}",
'source_code_uri' => "https://github.com/rubyzip/rubyzip/tree/v#{s.version}",
'wiki_uri' => 'https://github.com/rubyzip/rubyzip/wiki',
'rubygems_mfa_required' => 'true'
}
s.required_ruby_version = '>= 2.4'
s.add_development_dependency 'coveralls', '~> 0.7'
s.add_development_dependency 'minitest', '~> 5.4'
s.add_development_dependency 'pry', '~> 0.10'
s.add_development_dependency 'rake', '~> 12.3', '>= 12.3.3'
s.add_development_dependency 'rubocop', '~> 0.79'
s.required_ruby_version = '>= 3.0'
s.add_development_dependency 'minitest', '~> 5.25'
s.add_development_dependency 'rake', '~> 13.2'
s.add_development_dependency 'rdoc', '~> 6.11'
s.add_development_dependency 'rubocop', '~> 1.61.0'
s.add_development_dependency 'rubocop-performance', '~> 1.20.0'
s.add_development_dependency 'rubocop-rake', '~> 0.6.0'
s.add_development_dependency 'simplecov', '~> 0.22.0'
s.add_development_dependency 'simplecov-lcov', '~> 0.8'
end

View File

@ -1,4 +0,0 @@
example.zip
exampleout.zip
filesystem.zip
zipdialogui.rb

View File

@ -1,4 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH << '../lib'
system('zip example.zip example.rb gtk_ruby_zip.rb')
@ -20,7 +21,8 @@ end
zf = Zip::File.new('example.zip')
zf.each_with_index do |entry, index|
puts "entry #{index} is #{entry.name}, size = #{entry.size}, compressed size = #{entry.compressed_size}"
puts "entry #{index} is #{entry.name}, size = #{entry.size}, " \
"compressed size = #{entry.compressed_size}"
# use zf.get_input_stream(entry) to get a ZipInputStream for the entry
# entry can be the ZipEntry object or any object which has a to_s method that
# returns the name of the entry.
@ -70,8 +72,11 @@ part_zips_count = Zip::File.split('large_zip_file.zip', 2_097_152, false)
puts "Zip file splitted in #{part_zips_count} parts"
# Track splitting an archive
Zip::File.split('large_zip_file.zip', 1_048_576, true, 'part_zip_file') do |part_count, part_index, chunk_bytes, segment_bytes|
puts "#{part_index} of #{part_count} part splitting: #{(chunk_bytes.to_f / segment_bytes * 100).to_i}%"
Zip::File.split(
'large_zip_file.zip', 1_048_576, true, 'part_zip_file'
) do |part_count, part_index, chunk_bytes, segment_bytes|
puts "#{part_index} of #{part_count} part splitting: " \
"#{(chunk_bytes.to_f / segment_bytes * 100).to_i}%"
end
# For other examples, look at zip.rb and ziptest.rb

View File

@ -1,4 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH << '../lib'
@ -6,9 +7,9 @@ require 'zip/filesystem'
EXAMPLE_ZIP = 'filesystem.zip'
File.delete(EXAMPLE_ZIP) if File.exist?(EXAMPLE_ZIP)
FileUtils.rm_f(EXAMPLE_ZIP)
Zip::File.open(EXAMPLE_ZIP, Zip::File::CREATE) do |zf|
Zip::File.open(EXAMPLE_ZIP, create: true) do |zf|
zf.file.open('file1.txt', 'w') { |os| os.write 'first file1.txt' }
zf.dir.mkdir('dir1')
zf.dir.chdir('dir1')

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'zip'
# This is a simple example which uses rubyzip to
@ -21,7 +23,7 @@ class ZipFileGenerator
def write
entries = Dir.entries(@input_dir) - %w[. ..]
::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
::Zip::File.open(@output_file, create: true) do |zipfile|
write_entries entries, '', zipfile
end
end

View File

@ -1,4 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH << '../lib'
@ -42,7 +43,8 @@ class MainApp < Gtk::Window
end
class ButtonPanel < Gtk::HButtonBox
attr_reader :open_button, :extract_button
attr_reader :extract_button, :open_button
def initialize
super
set_layout(Gtk::BUTTONBOX_START)
@ -72,7 +74,7 @@ class MainApp < Gtk::Window
@zipfile.each do |entry|
@clist.append([entry.name,
entry.size.to_s,
(100.0 * entry.compressedSize / entry.size).to_s + '%'])
"#{100.0 * entry.compressedSize / entry.size}%"])
end
end
end

View File

@ -1,4 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$VERBOSE = true
@ -6,7 +7,7 @@ $LOAD_PATH << '../lib'
require 'Qt'
system('rbuic -o zipdialogui.rb zipdialogui.ui')
require 'zipdialogui.rb'
require 'zipdialogui'
require 'zip'
a = Qt::Application.new(ARGV)
@ -65,14 +66,14 @@ class ZipDialog < ZipDialogUI
end
puts "selected_items.size = #{selected_items.size}"
puts "unselected_items.size = #{unselected_items.size}"
items = !selected_items.empty? ? selected_items : unselected_items
items = selected_items.empty? ? unselected_items : selected_items
puts "items.size = #{items.size}"
d = Qt::FileDialog.get_existing_directory(nil, self)
if !d
puts 'No directory chosen'
else
if d
zipfile { |zf| items.each { |e| zf.extract(e, File.join(d, e)) } }
else
puts 'No directory chosen'
end
end

View File

@ -1,10 +1,11 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH << '../lib'
require 'zip'
::Zip::OutputStream.open('simple.zip') do |zos|
Zip::OutputStream.open('simple.zip') do |zos|
zos.put_next_entry 'entry.txt'
zos.puts 'Hello world'
end

View File

@ -1,4 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$VERBOSE = true

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class BasicZipFileTest < MiniTest::Test
@ -8,44 +10,35 @@ class BasicZipFileTest < MiniTest::Test
end
def test_entries
assert_equal(TestZipFile::TEST_ZIP2.entry_names.sort,
@zip_file.entries.entries.sort.map(&:name))
expected_entry_names = TestZipFile::TEST_ZIP2.entry_names
actual_entry_names = @zip_file.entries.entries.map(&:name)
assert_equal(expected_entry_names.sort, actual_entry_names.sort)
end
def test_each
count = 0
visited = {}
@zip_file.each do |entry|
assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name))
assert(!visited.include?(entry.name))
visited[entry.name] = nil
count = count.succ
end
assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count)
expected_entry_names = TestZipFile::TEST_ZIP2.entry_names
actual_entry_names = []
@zip_file.each { |entry| actual_entry_names << entry.name }
assert_equal(expected_entry_names.sort, actual_entry_names.sort)
end
def test_foreach
count = 0
visited = {}
::Zip::File.foreach(TestZipFile::TEST_ZIP2.zip_name) do |entry|
assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name))
assert(!visited.include?(entry.name))
visited[entry.name] = nil
count = count.succ
end
assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count)
expected_entry_names = TestZipFile::TEST_ZIP2.entry_names
actual_entry_names = []
::Zip::File.foreach(TestZipFile::TEST_ZIP2.zip_name) { |entry| actual_entry_names << entry.name }
assert_equal(expected_entry_names.sort, actual_entry_names.sort)
end
def test_get_input_stream
count = 0
visited = {}
expected_entry_names = TestZipFile::TEST_ZIP2.entry_names
actual_entry_names = []
@zip_file.each do |entry|
actual_entry_names << entry.name
assert_entry(entry.name, @zip_file.get_input_stream(entry), entry.name)
assert(!visited.include?(entry.name))
visited[entry.name] = nil
count = count.succ
end
assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count)
assert_equal(expected_entry_names.sort, actual_entry_names.sort)
end
def test_get_input_stream_block

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class Bzip2SupportTest < MiniTest::Test
@ -5,7 +7,12 @@ class Bzip2SupportTest < MiniTest::Test
def test_read
Zip::InputStream.open(BZIP2_ZIP_TEST_FILE) do |zis|
assert_raises(Zip::CompressionMethodError) { zis.get_next_entry }
error = assert_raises(Zip::CompressionMethodError) do
zis.get_next_entry
end
assert_equal(12, error.compression_method)
assert_match(/BZIP2/, error.message)
end
end
end

View File

@ -1,21 +1,25 @@
# frozen_string_literal: true
require 'test_helper'
class ZipCaseSensitivityTest < MiniTest::Test
include CommonZipFileFixture
SRC_FILES = [['test/data/file1.txt', 'testfile.rb'],
['test/data/file2.txt', 'testFILE.rb']]
SRC_FILES = [
['test/data/file1.txt', 'testfile.rb'],
['test/data/file2.txt', 'testFILE.rb']
].freeze
def teardown
::Zip.case_insensitive_match = false
::Zip.reset!
end
# Ensure that everything functions normally when +case_insensitive_match = false+
def test_add_case_sensitive
::Zip.case_insensitive_match = false
SRC_FILES.each { |fn, _en| assert(::File.exist?(fn)) }
zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
SRC_FILES.each { |(fn, _en)| assert(::File.exist?(fn)) }
zf = ::Zip::File.new(EMPTY_FILENAME, create: true)
SRC_FILES.each { |fn, en| zf.add(en, fn) }
zf.close
@ -33,20 +37,21 @@ class ZipCaseSensitivityTest < MiniTest::Test
def test_add_case_insensitive
::Zip.case_insensitive_match = true
SRC_FILES.each { |fn, _en| assert(::File.exist?(fn)) }
zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
SRC_FILES.each { |(fn, _en)| assert(::File.exist?(fn)) }
zf = ::Zip::File.new(EMPTY_FILENAME, create: true)
assert_raises Zip::EntryExistsError do
error = assert_raises Zip::EntryExistsError do
SRC_FILES.each { |fn, en| zf.add(en, fn) }
end
assert_match(/'add'/, error.message)
end
# Ensure that names are treated case insensitively when reading files and +case_insensitive_match = true+
def test_add_case_sensitive_read_case_insensitive
::Zip.case_insensitive_match = false
SRC_FILES.each { |fn, _en| assert(::File.exist?(fn)) }
zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
SRC_FILES.each { |(fn, _en)| assert(::File.exist?(fn)) }
zf = ::Zip::File.new(EMPTY_FILENAME, create: true)
SRC_FILES.each { |fn, en| zf.add(en, fn) }
zf.close

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class ZipCentralDirectoryEntryTest < MiniTest::Test
@ -57,13 +59,43 @@ class ZipCentralDirectoryEntryTest < MiniTest::Test
end
end
def test_read_entry_from_truncated_zip_file
fragment = ''
File.open('test/data/testDirectory.bin') { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes
fragment.extend(IOizeString)
entry = ::Zip::Entry.new
entry.read_c_dir_entry(fragment)
raise 'ZipError expected'
rescue ::Zip::Error
def test_read_entry_from_truncated_zip_file_raises_error
File.open('test/data/testDirectory.bin') do |f|
# cdir entry header is at least 46 bytes, so just read a bit.
fragment = f.read(12)
assert_raises(::Zip::Error) do
entry = ::Zip::Entry.new
entry.read_c_dir_entry(StringIO.new(fragment))
end
end
end
def test_read_entry_from_truncated_zip_file_returns_nil
File.open('test/data/testDirectory.bin') do |f|
# cdir entry header is at least 46 bytes, so just read a bit.
fragment = f.read(12)
assert_nil(::Zip::Entry.read_c_dir_entry(StringIO.new(fragment)))
end
end
def test_read_corrupted_entry_raises_error
fragment = File.binread('test/data/testDirectory.bin')
fragment.slice!(12)
io = StringIO.new(fragment)
assert_raises(::Zip::Error) do
entry = ::Zip::Entry.new
entry.read_c_dir_entry(io)
# First entry will be read but break later entries.
entry.read_c_dir_entry(io)
end
end
def test_read_corrupted_entry_returns_nil
fragment = File.binread('test/data/testDirectory.bin')
fragment.slice!(12)
io = StringIO.new(fragment)
refute_nil(::Zip::Entry.read_c_dir_entry(io))
# First entry will be read but break later entries.
assert_nil(::Zip::Entry.read_c_dir_entry(io))
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class ZipCentralDirectoryTest < MiniTest::Test
@ -7,12 +9,11 @@ class ZipCentralDirectoryTest < MiniTest::Test
def test_read_from_stream
::File.open(TestZipFile::TEST_ZIP2.zip_name, 'rb') do |zip_file|
cdir = ::Zip::CentralDirectory.read_from_stream(zip_file)
cdir = ::Zip::CentralDirectory.new
cdir.read_from_stream(zip_file)
assert_equal(TestZipFile::TEST_ZIP2.entry_names.size, cdir.size)
assert(cdir.entries.sort.compare_enumerables(TestZipFile::TEST_ZIP2.entry_names.sort) do |cdir_entry, test_entry_name|
cdir_entry.name == test_entry_name
end)
assert_equal(cdir.entries.map(&:name).sort, TestZipFile::TEST_ZIP2.entry_names.sort)
assert_equal(TestZipFile::TEST_ZIP2.comment, cdir.comment)
end
end
@ -26,21 +27,52 @@ class ZipCentralDirectoryTest < MiniTest::Test
rescue ::Zip::Error
end
def test_read_from_truncated_zip_file
fragment = ''
File.open('test/data/testDirectory.bin', 'rb') { |f| fragment = f.read }
fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete
fragment.extend(IOizeString)
entry = ::Zip::CentralDirectory.new
entry.read_from_stream(fragment)
raise 'ZipError expected'
rescue ::Zip::Error
def test_read_eocd_with_wrong_cdir_offset_from_file
::File.open('test/data/testDirectory.bin', 'rb') do |f|
assert_raises(::Zip::Error) do
cdir = ::Zip::CentralDirectory.new
cdir.read_from_stream(f)
end
end
end
def test_read_eocd_with_wrong_cdir_offset_from_buffer
::File.open('test/data/testDirectory.bin', 'rb') do |f|
assert_raises(::Zip::Error) do
cdir = ::Zip::CentralDirectory.new
cdir.read_from_stream(StringIO.new(f.read))
end
end
end
def test_count_entries
[
['test/data/osx-archive.zip', 4],
['test/data/zip64-sample.zip', 2],
['test/data/max_length_file_comment.zip', 1],
['test/data/100000-files.zip', 100_000]
].each do |filename, num_entries|
cdir = ::Zip::CentralDirectory.new
::File.open(filename, 'rb') do |f|
assert_equal(num_entries, cdir.count_entries(f))
f.seek(0)
s = StringIO.new(f.read)
assert_equal(num_entries, cdir.count_entries(s))
end
end
end
def test_write_to_stream
entries = [::Zip::Entry.new('file.zip', 'flimse', 'myComment', 'somethingExtra'),
::Zip::Entry.new('file.zip', 'secondEntryName'),
::Zip::Entry.new('file.zip', 'lastEntry.txt', 'Has a comment too')]
entries = [
::Zip::Entry.new(
'file.zip', 'flimse',
comment: 'myComment', extra: 'somethingExtra'
),
::Zip::Entry.new('file.zip', 'secondEntryName'),
::Zip::Entry.new('file.zip', 'lastEntry.txt', comment: 'Has a comment')
]
cdir = ::Zip::CentralDirectory.new(entries, 'my zip comment')
File.open('test/data/generated/cdirtest.bin', 'wb') do |f|
@ -55,50 +87,60 @@ class ZipCentralDirectoryTest < MiniTest::Test
assert_equal(cdir.entries.sort, cdir_readback.entries.sort)
end
def test_write64_to_stream
::Zip.write_zip64_support = true
entries = [::Zip::Entry.new('file.zip', 'file1-little', 'comment1', '', 200, 101, ::Zip::Entry::STORED, 200),
::Zip::Entry.new('file.zip', 'file2-big', 'comment2', '', 18_000_000_000, 102, ::Zip::Entry::DEFLATED, 20_000_000_000),
::Zip::Entry.new('file.zip', 'file3-alsobig', 'comment3', '', 15_000_000_000, 103, ::Zip::Entry::DEFLATED, 21_000_000_000),
::Zip::Entry.new('file.zip', 'file4-little', 'comment4', '', 100, 104, ::Zip::Entry::DEFLATED, 121)]
[0, 250, 18_000_000_300, 33_000_000_350].each_with_index do |offset, index|
entries[index].local_header_offset = offset
def test_write64_to_stream_65536_entries
skip unless ENV['FULL_ZIP64_TEST']
entries = []
0x10000.times do |i|
entries << Zip::Entry.new('file.zip', "#{i}.txt")
end
cdir = ::Zip::CentralDirectory.new(entries, 'zip comment')
cdir = Zip::CentralDirectory.new(entries)
File.open('test/data/generated/cdir64test.bin', 'wb') do |f|
cdir.write_to_stream(f)
end
cdir_readback = ::Zip::CentralDirectory.new
cdir_readback = Zip::CentralDirectory.new
File.open('test/data/generated/cdir64test.bin', 'rb') do |f|
cdir_readback.read_from_stream(f)
end
assert_equal(cdir.entries.sort, cdir_readback.entries.sort)
assert_equal(::Zip::VERSION_NEEDED_TO_EXTRACT_ZIP64, cdir_readback.instance_variable_get(:@version_needed_for_extract))
assert_equal(0x10000, cdir_readback.size)
assert_equal(Zip::VERSION_NEEDED_TO_EXTRACT_ZIP64, cdir_readback.instance_variable_get(:@version_needed_for_extract))
end
def test_equality
cdir1 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
'somethingExtra'),
::Zip::Entry.new('file.zip', 'secondEntryName'),
::Zip::Entry.new('file.zip', 'lastEntry.txt')],
'my zip comment')
cdir2 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
'somethingExtra'),
::Zip::Entry.new('file.zip', 'secondEntryName'),
::Zip::Entry.new('file.zip', 'lastEntry.txt')],
'my zip comment')
cdir3 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
'somethingExtra'),
::Zip::Entry.new('file.zip', 'secondEntryName'),
::Zip::Entry.new('file.zip', 'lastEntry.txt')],
'comment?')
cdir4 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
'somethingExtra'),
::Zip::Entry.new('file.zip', 'lastEntry.txt')],
'comment?')
cdir1 = ::Zip::CentralDirectory.new(
[
::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
::Zip::Entry.new('file.zip', 'secondEntryName'),
::Zip::Entry.new('file.zip', 'lastEntry.txt')
],
'my zip comment'
)
cdir2 = ::Zip::CentralDirectory.new(
[
::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
::Zip::Entry.new('file.zip', 'secondEntryName'),
::Zip::Entry.new('file.zip', 'lastEntry.txt')
],
'my zip comment'
)
cdir3 = ::Zip::CentralDirectory.new(
[
::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
::Zip::Entry.new('file.zip', 'secondEntryName'),
::Zip::Entry.new('file.zip', 'lastEntry.txt')
],
'comment?'
)
cdir4 = ::Zip::CentralDirectory.new(
[
::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
::Zip::Entry.new('file.zip', 'lastEntry.txt')
],
'comment?'
)
assert_equal(cdir1, cdir1)
assert_equal(cdir1, cdir2)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class ConstantsTest < MiniTest::Test

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class NullEncrypterTest < MiniTest::Test

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class TraditionalEncrypterTest < MiniTest::Test

2
test/data/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
file1.txt eol=lf
file2.txt eol=lf

BIN
test/data/100000-files.zip Normal file

Binary file not shown.

Binary file not shown.

BIN
test/data/invalid-split.zip Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
class NotZippedRuby
def return_true

BIN
test/data/osx-archive.zip Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class DecompressorTest < MiniTest::Test
TEST_COMPRESSION_METHOD = 255

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class DeflaterTest < MiniTest::Test
@ -8,6 +10,10 @@ class DeflaterTest < MiniTest::Test
DEFAULT_COMP_FILE = 'test/data/generated/compressiontest_default_compression.bin'
NO_COMP_FILE = 'test/data/generated/compressiontest_no_compression.bin'
def teardown
Zip.reset!
end
def test_output_operator
txt = load_file('test/data/file2.txt')
deflate(txt, DEFLATER_TEST_FILE)
@ -43,7 +49,7 @@ class DeflaterTest < MiniTest::Test
private
def load_file(filename)
File.open(filename, 'rb', &:read)
File.binread(filename)
end
def deflate(data, filename)

86
test/dos_time_test.rb Normal file
View File

@ -0,0 +1,86 @@
# frozen_string_literal: true
require 'test_helper'
require 'zip/dos_time'
class DOSTimeTest < MiniTest::Test
def setup
@dos_time = Zip::DOSTime.new(2022, 1, 1, 12, 0, 0)
end
def test_new
dos_time = Zip::DOSTime.new
assert(dos_time.absolute_time?)
dos_time = Zip::DOSTime.new(2022, 1, 1, 12, 0, 0)
assert(dos_time.absolute_time?)
dos_time = Zip::DOSTime.new(2022, 1, 1, 12, 0, 0, 0)
assert(dos_time.absolute_time?)
end
def test_now
dos_time = Zip::DOSTime.now
assert(dos_time.absolute_time?)
end
def test_utc
dos_time = Zip::DOSTime.utc(2022, 1, 1, 12, 0, 0)
assert(dos_time.absolute_time?)
end
def test_gm
dos_time = Zip::DOSTime.gm(2022, 1, 1, 12, 0, 0)
assert(dos_time.absolute_time?)
end
def test_mktime
dos_time = Zip::DOSTime.mktime(2022, 1, 1, 12, 0, 0)
assert(dos_time.absolute_time?)
end
def test_from_time
time = Time.new(2022, 1, 1, 12, 0, 0)
dos_time = Zip::DOSTime.from_time(time)
assert_equal(@dos_time, dos_time)
assert(dos_time.absolute_time?)
end
def test_parse_binary_dos_format
bin_dos_date = 0b101010000100001
bin_dos_time = 0b110000000000000
dos_time = Zip::DOSTime.parse_binary_dos_format(bin_dos_date, bin_dos_time)
assert_equal(@dos_time, dos_time)
refute(dos_time.absolute_time?)
end
def test_at
time = Time.at(1_641_038_400)
dos_time = Zip::DOSTime.at(1_641_038_400)
assert_equal(time, dos_time)
assert(dos_time.absolute_time?)
end
def test_local
dos_time = Zip::DOSTime.local(2022, 1, 1, 12, 0, 0)
assert(dos_time.absolute_time?)
end
def test_comparison
time = Time.new(2022, 1, 1, 12, 0, 0)
assert_equal(0, @dos_time <=> time)
end
def test_jruby_cmp
return unless defined? JRUBY_VERSION && Gem::Version.new(JRUBY_VERSION) < '9.2.18.0'
time = Time.new(2022, 1, 1, 12, 0, 0)
assert(@dos_time == time)
assert(@dos_time <= time)
assert(@dos_time >= time)
time = Time.new(2022, 1, 1, 12, 1, 1)
assert(time > @dos_time)
assert(@dos_time < time)
end
end

View File

@ -1,42 +1,68 @@
# frozen_string_literal: true
require 'test_helper'
class EncryptionTest < MiniTest::Test
ENCRYPT_ZIP_TEST_FILE = 'test/data/zipWithEncryption.zip'
INPUT_FILE1 = 'test/data/file1.txt'
INPUT_FILE2 = 'test/data/file2.txt'
def setup
@default_compression = Zip.default_compression
Zip.default_compression = ::Zlib::DEFAULT_COMPRESSION
end
def teardown
Zip.default_compression = @default_compression
Zip.reset!
end
def test_encrypt
test_file = ::File.open(ENCRYPT_ZIP_TEST_FILE, 'rb').read
content = File.read(INPUT_FILE1)
test_filename = 'top_secret_file.txt'
@rand = [250, 143, 107, 13, 143, 22, 155, 75, 228, 150, 12]
@output = ::Zip::DOSTime.stub(:now, ::Zip::DOSTime.new(2014, 12, 17, 15, 56, 24)) do
Random.stub(:rand, ->(_range) { @rand.shift }) do
Zip::OutputStream.write_buffer(::StringIO.new(''), Zip::TraditionalEncrypter.new('password')) do |zos|
zos.put_next_entry('file1.txt')
zos.write ::File.open(INPUT_FILE1).read
end.string
password = 'swordfish'
encrypted_zip = Zip::OutputStream.write_buffer(
::StringIO.new,
encrypter: Zip::TraditionalEncrypter.new(password)
) do |out|
out.put_next_entry(test_filename)
out.write content
end
Zip::InputStream.open(
encrypted_zip, decrypter: Zip::TraditionalDecrypter.new(password)
) do |zis|
entry = zis.get_next_entry
assert_equal test_filename, entry.name
assert_equal 1_327, entry.size
assert_equal content, zis.read
end
error = assert_raises(Zip::DecompressionError) do
Zip::InputStream.open(
encrypted_zip,
decrypter: Zip::TraditionalDecrypter.new("#{password}wrong")
) do |zis|
zis.get_next_entry
assert_equal content, zis.read
end
end
@output.unpack('C*').each_with_index do |c, i|
assert_equal test_file[i].ord, c
end
assert_match(/Zlib error \('.+'\) while inflating\./, error.message)
end
def test_decrypt
Zip::InputStream.open(ENCRYPT_ZIP_TEST_FILE, 0, Zip::TraditionalDecrypter.new('password')) do |zis|
Zip::InputStream.open(
ENCRYPT_ZIP_TEST_FILE,
decrypter: Zip::TraditionalDecrypter.new('password')
) do |zis|
entry = zis.get_next_entry
assert_equal 'file1.txt', entry.name
assert_equal 1327, entry.size
assert_equal ::File.open(INPUT_FILE1, 'r').read, zis.read
assert_equal 1_327, entry.size
assert_equal ::File.read(INPUT_FILE1), zis.read
entry = zis.get_next_entry
assert_equal 'file2.txt', entry.name
assert_equal 41_234, entry.size
assert_equal ::File.read(INPUT_FILE2), zis.read
end
end
end

View File

@ -1,14 +1,16 @@
# frozen_string_literal: true
require 'test_helper'
class ZipEntrySetTest < MiniTest::Test
ZIP_ENTRIES = [
::Zip::Entry.new('zipfile.zip', 'name1', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name3', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name2', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name4', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name5', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name6', 'comment1')
]
::Zip::Entry.new('zipfile.zip', 'name1', comment: 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name3', comment: 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name2', comment: 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name4', comment: 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name5', comment: 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name6', comment: 'comment1')
].freeze
def setup
@zip_entry_set = ::Zip::EntrySet.new(ZIP_ENTRIES)
@ -20,13 +22,17 @@ class ZipEntrySetTest < MiniTest::Test
def test_include
assert(@zip_entry_set.include?(ZIP_ENTRIES.first))
assert(!@zip_entry_set.include?(::Zip::Entry.new('different.zip', 'different', 'aComment')))
assert(
!@zip_entry_set.include?(
::Zip::Entry.new('different.zip', 'different', comment: 'aComment')
)
)
end
def test_size
assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size)
assert_equal(ZIP_ENTRIES.size, @zip_entry_set.length)
@zip_entry_set << ::Zip::Entry.new('a', 'b', 'c')
@zip_entry_set << ::Zip::Entry.new('a', 'b', comment: 'c')
assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.length)
end
@ -54,11 +60,19 @@ class ZipEntrySetTest < MiniTest::Test
def test_each
# Used each instead each_with_index due the bug in jRuby
count = 0
new_size = 200
@zip_entry_set.each do |entry|
assert(ZIP_ENTRIES.include?(entry))
entry.clean_up # Start from a "saved" state.
entry.size = new_size # Check that entries can be changed in this block.
count += 1
end
assert_equal(ZIP_ENTRIES.size, count)
@zip_entry_set.each do |entry|
assert_equal(new_size, entry.size)
assert(entry.dirty?) # Size was changed.
end
end
def test_entries
@ -66,7 +80,9 @@ class ZipEntrySetTest < MiniTest::Test
end
def test_find_entry
entries = [::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', 'comment1')]
entries = [
::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', comment: 'comment1')
]
::Zip.case_insensitive_match = true
zip_entry_set = ::Zip::EntrySet.new(entries)
@ -93,10 +109,20 @@ class ZipEntrySetTest < MiniTest::Test
arr << entry
end
assert_equal(ZIP_ENTRIES.sort, arr)
# Ensure `each` above hasn't permanently altered the ordering.
::Zip.sort_entries = false
arr = []
@zip_entry_set.each do |entry|
arr << entry
end
assert_equal(ZIP_ENTRIES, arr)
end
def test_compound
new_entry = ::Zip::Entry.new('zf.zip', 'new entry', "new entry's comment")
new_entry = ::Zip::Entry.new(
'zf.zip', 'new entry', comment: "new entry's comment"
)
assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size)
@zip_entry_set << new_entry
assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.size)

View File

@ -1,18 +1,23 @@
# frozen_string_literal: true
require 'test_helper'
class ZipEntryTest < MiniTest::Test
include ZipEntryData
def teardown
::Zip.reset!
end
def test_constructor_and_getters
entry = ::Zip::Entry.new(TEST_ZIPFILE,
TEST_NAME,
TEST_COMMENT,
TEST_EXTRA,
TEST_COMPRESSED_SIZE,
TEST_CRC,
TEST_COMPRESSIONMETHOD,
TEST_SIZE,
TEST_TIME)
entry = ::Zip::Entry.new(
TEST_ZIPFILE, TEST_NAME,
comment: TEST_COMMENT, extra: TEST_EXTRA,
compressed_size: TEST_COMPRESSED_SIZE,
crc: TEST_CRC, size: TEST_SIZE, time: TEST_TIME,
compression_method: TEST_COMPRESSIONMETHOD,
compression_level: TEST_COMPRESSIONLEVEL
)
assert_equal(TEST_COMMENT, entry.comment)
assert_equal(TEST_COMPRESSED_SIZE, entry.compressed_size)
@ -21,7 +26,10 @@ class ZipEntryTest < MiniTest::Test
assert_equal(TEST_COMPRESSIONMETHOD, entry.compression_method)
assert_equal(TEST_NAME, entry.name)
assert_equal(TEST_SIZE, entry.size)
assert_equal(TEST_TIME, entry.time)
# Reverse times when testing because we need to use DOSTime#== for the
# comparison, not Time#==.
assert_equal(entry.time, TEST_TIME)
end
def test_is_directory_and_is_file
@ -39,30 +47,54 @@ class ZipEntryTest < MiniTest::Test
end
def test_equality
entry1 = ::Zip::Entry.new('file.zip', 'name', 'isNotCompared',
'something extra', 123, 1234,
::Zip::Entry::DEFLATED, 10_000)
entry2 = ::Zip::Entry.new('file.zip', 'name', 'isNotComparedXXX',
'something extra', 123, 1234,
::Zip::Entry::DEFLATED, 10_000)
entry3 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
'something extra', 123, 1234,
::Zip::Entry::DEFLATED, 10_000)
entry4 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
'something extraXX', 123, 1234,
::Zip::Entry::DEFLATED, 10_000)
entry5 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
'something extraXX', 12, 1234,
::Zip::Entry::DEFLATED, 10_000)
entry6 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
'something extraXX', 12, 123,
::Zip::Entry::DEFLATED, 10_000)
entry7 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
'something extraXX', 12, 123,
::Zip::Entry::STORED, 10_000)
entry8 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
'something extraXX', 12, 123,
::Zip::Entry::STORED, 100_000)
entry1 = ::Zip::Entry.new(
'file.zip', 'name',
comment: 'isNotCompared', extra: 'something extra',
compressed_size: 123, crc: 1234, size: 10_000
)
entry2 = ::Zip::Entry.new(
'file.zip', 'name',
comment: 'isNotComparedXXX', extra: 'something extra',
compressed_size: 123, crc: 1234, size: 10_000
)
entry3 = ::Zip::Entry.new(
'file.zip', 'name2',
comment: 'isNotComparedXXX', extra: 'something extra',
compressed_size: 123, crc: 1234, size: 10_000
)
entry4 = ::Zip::Entry.new(
'file.zip', 'name2',
comment: 'isNotComparedXXX', extra: 'something extraXX',
compressed_size: 123, crc: 1234, size: 10_000
)
entry5 = ::Zip::Entry.new(
'file.zip', 'name2',
comment: 'isNotComparedXXX', extra: 'something extraXX',
compressed_size: 12, crc: 1234, size: 10_000
)
entry6 = ::Zip::Entry.new(
'file.zip', 'name2',
comment: 'isNotComparedXXX', extra: 'something extraXX',
compressed_size: 12, crc: 123, size: 10_000
)
entry7 = ::Zip::Entry.new(
'file.zip', 'name2', comment: 'isNotComparedXXX',
extra: 'something extraXX', compressed_size: 12, crc: 123, size: 10_000,
compression_method: ::Zip::Entry::STORED
)
entry8 = ::Zip::Entry.new(
'file.zip', 'name2',
comment: 'isNotComparedXXX', extra: 'something extraXX',
compressed_size: 12, crc: 123, size: 100_000,
compression_method: ::Zip::Entry::STORED
)
assert_equal(entry1, entry1)
assert_equal(entry1, entry2)
@ -118,38 +150,52 @@ class ZipEntryTest < MiniTest::Test
end
def test_entry_name_cannot_start_with_slash
assert_raises(::Zip::EntryNameError) { ::Zip::Entry.new('zf.zip', '/hej/der') }
error = assert_raises(::Zip::EntryNameError) do
::Zip::Entry.new('zf.zip', '/hej/der')
end
assert_match(/'\/hej\/der'/, error.message)
end
def test_entry_name_cannot_be_too_long
name = 'a' * 65_535
::Zip::Entry.new('', name) # Should not raise anything.
error = assert_raises(::Zip::EntryNameError) do
::Zip::Entry.new('', "a#{name}")
end
assert_match(/65,536/, error.message)
end
def test_store_file_without_compression
File.delete('/tmp/no_compress.zip') if File.exist?('/tmp/no_compress.zip')
files = Dir[File.join('test/data/globTest', '**', '**')]
Dir.mktmpdir do |tmp|
tmp_zip = File.join(tmp, 'no_compress.zip')
Zip.setup do |z|
z.write_zip64_support = false
Zip.setup do |z|
z.write_zip64_support = false
end
zipfile = Zip::File.open(tmp_zip, create: true)
mimetype_entry = Zip::Entry.new(
zipfile, # @zipfile
'mimetype', # @name
compression_method: Zip::Entry::STORED
)
zipfile.add(mimetype_entry, 'test/data/mimetype')
files = Dir[File.join('test/data/globTest', '**', '**')]
files.each do |file|
zipfile.add(file.sub('test/data/globTest/', ''), file)
end
zipfile.close
f = File.open(tmp_zip, 'rb')
first_100_bytes = f.read(100)
f.close
assert_match(/mimetypeapplication\/epub\+zip/, first_100_bytes)
end
zipfile = Zip::File.open('/tmp/no_compress.zip', Zip::File::CREATE)
mimetype_entry = Zip::Entry.new(zipfile, # @zipfile
'mimetype', # @name
'', # @comment
'', # @extra
0, # @compressed_size
0, # @crc
Zip::Entry::STORED) # @comppressed_method
zipfile.add(mimetype_entry, 'test/data/mimetype')
files.each do |file|
zipfile.add(file.sub('test/data/globTest/', ''), file)
end
zipfile.close
f = File.open('/tmp/no_compress.zip', 'rb')
first_100_bytes = f.read(100)
f.close
assert_match(/mimetypeapplication\/epub\+zip/, first_100_bytes)
end
def test_encrypted?
@ -169,4 +215,137 @@ class ZipEntryTest < MiniTest::Test
entry.gp_flags = 0
assert_equal(false, entry.incomplete?)
end
def test_compression_level_flags
[
[Zip.default_compression, 0],
[0, 0],
[1, 6],
[2, 4],
[3, 0],
[7, 0],
[8, 2],
[9, 2]
].each do |level, flags|
# Check flags are set correctly when DEFLATED is (implicitly) specified.
e_def = Zip::Entry.new(
'', '',
compression_level: level
)
assert_equal(flags, e_def.gp_flags & 0b110)
# Check that flags are not set when STORED is specified.
e_sto = Zip::Entry.new(
'', '',
compression_method: Zip::Entry::STORED,
compression_level: level
)
assert_equal(0, e_sto.gp_flags & 0b110)
end
# Check that a directory entry's flags are not set, even if DEFLATED
# is specified.
e_dir = Zip::Entry.new(
'', 'd/', compression_method: Zip::Entry::DEFLATED, compression_level: 1
)
assert_equal(0, e_dir.gp_flags & 0b110)
end
def test_compression_method_reader
[
[Zip.default_compression, Zip::Entry::DEFLATED],
[0, Zip::Entry::STORED],
[1, Zip::Entry::DEFLATED],
[9, Zip::Entry::DEFLATED]
].each do |level, method|
# Check that the correct method is returned when DEFLATED is specified.
entry = Zip::Entry.new(compression_level: level)
assert_equal(method, entry.compression_method)
end
# Check that the correct method is returned when STORED is specified.
entry = Zip::Entry.new(
compression_method: Zip::Entry::STORED, compression_level: 1
)
assert_equal(Zip::Entry::STORED, entry.compression_method)
# Check that directories are always STORED, whatever level is specified.
entry = Zip::Entry.new(
'', 'd/', compression_method: Zip::Entry::DEFLATED, compression_level: 1
)
assert_equal(Zip::Entry::STORED, entry.compression_method)
end
def test_set_time_as_dos_time
entry = ::Zip::Entry.new
assert(entry.time.kind_of?(::Zip::DOSTime))
entry.time = Time.now
assert(entry.time.kind_of?(::Zip::DOSTime))
entry.time = ::Zip::DOSTime.now
assert(entry.time.kind_of?(::Zip::DOSTime))
end
def test_atime
entry = ::Zip::Entry.new
time = Time.new(1999, 12, 31, 23, 59, 59)
entry.atime = time
assert(entry.dirty?)
assert_equal(::Zip::DOSTime.from_time(time), entry.atime)
refute_equal(entry.time, entry.atime)
assert(entry.atime.kind_of?(::Zip::DOSTime))
assert_nil(entry.ctime)
end
def test_ctime
entry = ::Zip::Entry.new
time = Time.new(1999, 12, 31, 23, 59, 59)
entry.ctime = time
assert(entry.dirty?)
assert_equal(::Zip::DOSTime.from_time(time), entry.ctime)
refute_equal(entry.time, entry.ctime)
assert(entry.ctime.kind_of?(::Zip::DOSTime))
assert_nil(entry.atime)
end
def test_mtime
entry = ::Zip::Entry.new
time = Time.new(1999, 12, 31, 23, 59, 59)
entry.mtime = time
assert(entry.dirty?)
assert_equal(::Zip::DOSTime.from_time(time), entry.mtime)
assert_equal(entry.time, entry.mtime)
assert(entry.mtime.kind_of?(::Zip::DOSTime))
assert_nil(entry.atime)
assert_nil(entry.ctime)
end
def test_time
entry = ::Zip::Entry.new
time = Time.new(1999, 12, 31, 23, 59, 59)
entry.time = time
assert(entry.dirty?)
assert_equal(::Zip::DOSTime.from_time(time), entry.time)
assert_equal(entry.mtime, entry.time)
assert(entry.time.kind_of?(::Zip::DOSTime))
assert_nil(entry.atime)
assert_nil(entry.ctime)
end
def test_ensure_entry_time_set_to_file_mtime
entry = ::Zip::Entry.new
entry.gather_fileinfo_from_srcpath('test/data/mimetype')
assert_equal(entry.time, File.stat('test/data/mimetype').mtime)
end
def test_absolute_time
entry = ::Zip::Entry.new
refute(entry.absolute_time?)
entry.time = Time.now
assert(entry.absolute_time?)
end
end

View File

@ -1,33 +0,0 @@
require 'test_helper'
class ErrorsTest < MiniTest::Test
def test_rescue_legacy_zip_error
raise ::Zip::Error
rescue ::Zip::ZipError
end
def test_rescue_legacy_zip_entry_exists_error
raise ::Zip::EntryExistsError
rescue ::Zip::ZipEntryExistsError
end
def test_rescue_legacy_zip_destination_file_exists_error
raise ::Zip::DestinationFileExistsError
rescue ::Zip::ZipDestinationFileExistsError
end
def test_rescue_legacy_zip_compression_method_error
raise ::Zip::CompressionMethodError
rescue ::Zip::ZipCompressionMethodError
end
def test_rescue_legacy_zip_entry_name_error
raise ::Zip::EntryNameError
rescue ::Zip::ZipEntryNameError
end
def test_rescue_legacy_zip_internal_error
raise ::Zip::InternalError
rescue ::Zip::ZipInternalError
end
end

View File

@ -1,30 +1,56 @@
# frozen_string_literal: true
require 'test_helper'
class ZipExtraFieldTest < MiniTest::Test
def test_new
extra_pure = ::Zip::ExtraField.new('')
extra_withstr = ::Zip::ExtraField.new('foo')
extra_withstr_local = ::Zip::ExtraField.new('foo', local: true)
assert_instance_of(::Zip::ExtraField, extra_pure)
assert_instance_of(::Zip::ExtraField, extra_withstr)
assert_instance_of(::Zip::ExtraField, extra_withstr_local)
assert_equal('foo', extra_withstr['Unknown'].to_c_dir_bin)
assert_equal('foo', extra_withstr_local['Unknown'].to_local_bin)
end
def test_unknownfield
extra = ::Zip::ExtraField.new('foo')
assert_equal(extra['Unknown'], 'foo')
assert_equal('foo', extra['Unknown'].to_c_dir_bin)
extra.merge('a')
assert_equal(extra['Unknown'], 'fooa')
assert_equal('fooa', extra['Unknown'].to_c_dir_bin)
extra.merge('barbaz')
assert_equal(extra.to_s, 'fooabarbaz')
assert_equal('fooabarbaz', extra['Unknown'].to_c_dir_bin)
extra.merge('bar', local: true)
assert_equal('bar', extra['Unknown'].to_local_bin)
assert_equal('fooabarbaz', extra['Unknown'].to_c_dir_bin)
end
def test_bad_header_id
str = "ut\x5\0\x3\250$\r@"
ut = nil
assert_output('', /WARNING/) do
ut = ::Zip::ExtraField::UniversalTime.new(str)
end
assert_instance_of(::Zip::ExtraField::UniversalTime, ut)
assert_nil(ut.mtime)
end
def test_ntfs
str = "\x0A\x00 \x00\x00\x00\x00\x00\x01\x00\x18\x00\xC0\x81\x17\xE8B\xCE\xCF\x01\xC0\x81\x17\xE8B\xCE\xCF\x01\xC0\x81\x17\xE8B\xCE\xCF\x01"
str = +"\x0A\x00 \x00\x00\x00\x00\x00\x01\x00\x18\x00\xC0\x81\x17\xE8B\xCE\xCF\x01\xC0\x81\x17\xE8B\xCE\xCF\x01\xC0\x81\x17\xE8B\xCE\xCF\x01"
extra = ::Zip::ExtraField.new(str)
assert(extra.member?('NTFS'))
t = ::Zip::DOSTime.at(1_410_496_497.405178)
assert_equal(t, extra['NTFS'].mtime)
assert_equal(t, extra['NTFS'].atime)
assert_equal(t, extra['NTFS'].ctime)
assert_equal(str.force_encoding('BINARY'), extra.to_local_bin)
end
def test_merge
@ -52,9 +78,9 @@ class ZipExtraFieldTest < MiniTest::Test
extra = ::Zip::ExtraField.new(str)
assert_instance_of(String, extra.to_s)
s = extra.to_s
extra.merge('foo')
assert_equal(s.length + 3, extra.to_s.length)
extra_len = extra.to_s.length
extra.merge('foo', local: true)
assert_equal(extra_len + 3, extra.to_s.length)
end
def test_equality
@ -73,4 +99,26 @@ class ZipExtraFieldTest < MiniTest::Test
extra1.create('IUnix')
assert_equal(extra1, extra3)
end
def test_read_local_extra_field
::Zip::File.open('test/data/local_extra_field.zip') do |zf|
['file1.txt', 'file2.txt'].each do |file|
entry = zf.get_entry(file)
assert_instance_of(::Zip::ExtraField, entry.extra)
assert_equal(1_000, entry.extra['IUnix'].uid)
assert_equal(1_000, entry.extra['IUnix'].gid)
end
end
end
def test_load_unknown_extra_field
::Zip::File.open('test/data/osx-archive.zip') do |zf|
zf.each do |entry|
# Check that there is only one occurance of the 'ux' extra field.
assert_equal(0, entry.extra['Unknown'].to_c_dir_bin.rindex('ux'))
assert_equal(0, entry.extra['Unknown'].to_local_bin.rindex('ux'))
end
end
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'test_helper'
class ZipExtraFieldUnknownTest < MiniTest::Test
def test_new
extra = ::Zip::ExtraField::Unknown.new
assert_empty(extra.to_c_dir_bin)
assert_empty(extra.to_local_bin)
end
def test_merge_cdir_then_local
extra = ::Zip::ExtraField::Unknown.new
field = "ux\v\x00\x01\x04\xF6\x01\x00\x00\x04\x14\x00\x00\x00"
extra.merge(field)
assert_empty(extra.to_local_bin)
assert_equal(field, extra.to_c_dir_bin)
extra.merge(field, local: true)
assert_equal(field, extra.to_local_bin)
assert_equal(field, extra.to_c_dir_bin)
end
def test_merge_local_only
extra = ::Zip::ExtraField::Unknown.new
field = "ux\v\x00\x01\x04\xF6\x01\x00\x00\x04\x14\x00\x00\x00"
extra.merge(field, local: true)
assert_equal(field, extra.to_local_bin)
assert_empty(extra.to_c_dir_bin)
end
def test_equality
extra1 = ::Zip::ExtraField::Unknown.new
extra2 = ::Zip::ExtraField::Unknown.new
assert_equal(extra1, extra2)
extra1.merge("ux\v\x00\x01\x04\xF6\x01\x00\x00\x04\x14\x00\x00\x00")
refute_equal(extra1, extra2)
extra2.merge("ux\v\x00\x01\x04\xF6\x01\x00\x00\x04\x14\x00\x00\x00")
assert_equal(extra1, extra2)
extra1.merge('foo', local: true)
refute_equal(extra1, extra2)
extra2.merge('foo', local: true)
assert_equal(extra1, extra2)
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class ZipExtraFieldUTTest < MiniTest::Test
@ -9,7 +11,7 @@ class ZipExtraFieldUTTest < MiniTest::Test
["UT\x09\x00\x05PS>APS>A", 0b101, true, false, false],
["UT\x09\x00\x06PS>APS>A", 0b110, false, false, true],
["UT\x13\x00\x07PS>APS>APS>A", 0b111, false, false, false]
]
].freeze
def test_parse
PARSE_TESTS.each do |bin, flags, a, c, m|

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class ZipFileExtractDirectoryTest < MiniTest::Test
@ -20,7 +22,7 @@ class ZipFileExtractDirectoryTest < MiniTest::Test
super
Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME
File.delete(TEST_OUT_NAME) if File.exist? TEST_OUT_NAME
FileUtils.rm_f(TEST_OUT_NAME)
end
def test_extract_directory
@ -36,7 +38,9 @@ class ZipFileExtractDirectoryTest < MiniTest::Test
def test_extract_directory_exists_as_file
File.open(TEST_OUT_NAME, 'w') { |f| f.puts 'something' }
assert_raises(::Zip::DestinationFileExistsError) { extract_test_dir }
assert_raises(::Zip::DestinationExistsError) do
extract_test_dir
end
end
def test_extract_directory_exists_as_file_overwrite
@ -44,7 +48,7 @@ class ZipFileExtractDirectoryTest < MiniTest::Test
called = false
extract_test_dir do |entry, dest_path|
called = true
assert_equal(TEST_OUT_NAME, dest_path)
assert_equal(File.absolute_path(TEST_OUT_NAME), dest_path)
assert(entry.directory?)
true
end

Some files were not shown because too many files have changed in this diff Show More