Compare commits

..

38 Commits
master ... 2.4

Author SHA1 Message Date
Robert Haines 6c4b7a9f90 Move to version `2.4.1` due to clash with `2.4`. 2025-01-05 18:15:02 +00:00
Geremia Taglialatela 3b4c2bfa22 Opt-in for MFA requirement explicitly on 2.4
As a pupular gem, `rubyzip` implicitly requires that all privileged
operations by any of the owners require OTP.

By explicitly setting `rubygems_mfa_required` metadata, the
gem will show "NEW VERSIONS REQUIRE MFA" and "VERSION PUBLISHED WITH
MFA" in the sidebar at https://rubygems.org/gems/rubyzip

NOTE: The explicit opt-in was introduced in the `master` branch via
1c06454, but has not been backported to `2.4`

Ref:
- https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html
- https://guides.rubygems.org/mfa-requirement-opt-in/
2025-01-05 18:00:28 +00:00
Robert Haines e3eb62491b Make sure version number is 2.4.0.
And add a test to keep version numbers consistent from now on.

Fixes: #623
2025-01-05 11:43:53 +00:00
Robert Haines c09352b546 Bump version and Changelog for release. 2025-01-04 10:12:55 +00:00
Robert Haines 71bb069049 Update actions with latest rubies.
Also, allow failure on 'head' versions.
2025-01-04 09:54:58 +00:00
Robert Haines bb06f99b14 Update actions dependencies. 2024-10-26 13:36:30 +01:00
Robert Haines 3d95a8204f Update earliest Ruby version for MacOS builds in CI. 2024-10-26 13:35:43 +01:00
Tsutomu Katsube 56954b0b59 Suppress "literal string will be frozen in the future" warning
Ref: https://bugs.ruby-lang.org/issues/20205

This patch will suppress the "literal string will be frozen in the
future" warning on the 2.4 branch.

Steps to reproduce
---

Run the following commands with ruby 3.4.0preview1:

```console
$ cd /tmp
$ git clone git@github.com:rubyzip/rubyzip.git
$ cd rubyzip
$ git switch 2.4
$ bundle install
$ ruby -I lib -W:deprecated -e '
require "zip"

Zip::File.open("test/data/ntfs.zip") do |zip_file|
  zip_file.each do |entry|
    puts entry.get_input_stream.read
  end
end
'
```

Expected result
---

```console
ntfs test
```

Actual result
---

```console
/private/tmp/rubyzip/lib/zip/extra_field/ntfs.rb:54: warning: literal string will be frozen in the future
ntfs test
```

Environment
---

```console
$ ruby -v
ruby 3.4.0preview1 (2024-05-16 master 9d69619623) [x86_64-darwin23]
```
2024-09-21 09:06:36 +09:00
Robert Haines 6ff40f7a78 Fix setting and restoring `RUBYZIP_V3_API_WARN` in tests.
Make sure that it is reset to its original value so that we can set it
up front for all tests.
2024-04-09 18:05:05 +01:00
Robert Haines e05dc9b978 Improve version 3 API messages.
The messages now tell you where you have called the changed method from.
2024-04-09 17:53:27 +01:00
Jean Boussier a4c3f5bddb Fix deprecation in Entry#get_input_stream 2024-04-09 16:37:04 +01:00
Jean Boussier 57cff3338f Fix `File#write_buffer` to always return the given `io`
Ref: ef89a62b70

This fixes a regression in 2.4.rc1.
2024-04-09 10:05:18 +01:00
Robert Haines 0001864cfe Bump version, Changelog and README for release. 2024-04-08 16:15:44 +01:00
Jean Boussier 385ebd054a Ensure compatibility with `--enable-frozen-string-literal`
Ref: https://bugs.ruby-lang.org/issues/20205

In Ruby 3.4 string literals will be "chilled" by default. Meaning
they are still mutable, but will pretend to be frozen.

In most case it has no impact, just emit a few warnings there and
there, but there is one thing it impacts is the `StringIO.new('')`
pattern. `StringIO` checks if the given string is frozen, and if
it is will act as a read only IO.

This breaks rubyzip 2.x.

This commit make the 2.x branch compatible with frozen string literals.
2024-04-08 15:10:07 +01:00
Robert Haines 46689d7350 Add `DOSTime` to the post_install message. 2024-04-08 14:29:10 +01:00
Robert Haines ef89a62b70 Ensure `File.open_buffer` doesn't rewrite unchanged data.
This is a backport of 14b63f68 from trunk.
2024-04-08 12:44:12 +01:00
Robert Haines 900db76760 Handle the `extract` methods in `Entry` and `File`.
Add v3 versions and warning messages to the v2 versions. We need to do
these methods like this because the new versions are very different.
2024-03-10 17:21:53 +00:00
Robert Haines 14efdd1cc4 Add `DOSTime#<=>` and a warning message to `DOSTime#dos_equals`.
`#dos_equals` now uses `#==` for its implementation.
2024-03-10 11:34:36 +00:00
Robert Haines 02faddaf44 Add warning messages to `File#get_output_stream`. 2024-03-10 09:00:17 +00:00
Robert Haines 9aa5964262 Add warning messages to the `File` class methods.
* `new`
 * `open`
 * `open_buffer`
2024-03-10 08:03:49 +00:00
Robert Haines 13f4ea766f Update gemspec. 2024-03-03 21:10:08 +00:00
Robert Haines 3910870f3a Remove Coveralls/Simplecov from this branch.
We don't use it in CI anyway.
2024-03-03 20:56:50 +00:00
Robert Haines 07d833ca78 Add warning messages to `InputStream.open`. 2024-03-03 18:06:51 +00:00
Robert Haines 6bbda380fe Add warning messages to deprectated methods.
* File.add_buffer
 * InputStream.open_buffer
2024-03-02 18:50:47 +00:00
Robert Haines a6e6c3c469 Update rubocop fails. 2024-03-02 18:38:00 +00:00
Robert Haines b691a4b72c Minor updates to the Actions workflow. 2024-03-02 08:59:52 +00:00
Robert Haines f68877920a Update File.split API to allow v3.0 calling style. 2024-03-02 08:23:27 +00:00
Robert Haines 13781b20d3 Add a warning when run on Ruby < 3.0. 2024-03-01 23:04:02 +00:00
Robert Haines 4b1cfba9db Add warning message (and tests) to OutputStream. 2024-03-01 18:05:06 +00:00
Robert Haines 5e9ee53c70 Add warning message to InputStream. 2024-03-01 18:05:06 +00:00
Robert Haines cb0505d735 Add a switchable warning message re the v3 API.
Switched via an environment variable, and tested.
2024-03-01 18:05:06 +00:00
Robert Haines 49950d924c Update Entry#new API to allow v3.0 calling style.
See https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x#new-1
for details.
2024-03-01 18:05:06 +00:00
Robert Haines a17da19e0c Update InputStream API to allow v3.0 calling style.
See https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x#zipinputstream
for details.
2024-03-01 18:05:06 +00:00
Robert Haines 7c1a8fbf8f Update OutputStream API to allow v3.0 calling style.
See https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x#zipoutputstream
for details.
2024-03-01 18:05:06 +00:00
Robert Haines 77611045f1 Switch to GitHub Actions on the `2.3.2` branch. 2022-02-06 14:31:04 +00:00
Robert Haines 2f1c1ea400 Move to using a post install message for 3.0 warning. 2021-07-05 20:13:36 +01:00
Robert Haines 16de339666 Print banner text re v3.0.0 when `zip` is required. 2021-07-03 12:12:22 +01:00
Robert Haines 84d7a66abc Bump version number and Changelog. 2021-07-03 12:10:30 +01:00
129 changed files with 2687 additions and 4042 deletions

View File

@ -1,19 +0,0 @@
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

View File

@ -8,15 +8,12 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu]
ruby: ['3.0', '3.1', '3.2', '3.3', '3.4', head, jruby, jruby-head, truffleruby, truffleruby-head]
ruby: ['2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', head, jruby, truffleruby]
include:
- { os: macos , ruby: '3.0' }
- { os: windows, ruby: '3.0' }
# head builds
- { os: windows, ruby: ucrt }
- { os: windows, ruby: mswin }
- os: macos
ruby: '2.6'
runs-on: ${{ matrix.os }}-latest
continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.os == 'windows' }}
continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
steps:
- name: Checkout rubyzip code
uses: actions/checkout@v4
@ -25,7 +22,7 @@ jobs:
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
rubygems: latest
rubygems: '3.2.3'
bundler-cache: true
- name: Run the tests
@ -35,13 +32,29 @@ jobs:
FULL_ZIP64_TEST: 1
run: bundle exec rake
- name: Coveralls
if: matrix.os == 'ubuntu' && !endsWith(matrix.ruby, 'head')
uses: coverallsapp/github-action@v2
test-frozen-string-literal:
strategy:
fail-fast: false
matrix:
os: [ubuntu]
ruby: ['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:
github-token: ${{ secrets.github_token }}
flag-name: ${{ matrix.ruby }}
parallel: true
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run the tests
env:
RUBYOPT: --enable-frozen-string-literal
FULL_ZIP64_TEST: 1
run: bundle exec rake
test-yjit:
strategy:
@ -66,13 +79,3 @@ jobs:
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,9 +4,7 @@
Gemfile.lock
+samples/*.zip
+samples/*.zip.*
samples/zipdialogui.rb
coverage
html/
pkg/
.ruby-gemset
.ruby-version

View File

@ -1,20 +1,9 @@
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:
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'
TargetRubyVersion: 2.4
Layout/HashAlignment:
EnforcedHashRocketStyle: table
@ -23,13 +12,10 @@ Layout/HashAlignment:
# Set a workable line length, given the current state of the code,
# and turn off for the tests.
Layout/LineLength:
Max: 100
Max: 135
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:
@ -37,11 +23,6 @@ Lint/SuppressedException:
Exclude:
- 'test/**/*.rb'
# Allow this "useless" test, as we are testing <=> here.
Lint/BinaryOperatorWithIdenticalOperands:
Exclude:
- 'test/entry_test.rb'
# Turn off ABC metrics for the tests and set a workable max given
# the current state of the code.
Metrics/AbcSize:
@ -49,10 +30,9 @@ Metrics/AbcSize:
Exclude:
- 'test/**/*.rb'
# Turn block length metrics off for the tests and gemspec.
# Turn block length metrics off for the tests.
Metrics/BlockLength:
Exclude:
- 'rubyzip.gemspec'
- 'test/**/*.rb'
# Turn class length metrics off for the tests.
@ -65,31 +45,10 @@ 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,61 +1,54 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2021-06-18 14:28:03 UTC using RuboCop version 1.12.1.
# on 2020-02-08 14:58:51 +0000 using RuboCop version 0.79.0.
# 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.
Gemspec/DevelopmentDependencies:
Enabled: false
# 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.
# Offense count: 15
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 650
Max: 600
# Offense count: 21
# Configuration parameters: IgnoredMethods.
# Offense count: 26
Metrics/CyclomaticComplexity:
Max: 14
# Offense count: 47
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
# Offense count: 120
# Configuration parameters: CountComments, ExcludedMethods.
Metrics/MethodLength:
Max: 34
Max: 32
# Offense count: 5
# Offense count: 2
# Configuration parameters: CountKeywordArgs.
Metrics/ParameterLists:
Max: 11
MaxOptionalParameters: 9
Max: 10
# Offense count: 14
# Configuration parameters: IgnoredMethods.
# Offense count: 21
Metrics/PerceivedComplexity:
Max: 15
# Offense count: 7
# Offense count: 9
Naming/AccessorMethodName:
Exclude:
- 'lib/zip/entry.rb'
- 'lib/zip/filesystem.rb'
- 'lib/zip/input_stream.rb'
- 'lib/zip/streamable_stream.rb'
# Offense count: 7
# Cop supports --auto-correct.
# 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.
# SupportedStyles: nested, compact
Style/ClassAndModuleChildren:
Exclude:
@ -64,49 +57,71 @@ 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: 22
# Configuration parameters: AllowedConstants.
# Offense count: 26
Style/Documentation:
Enabled: false
# Offense count: 13
# 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
# Cop supports --auto-correct.
Style/IfUnlessModifier:
Exclude:
- 'lib/zip/entry.rb'
- 'lib/zip/file_split.rb'
- 'lib/zip/filesystem/dir.rb'
- 'lib/zip/filesystem/file.rb'
- 'lib/zip/extra_field/generic.rb'
- 'lib/zip/file.rb'
- 'lib/zip/filesystem.rb'
- 'lib/zip/input_stream.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:
Exclude:
- 'lib/zip/extra_field.rb'
Enabled: false
# Offense count: 21
# Offense count: 23
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, IgnoredMethods.
# Configuration parameters: AutoCorrect, 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/file.rb'
- 'lib/zip/filesystem.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,22 +0,0 @@
# frozen_string_literal: true
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,72 +1,4 @@
# 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)
# X.X.X (Next)
# 2.4.1 (2025-01-05)
@ -93,11 +25,11 @@ Tooling:
# 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`.
- This is 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.
- This is a dummy release to warn about breaking changes coming in version 3.0.
# 2.3.0 (2020-03-14)

View File

@ -1,12 +1,3 @@
# 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,8 +1,6 @@
# 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

View File

@ -1,24 +0,0 @@
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.

172
README.md
View File

@ -2,21 +2,23 @@
[![Gem Version](https://badge.fury.io/rb/rubyzip.svg)](http://badge.fury.io/rb/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 notes
## Important note
Rubyzip 2.4 is intended to be the last release in the 2.x series. Please get ready for version 3.0.
### Updating to version 3.0
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:
The public API of some classes has been modernized to use named parameters for optional arguments. Also some methods have been changed or removed. Please check your usage of the following Rubyzip classes:
* `File`
* `Entry`
* `InputStream`
* `OutputStream`
* `DOSTime`
**Please see [Updating to version 3.x](https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x) in the wiki for details.**
@ -55,7 +57,7 @@ input_filenames = ['image.jpg', 'description.txt', 'stats.csv']
zipfile_name = "/Users/me/Desktop/archive.zip"
Zip::File.open(zipfile_name, create: true) do |zipfile|
Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
input_filenames.each do |filename|
# Two arguments:
# - The name of the file as it will appear in the archive
@ -94,7 +96,7 @@ class ZipFileGenerator
def write
entries = Dir.entries(@input_dir) - %w[. ..]
::Zip::File.open(@output_file, create: true) do |zipfile|
::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
write_entries entries, '', zipfile
end
end
@ -127,9 +129,9 @@ class ZipFileGenerator
end
```
### Save zip archive entries sorted by name
### Save zip archive entries in sorted by name state
To save zip archives with their entries sorted by name (see below), set `::Zip.sort_entries` to `true`
To save zip archives in sorted order like below, you need to set `::Zip.sort_entries` to `true`
```
Vegetable/
@ -143,7 +145,7 @@ fruit/mango
fruit/orange
```
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.
After this, entries in the zip archive will be saved in ordered state.
### Default permissions of zip archives
@ -179,71 +181,28 @@ Zip::File.open('foo.zip') do |zip_file|
end
```
### Notes on `Zip::InputStream`
#### Notice about ::Zip::InputStream
`Zip::InputStream` can be used for faster reading of zip file content because it does not read the Central directory up front.
`::Zip::InputStream` usable for fast reading zip file content because it not read Central directory.
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.
But there is one exception when it is not working - General Purpose Flag Bit 3.
> 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 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`.
If `::Zip::InputStream` finds such entry in the zip archive it will raise an exception.
### 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
# 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
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
```
#### 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._
This is an experimental feature and the interface for encryption may change in future versions.
## Known issues
@ -337,37 +296,25 @@ 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.
### Compression level
### Default Compression
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:
You can set the default compression level like so:
```ruby
Zip.default_compression = X
Zip.default_compression = Zlib::DEFAULT_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
```
It defaults to `Zlib::DEFAULT_COMPRESSION`. Possible values are `Zlib::BEST_COMPRESSION`, `Zlib::DEFAULT_COMPRESSION` and `Zlib::NO_COMPRESSION`
### Zip64 Support
Since version 3.0, Zip64 support is enabled for writing by default. To disable it do this:
By default, Zip64 support is disabled for writing. To enable it do this:
```ruby
Zip.write_zip64_support = false
Zip.write_zip64_support = true
```
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.
_NOTE_: If you will enable Zip64 writing then you will need zip extractor with Zip64 support to extract archive.
### Block Form
@ -382,50 +329,15 @@ 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
Install the dependencies:
To run the test you need to do this:
```shell
bundle install
```
Run the tests with `rake`:
```shell
bundle install
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
@ -434,29 +346,17 @@ http://rdoc.info/github/rubyzip/rubyzip/master/frames
## Authors
See https://github.com/rubyzip/rubyzip/graphs/contributors for a comprehensive list.
Alexander Simonov ( alex at simonov.me)
### Current maintainers
Alan Harper ( alan at aussiegeek.net)
* Robert Haines (@hainesr)
* John Lees-Miller (@jdleesmiller)
* Oleksandr Simonov (@simonoff)
Thomas Sondergaard (thomas at sondergaard.cc)
### Original author
Technorama Ltd. (oss-ruby-zip at technorama.net)
* Thomas Sondergaard
extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org)
## License
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).
Rubyzip is distributed under the same license as ruby. See
http://www.ruby-lang.org/en/LICENSE.txt

View File

@ -1,8 +1,5 @@
# frozen_string_literal: true
require 'bundler/gem_tasks'
require 'rake/testtask'
require 'rdoc/task'
require 'rubocop/rake_task'
task default: :test
@ -14,12 +11,11 @@ Rake::TestTask.new(:test) do |test|
test.verbose = true
end
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
RuboCop::RakeTask.new
# 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

15
TODO Normal file
View File

@ -0,0 +1,15 @@
* 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,5 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/setup'
require 'zip'

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'English'
require 'delegate'
require 'singleton'
@ -35,12 +33,26 @@ 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
V3_API_WARNING_MSG = <<~END_MSG
You have called '%s' (from %s).
This method is changing or deprecated in version 3.0.0. Please see
https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x
for more information.
END_MSG
def self.warn_about_v3_api(method)
return unless ENV['RUBYZIP_V3_API_WARN']
loc = caller_locations(2, 1)[0]
from = "#{loc.path.split('/').last}:#{loc.lineno}"
warn format(V3_API_WARNING_MSG, method, from)
end
if ENV['RUBYZIP_V3_API_WARN'] && RUBY_VERSION < '3.0'
warn 'RubyZip 3.0 will require Ruby 3.0 or later.'
end
extend self
attr_accessor :unicode_names,
:on_exists_proc,
@ -53,27 +65,19 @@ module Zip
:force_entry_names_encoding,
:validate_entry_sizes
DEFAULT_RESTORE_OPTIONS = {
restore_ownership: false,
restore_permissions: true,
restore_times: true
}.freeze # :nodoc:
def reset! # :nodoc:
def reset!
@_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 = true
@default_compression = ::Zlib::DEFAULT_COMPRESSION
@write_zip64_support = false
@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,42 +1,24 @@
# frozen_string_literal: true
require 'forwardable'
require_relative 'dirtyable'
module Zip
class CentralDirectory # :nodoc:
extend Forwardable
include Dirtyable
END_OF_CD_SIG = 0x06054b50
ZIP64_END_OF_CD_SIG = 0x06064b50
ZIP64_EOCD_LOCATOR_SIG = 0x07064b50
class CentralDirectory
include Enumerable
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_accessor :comment
attr_reader :comment
def_delegators :@entry_set,
:<<, :delete, :each, :entries, :find_entry, :glob,
:include?, :size
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
# Returns an Enumerable containing the entries.
def entries
@entry_set.entries
end
def read_from_stream(io)
read_eocds(io)
read_central_directory_entries(io)
def initialize(entries = EntrySet.new, comment = '') #:nodoc:
super()
@entry_set = entries.kind_of?(EntrySet) ? entries : EntrySet.new(entries)
@comment = comment
end
def write_to_stream(io) #:nodoc:
@ -44,34 +26,20 @@ module Zip
@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 &&
(cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF)
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
end
write_e_o_c_d(io, cdir_offset, cdir_size)
end
# 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_CD_SIG,
END_OF_CDS,
0, # @numberOfThisDisk
0, # @numberOfDiskWithStartOfCDir
@entry_set ? [@entry_set.size, 0xFFFF].min : 0,
@ -84,9 +52,11 @@ module Zip
io << @comment
end
private :write_e_o_c_d
def write_64_e_o_c_d(io, offset, cdir_size) #:nodoc:
tmp = [
ZIP64_END_OF_CD_SIG,
ZIP64_END_OF_CDS,
44, # size of zip64 end of central directory record (excludes signature and field itself)
VERSION_MADE_BY,
VERSION_NEEDED_TO_EXTRACT_ZIP64,
@ -100,9 +70,11 @@ 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_SIG,
ZIP64_EOCD_LOCATOR,
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
@ -110,145 +82,127 @@ module Zip
io << tmp.pack('VVQ<V')
end
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<')
private :write_64_eocd_locator
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?
''
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?
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
buffer.slice(
ZIP64_STATIC_EOCD_SIZE,
zip64_extensible_data_size
)
end
end
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 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
''
buf.read(comment_length)
end
raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
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
eof = true
raise Error, 'Zip consistency problem while reading central directory entry'
end
raise Error, 'Zip consistency problem while reading central directory entry' if eof || io.eof?
@entry_set = EntrySet.new
@size.times do
entry = Entry.read_c_dir_entry(io)
next unless entry
@entry_set << Entry.read_c_dir_entry(io)
end
end
offset = if entry.zip64?
entry.extra['Zip64'].relative_header_offset
def read_from_stream(io) #:nodoc:
buf = start_buf(io)
if zip64_file?(buf)
read_64_e_o_c_d(buf)
else
entry.local_header_offset
read_e_o_c_d(buf)
end
read_central_directory_entries(io)
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)
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
buf = buf.slice!((sig_index + 4)..(buf.bytesize))
def buf.read(count)
slice!(0, count)
end
@entry_set << entry
end
buf
end
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)
def zip64_file?(buf)
buf.rindex([ZIP64_END_OF_CDS].pack('V')) && buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
end
def read_eocds(io) # :nodoc:
base_location, data = eocd_data(io)
eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
raise Error, 'Zip end of central directory signature not found' unless eocd_location
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
unpack_e_o_c_d(data.slice(eocd_location..-1))
end
def eocd_data(io)
def start_buf(io)
begin
io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
io.seek(-MAX_END_OF_CDS_SIZE, IO::SEEK_END)
rescue Errno::EINVAL
io.seek(0, IO::SEEK_SET)
end
io.read
end
[io.tell, io.read]
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
end
end
end

View File

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

View File

@ -1,8 +1,4 @@
# frozen_string_literal: true
module Zip
# :stopdoc:
RUNNING_ON_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/i
CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
@ -15,8 +11,6 @@ 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
@ -44,27 +38,27 @@ module Zip
FSTYPE_ATHEOS = 30
FSTYPES = {
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'
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
}.freeze
COMPRESSION_METHOD_STORE = 0
@ -118,6 +112,4 @@ module Zip
COMPRESSION_METHOD_PPMD => 'PPMd version I, Rev 1',
COMPRESSION_METHOD_AES => 'AES encryption'
}.freeze
# :startdoc:
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +0,0 @@
# 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,7 +1,3 @@
# frozen_string_literal: true
require 'rubygems'
module Zip
class DOSTime < Time #:nodoc:all
# MS-DOS File Date and Time format as used in Interrupt 21H Function 57H:
@ -16,14 +12,6 @@ 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) +
@ -37,7 +25,8 @@ module Zip
end
def dos_equals(other)
warn 'Zip::DOSTime#dos_equals is deprecated. Use `==` instead.'
Zip.warn_about_v3_api('DOSTime#dos_equals')
self == other
end
@ -61,35 +50,7 @@ module Zip
month = (0b111100000 & bin_dos_date) >> 5
year = ((0b1111111000000000 & bin_dos_date) >> 9) + 1980
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
local(year, month, day, hour, minute, second)
end
end
end

View File

@ -1,44 +1,21 @@
# frozen_string_literal: true
require 'pathname'
require_relative 'dirtyable'
module Zip
# Zip::Entry represents an entry in a Zip archive.
class Entry
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
STORED = 0
DEFLATED = 8
# Language encoding flag (EFS) bit
EFS = 0b100000000000 # :nodoc:
EFS = 0b100000000000
# 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:
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:
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:
def set_default_vars_values
@local_header_offset = 0
@local_header_size = nil # not known until local entry is created or read
@internal_file_attributes = 1
@ -57,222 +34,175 @@ module Zip
end
@follow_symlinks = false
@restore_times = DEFAULT_RESTORE_OPTIONS[:restore_times]
@restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
@restore_ownership = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
@restore_times = false
@restore_permissions = false
@restore_ownership = false
# 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) # :nodoc:
raise EntryNameError, name if name.start_with?('/')
raise EntryNameError if name.length > 65_535
def check_name(name)
return unless name.start_with?('/')
raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
end
# 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)
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def initialize(zipfile = nil, name = nil, *args)
name ||= ''
check_name(name)
set_default_vars_values
@fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
@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)
@zipfile = zipfile || ''
@name = name
if (args_hash = args.first).kind_of?(::Hash)
@comment = args_hash[:comment] || ''
@extra = args_hash[:extra] || ''
@compressed_size = args_hash[:compressed_size] || 0
@crc = args_hash[:crc] || 0
@compression_method = args_hash[:compression_method] || ::Zip::Entry::DEFLATED
@size = args_hash[:size] || 0
@time = args_hash[:time] || ::Zip::DOSTime.now
else
::Zip::DOSTime.now
end
@extra =
extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
Zip.warn_about_v3_api('Zip::Entry.new') unless args.empty?
set_compression_level_flags
@comment = args[0] || ''
@extra = args[1] || ''
@compressed_size = args[2] || 0
@crc = args[3] || 0
@compression_method = args[4] || ::Zip::Entry::DEFLATED
@size = args[5] || 0
@time = args[6] || ::Zip::DOSTime.now
end
# Is this entry encrypted?
@ftype = name_is_directory? ? :directory : :file
@extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.kind_of?(::Zip::ExtraField)
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def encrypted?
gp_flags & 1 == 1
end
def incomplete? # :nodoc:
(gp_flags & 8 == 8) && (crc == 0 || size == 0 || compressed_size == 0)
def incomplete?
gp_flags & 8 == 8
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 =
def time
if @extra['UniversalTime']
@extra['UniversalTime'].send(component)
@extra['UniversalTime'].mtime
elsif @extra['NTFS']
@extra['NTFS'].send(component)
end
@extra['NTFS'].mtime
else
# Standard time field in central directory has local time
# under archive creator. Then, we can't get timezone.
time || (@time if component == :mtime)
@time
end
end
alias mtime time
# 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
def time=(value)
unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
@extra.create('UniversalTime')
end
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
(@extra['UniversalTime'] || @extra['NTFS']).mtime = value
@time = value
end
alias mtime= time=
def file_type_is?(type)
raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
# 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
@ftype == type
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:
def name_is_directory? #:nodoc:all
@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? # :nodoc:
def name_safe?
cleanpath = Pathname.new(@name).cleanpath
return false unless cleanpath.relative?
root = ::File::SEPARATOR
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)
naive_expanded_path = ::File.join(root, cleanpath.to_s)
::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
end
def local_entry_offset # :nodoc:
def local_entry_offset #:nodoc:all
local_header_offset + @local_header_size
end
def name_size # :nodoc:
def name_size
@name ? @name.bytesize : 0
end
def extra_size # :nodoc:
def extra_size
@extra ? @extra.local_size : 0
end
def comment_size # :nodoc:
def comment_size
@comment ? @comment.bytesize : 0
end
def calculate_local_header_size # :nodoc:
def calculate_local_header_size #:nodoc:all
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! # :nodoc:
def verify_local_header_size!
return if @local_header_size.nil?
new_size = calculate_local_header_size
return unless @local_header_size != new_size
raise Error,
"Local header size changed (#{@local_header_size} -> #{new_size})"
raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
end
def cdir_header_size # :nodoc:
def cdir_header_size #:nodoc:all
CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
(@extra ? @extra.c_dir_size : 0) + comment_size
end
def next_header_offset # :nodoc:
local_entry_offset + compressed_size
def next_header_offset #:nodoc:all
local_entry_offset + compressed_size + data_descriptor_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)
Zip.warn_about_v3_api('Zip::Entry#extract')
if dest_path.nil? && !name_safe?
warn "WARNING: skipped '#{@name}' 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)
self
end
# Extracts this entry to a file at `entry_path`, with
@ -280,7 +210,7 @@ module Zip
#
# NB: The caller is responsible for making sure `destination_directory` is
# safe, if it is passed.
def extract(entry_path = @name, destination_directory: '.', &block)
def extract_v3(entry_path = @name, destination_directory: '.', &block)
dest_dir = ::File.absolute_path(destination_directory || '.')
extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
@ -297,12 +227,24 @@ module Zip
self
end
def to_s # :nodoc:
def to_s
@name
end
class << self
def read_c_dir_entry(io) # :nodoc:
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
path = if io.respond_to?(:path)
io.path
else
@ -315,18 +257,16 @@ module Zip
nil
end
def read_local_entry(io) # :nodoc:
def read_local_entry(io)
entry = new(io)
entry.read_local_entry(io)
entry
rescue SplitArchiveError
raise
rescue Error
nil
end
end
def unpack_local_entry(buf) # :nodoc:
def unpack_local_entry(buf)
@header_signature,
@version,
@fstype,
@ -341,66 +281,62 @@ module Zip
@extra_length = buf.unpack('VCCvvvvVVVvv')
end
def read_local_entry(io) # :nodoc:
@dirty = false # No changes at this point.
current_offset = io.tell
def read_local_entry(io) #:nodoc:all
@local_header_offset = io.tell
read_local_header_fields(io)
static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
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)
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
unless @header_signature == LOCAL_ENTRY_SIGNATURE
raise Error, "Zip local header magic not found at location '#{current_offset}'"
end
unpack_local_entry(static_sized_fields_buf)
@local_header_offset = current_offset
unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
end
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
read_extra_field(extra, local: true)
if @extra.kind_of?(::Zip::ExtraField)
@extra.merge(extra) if extra
else
@extra = ::Zip::ExtraField.new(extra)
end
parse_zip64_extra(true)
@local_header_size = calculate_local_header_size
end
def pack_local_entry # :nodoc:
def pack_local_entry
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 || 0),
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
name_size,
@extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
end
def write_local_entry(io, rewrite: false) # :nodoc:
prep_local_zip64_extra
def write_local_entry(io, rewrite = false) #:nodoc:all
prep_zip64_extra(true)
verify_local_header_size! if rewrite
@local_header_offset = io.tell
@ -411,7 +347,7 @@ module Zip
@local_header_size = io.tell - @local_header_offset
end
def unpack_c_dir_entry(buf) # :nodoc:
def unpack_c_dir_entry(buf)
@header_signature,
@version, # version of encoding software
@fstype, # filesystem type
@ -435,7 +371,7 @@ module Zip
@comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
end
def set_ftype_from_c_dir_entry # :nodoc:
def set_ftype_from_c_dir_entry
@ftype = case @fstype
when ::Zip::FSTYPE_UNIX
@unix_perms = (@external_file_attributes >> 16) & 0o7777
@ -447,9 +383,8 @@ 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 be 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 extracted
if name_is_directory?
:directory
else
@ -465,47 +400,43 @@ module Zip
end
end
def check_c_dir_entry_static_header_length(buf) # :nodoc:
return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
def check_c_dir_entry_static_header_length(buf)
return if 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 # :nodoc:
return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
def check_c_dir_entry_signature
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 # :nodoc:
def check_c_dir_entry_comment_size
return if @comment && @comment.bytesize == @comment_length
raise ::Zip::Error, 'Truncated cdir zip entry header'
end
def read_extra_field(buf, local: false) # :nodoc:
def read_c_dir_extra_field(io)
if @extra.kind_of?(::Zip::ExtraField)
@extra.merge(buf, local: local) if buf
@extra.merge(io.read(@extra_length))
else
@extra = ::Zip::ExtraField.new(buf, local: local)
@extra = ::Zip::ExtraField.new(io.read(@extra_length))
end
end
def read_c_dir_entry(io) # :nodoc:
@dirty = false # No changes at this point.
def read_c_dir_entry(io) #:nodoc:all
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
@name.tr!('\\', '/') # Normalise filepath separators after encoding set.
read_extra_field(io.read(@extra_length))
read_c_dir_extra_field(io)
@comment = io.read(@comment_length)
check_c_dir_entry_comment_size
set_ftype_from_c_dir_entry
@ -521,27 +452,27 @@ module Zip
end
def get_extra_attributes_from_path(path) # :nodoc:
stat = file_stat(path)
@time = DOSTime.from_time(stat.mtime)
return if ::Zip::RUNNING_ON_WINDOWS
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
# 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)
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
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?
@ -550,14 +481,9 @@ 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 # :nodoc:
def pack_c_dir_entry
zip64 = @extra['Zip64']
[
@header_signature,
@ -565,12 +491,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 || 0),
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
name_size,
@extra ? @extra.c_dir_size : 0,
comment_size,
@ -584,12 +510,11 @@ module Zip
].pack('VCCvvvvvVVVvvvvvVV')
end
def write_c_dir_entry(io) # :nodoc:
prep_cdir_zip64_extra
def write_c_dir_entry(io) #:nodoc:all
prep_zip64_extra(false)
case @fstype
when ::Zip::FSTYPE_UNIX
ft = case ftype
ft = case @ftype
when :file
@unix_perms ||= 0o644
::Zip::FILE_TYPE_FILE
@ -602,7 +527,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
@ -613,42 +538,43 @@ module Zip
io << @comment
end
def ==(other) # :nodoc:
def ==(other)
return false unless other.class == self.class
# Compares contents of local entry and exposed fields
%w[compression_method crc compressed_size size name extra filepath time].all? do |k|
keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
other.__send__(k.to_sym) == __send__(k.to_sym)
end
keys_equal && time == other.time
end
def <=>(other) # :nodoc:
def <=>(other)
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
if @ftype == :directory
yield ::Zip::NullInputStream if block_given?
::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
yield(stringio) if block_given?
stringio
else
raise "unknown @file_type #{ftype}"
raise "unknown @file_type #{@ftype}"
end
else
zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
zis.instance_variable_set(:@complete_entry, self)
zis.get_next_entry
if block
if block_given?
begin
yield(zis)
ensure
@ -685,30 +611,27 @@ 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:
if ftype == :directory
zip_output_stream.put_next_entry(self)
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)
elsif @filepath
zip_output_stream.put_next_entry(self)
get_input_stream do |is|
::Zip::IOExtras.copy_stream(zip_output_stream, is)
end
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) }
else
zip_output_stream.copy_raw_entry(self)
end
end
def parent_as_string # :nodoc:
def parent_as_string
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) # :nodoc:
def get_raw_input_stream(&block)
if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
yield @zipfile
else
@ -716,22 +639,12 @@ module Zip
end
end
def clean_up # :nodoc:
@dirty = false # Any changes are written at this point.
def clean_up
# By default, do nothing
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
@ -740,9 +653,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::DestinationExistsError, dest_path
raise ::Zip::DestinationFileExistsError,
"Destination '#{dest_path}' already exists"
end
::File.open(dest_path, 'wb') do |os|
get_input_stream do |is|
bytes_written = 0
@ -753,10 +666,10 @@ module Zip
bytes_written += buf.bytesize
next unless bytes_written > size && !warned
error = ::Zip::EntrySizeError.new(self)
raise error if ::Zip.validate_entry_sizes
message = "entry '#{name}' should be #{size}B, but is larger when inflated."
raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
warn "WARNING: #{error.message}"
warn "WARNING: #{message}"
warned = true
end
end
@ -769,11 +682,14 @@ module Zip
return if ::File.directory?(dest_path)
if ::File.exist?(dest_path)
raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, 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
end
::FileUtils.mkdir_p(dest_path)
set_extra_attributes_on_path(dest_path)
end
@ -787,71 +703,53 @@ 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:
return unless zip64?
def parse_zip64_extra(for_local_header) #:nodoc:all
return if @extra['Zip64'].nil?
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
# 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
def data_descriptor_size
(@gp_flags & 0x0008) > 0 ? 16 : 0
end
# rubocop:disable Style/GuardClause
def prep_local_zip64_extra
# create a zip64 extra information field if we need one
def prep_zip64_extra(for_local_header) #:nodoc:all
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
need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
if need_zip64
@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
@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
end
end
def prep_cdir_zip64_extra
return unless ::Zip.write_zip64_support
if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
@local_header_offset >= 0xFFFFFFFF
@version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
zip64 = @extra['Zip64'] || @extra.create('Zip64')
# Central directory entry entries include whichever fields are necessary.
zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
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')
# 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
end
end
# rubocop:enable Style/GuardClause
end
end

View File

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

View File

@ -1,139 +1,19 @@
# frozen_string_literal: true
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
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
# 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
# Backwards compatibility with v1 (delete in v2)
ZipError = Error
ZipEntryExistsError = EntryExistsError
ZipDestinationFileExistsError = DestinationFileExistsError
ZipCompressionMethodError = CompressionMethodError
ZipEntryNameError = EntryNameError
ZipInternalError = InternalError
end

View File

@ -1,11 +1,9 @@
# frozen_string_literal: true
module Zip
class ExtraField < Hash # :nodoc:all
class ExtraField < Hash
ID_MAP = {}
def initialize(binstr = nil, local: false)
merge(binstr, local: local) if binstr
def initialize(binstr = nil)
merge(binstr) if binstr
end
def extra_field_type_exist(binstr, id, len, index)
@ -18,18 +16,25 @@ module Zip
end
end
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)
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]
return
end
self['Unknown'].merge(binstr[index, len + 4], local: local)
self['Unknown'] << binstr[index, len + 4]
end
def merge(binstr, local: false)
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)
return if binstr.empty?
i = 0
@ -39,7 +44,8 @@ module Zip
if id && ID_MAP.member?(id)
extra_field_type_exist(binstr, id, len, i)
elsif id
break unless extra_field_type_unknown(binstr, len, i, local)
create_unknown_item unless self['Unknown']
break unless extra_field_type_unknown(binstr, len, i)
end
i += len + 4
end
@ -53,8 +59,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) }
@ -84,12 +90,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,7 +1,5 @@
# frozen_string_literal: true
module Zip
class ExtraField::Generic # :nodoc:
class ExtraField::Generic
def self.register_map
return unless const_defined?(:HEADER_ID)
@ -21,17 +19,26 @@ module Zip
return false
end
[binstr[2, 2].unpack1('v'), binstr[4..]]
[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
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,9 +1,7 @@
# frozen_string_literal: true
module Zip
# PKWARE NTFS Extra Field (0x000a)
# Only Tag 0x0001 is supported
class ExtraField::NTFS < ExtraField::Generic # :nodoc:
class ExtraField::NTFS < ExtraField::Generic
HEADER_ID = [0x000A].pack('v')
register_map
@ -25,7 +23,7 @@ module Zip
size, content = initial_parse(binstr)
(size && content) || return
content = content[4..]
content = content[4..-1]
tags = parse_tags(content)
tag1 = tags[1]
@ -53,7 +51,7 @@ module Zip
# reserved 0 and tag 1
s = [0, 1].pack('Vv')
tag1 = (+'').force_encoding(Encoding::BINARY)
tag1 = ''.b
if @mtime
tag1 << [to_ntfs_time(@mtime)].pack('Q<')
if @atime
@ -86,7 +84,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,8 +1,6 @@
# frozen_string_literal: true
module Zip
# Olf Info-ZIP Extra for UNIX uid/gid and file timestampes
class ExtraField::OldUnix < ExtraField::Generic # :nodoc:
class ExtraField::OldUnix < ExtraField::Generic
HEADER_ID = 'UX'
register_map

View File

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

View File

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

View File

@ -1,33 +0,0 @@
# 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,11 +1,7 @@
# frozen_string_literal: true
module Zip
# Info-ZIP Extra for Zip64 size
class ExtraField::Zip64 < ExtraField::Generic # :nodoc:
attr_accessor :compressed_size, :disk_start_number,
:original_size, :relative_header_offset
class ExtraField::Zip64 < ExtraField::Generic
attr_accessor :original_size, :compressed_size, :relative_header_offset, :disk_start_number
HEADER_ID = ['0100'].pack('H*')
register_map
@ -40,9 +36,7 @@ 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
if relative_header_offset && relative_header_offset == 0xFFFFFFFF
@relative_header_offset = extract(8, 'Q<')
end
@relative_header_offset = extract(8, 'Q<') if relative_header_offset && relative_header_offset == 0xFFFFFFFF
@disk_start_number = extract(4, 'V') if disk_start_number && disk_start_number == 0xFFFF
@content = nil
[@original_size || original_size,
@ -57,8 +51,7 @@ 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<')
@ -66,7 +59,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 = ''.b
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

@ -0,0 +1,15 @@
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,109 +1,132 @@
# frozen_string_literal: true
require 'forwardable'
require_relative 'file_split'
module Zip
# 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
# 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
# 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 `my.zip`
# The following example opens zip archive <code>my.zip</code>
# (creating it if it doesn't exist) and adds an entry
# `first.txt` and a directory entry `a_dir`
# <code>first.txt</code> and a directory entry <code>a_dir</code>
# to it.
#
# ```
# require 'zip'
#
# 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
# ```
# 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")
# }
#
# The next example reopens `my.zip`, writes the contents of
# `first.txt` to standard out and deletes the entry from
# 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 archive.
#
# ```
# require 'zip'
#
# Zip::File.open('my.zip', create: true) do |zipfile|
# puts zipfile.read('first.txt')
# zipfile.remove('first.txt')
# end
# Zip::File.open("my.zip", Zip::File::CREATE) {
# |zipfile|
# puts zipfile.read("first.txt")
# zipfile.remove("first.txt")
# }
#
# 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
# ZipFileSystem offers an alternative API that emulates ruby's
# interface for accessing the filesystem, ie. the File and Dir classes.
IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze # :nodoc:
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
# The name of this zip archive.
attr_reader :name
# default -> false.
attr_accessor :restore_ownership
# default -> true.
# default -> false, but will be set to true in a future version.
attr_accessor :restore_permissions
# default -> true.
# default -> false, but will be set to true in a future version.
attr_accessor :restore_times
def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
# Returns the zip files comment, if it has one
attr_accessor :comment
# Opens a zip archive. Pass create: true to create
# Opens a zip archive. Pass true as the second parameter to create
# a new archive if it doesn't exist already.
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)
def initialize(path_or_io, dep_create = false, dep_buffer = false,
create: false, buffer: false, **options)
super()
Zip.warn_about_v3_api('File#new') if dep_create || dep_buffer
options = DEFAULT_OPTIONS.merge(options)
@name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
@create = create ? true : false # allow any truthy value to mean true
@comment = ''
@create = create || dep_create ? true : false # allow any truthy value to mean true
buffer ||= dep_buffer
initialize_cdir(path_or_io, buffer: buffer)
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
@restore_ownership = restore_ownership
@restore_permissions = restore_permissions
@restore_times = restore_times
@compression_level = compression_level
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.
@create = false
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]
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,
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)
def open(file_name, dep_create = false, create: false, **options)
Zip.warn_about_v3_api('Zip::File.open') if dep_create
zf = ::Zip::File.new(file_name, create: (dep_create || create), buffer: false, **options)
return zf unless block_given?
begin
@ -113,29 +136,31 @@ module Zip
end
end
# Same as #open. But outputs data to a buffer instead of a file
def add_buffer
Zip.warn_about_v3_api('Zip::File.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 = ::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)
def open_buffer(io, **options)
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)
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)
# https://github.com/rubyzip/rubyzip/issues/119
io.binmode if io.respond_to?(:binmode)
zf = ::Zip::File.new(io, create: true, buffer: true, **options)
return zf unless block_given?
yield zf
@ -159,19 +184,89 @@ module Zip
end
end
# 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
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
else
cdir.count_entries(path_or_io)
segment_size
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,
dep_segment_size = MAX_SEGMENT_SIZE, dep_delete_zip_file = true, dep_partial_zip_file_name = nil,
segment_size: MAX_SEGMENT_SIZE, delete_zip_file: nil, 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)
if dep_segment_size != MAX_SEGMENT_SIZE || !dep_delete_zip_file || dep_partial_zip_file_name
Zip.warn_about_v3_api('Zip::File.split')
end
zip_file_size = ::File.size(zip_file_name)
segment_size = get_segment_size_for_split(segment_size || dep_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 || dep_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
delete_zip_file = delete_zip_file.nil? ? dep_delete_zip_file : delete_zip_file
::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
@ -186,31 +281,45 @@ 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, permissions: nil, comment: nil,
# rubocop:disable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
def get_output_stream(entry,
dep_permission_int = nil, dep_comment = nil,
dep_extra = nil, dep_compressed_size = nil, dep_crc = nil,
dep_compression_method = nil, dep_size = nil, dep_time = nil,
permission_int: nil, comment: nil,
extra: nil, compressed_size: nil, crc: nil,
compression_method: nil, compression_level: nil,
size: nil, time: nil, &a_proc)
compression_method: nil, size: nil, time: nil,
&a_proc)
unless dep_permission_int.nil? && dep_comment.nil? && dep_extra.nil? &&
dep_compressed_size.nil? && dep_crc.nil? && dep_compression_method.nil? &&
dep_size.nil? && dep_time.nil?
Zip.warn_about_v3_api('Zip::File#get_output_stream')
end
new_entry =
if entry.kind_of?(Entry)
entry
else
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
)
Entry.new(@name, entry.to_s,
comment: (comment || dep_comment),
extra: (extra || dep_extra),
compressed_size: (compressed_size || dep_compressed_size),
crc: (crc || dep_crc),
compression_method: (compression_method || dep_compression_method),
size: (size || dep_size),
time: (time || dep_time))
end
if new_entry.directory?
raise ArgumentError,
"cannot open stream to directory entry - '#{new_entry}'"
end
new_entry.unix_perms = permissions
new_entry.unix_perms = (permission_int || dep_permission_int)
zip_streamable_entry = StreamableStream.new(new_entry)
@cdir << zip_streamable_entry
@entry_set << zip_streamable_entry
zip_streamable_entry.get_output_stream(&a_proc)
end
# rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
# Returns the name of the zip archive
def to_s
@ -226,39 +335,31 @@ 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 = if entry.kind_of?(::Zip::Entry)
entry
else
::Zip::Entry.new(
@name, entry.to_s,
compression_level: @compression_level
)
end
new_entry = entry.kind_of?(::Zip::Entry) ? entry : ::Zip::Entry.new(@name, entry.to_s)
new_entry.gather_fileinfo_from_srcpath(src_path)
@cdir << new_entry
new_entry.dirty = true
@entry_set << 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, compression_method: ::Zip::Entry::STORED
)
entry = ::Zip::Entry.new(@name, entry.to_s, nil, nil, nil, nil, ::Zip::Entry::STORED)
add(entry, src_path, &continue_on_exists_proc)
end
# Removes the specified entry.
def remove(entry)
@cdir.delete(get_entry(entry))
@entry_set.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')
@cdir.delete(found_entry)
@entry_set.delete(found_entry)
found_entry.name = new_name
@cdir << found_entry
@entry_set << found_entry
end
# Replaces the specified entry with the contents of src_path (from
@ -269,16 +370,25 @@ module Zip
add(entry, src_path)
end
# Extracts entry to file dest_path.
def extract(entry, dest_path, &block)
Zip.warn_about_v3_api('Zip::File#extract')
block ||= proc { ::Zip.on_exists_proc }
found_entry = get_entry(entry)
found_entry.extract(dest_path, &block)
end
# 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)
def extract_v3(entry, entry_path = nil, destination_directory: '.', &block)
block ||= proc { ::Zip.on_exists_proc }
found_entry = get_entry(entry)
entry_path ||= found_entry.name
found_entry.extract(entry_path, destination_directory: destination_directory, &block)
found_entry.extract_v3(entry_path, destination_directory: destination_directory, &block)
end
# Commits changes that has been made since the previous commit to
@ -288,15 +398,16 @@ module Zip
on_success_replace do |tmp_file|
::Zip::OutputStream.open(tmp_file) do |zos|
@cdir.each do |e|
@entry_set.each do |e|
e.write_to_zip_output_stream(zos)
e.dirty = false
e.clean_up
end
zos.comment = comment
end
true
end
initialize_cdir(@name)
initialize(name)
end
# Write buffer write changes to buffer and return
@ -304,7 +415,7 @@ module Zip
return io unless commit_required?
::Zip::OutputStream.write_buffer(io) do |zos|
@cdir.each { |e| e.write_to_zip_output_stream(zos) }
@entry_set.each { |e| e.write_to_zip_output_stream(zos) }
zos.comment = comment
end
end
@ -317,19 +428,16 @@ module Zip
# Returns true if any changes has been made to this archive since
# the previous commit
def commit_required?
return true if @create || @cdir.dirty?
@cdir.each do |e|
return true if e.dirty?
@entry_set.each do |e|
return true if e.dirty
end
false
@comment != @stored_comment || @entry_set != @stored_entries || @create
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 = @cdir.find_entry(entry_name)
selected_entry = @entry_set.find_entry(entry_name)
return if selected_entry.nil?
selected_entry.restore_ownership = @restore_ownership
@ -338,6 +446,11 @@ 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)
@ -353,50 +466,33 @@ module Zip
entry_name = entry_name.dup.to_s
entry_name << '/' unless entry_name.end_with?('/')
@cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
@entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
end
private
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"
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 += '/'
end
new_entry.directory? && path_is_directory
end
def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
return unless @cdir.include?(entry_name)
continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
return unless @entry_set.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
end
def check_file(path)
@ -406,6 +502,7 @@ 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
@ -416,6 +513,7 @@ module Zip
end
end
end
end
# Copyright (C) 2002, 2003 Thomas Sondergaard
# rubyzip is free software; you can redistribute it and/or

View File

@ -1,91 +0,0 @@
# 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,10 +1,4 @@
# 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
@ -19,52 +13,627 @@ 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'
#
# 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
# ```
# 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" }
# }
#
# 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'
#
# Zip::File.open('my.zip') do |zipfile|
# puts zipfile.file.read('first.txt')
# end
# ```
# Zip::File.open("my.zip") {
# |zipfile|
# puts zipfile.file.read("first.txt")
# }
module FileSystem
def initialize # :nodoc:
mapped_zip = ZipFileNameMapper.new(self)
@zip_fs_dir = Dir.new(mapped_zip)
@zip_fs_file = File.new(mapped_zip)
@zip_fs_dir = ZipFsDir.new(mapped_zip)
@zip_fs_file = ZipFsFile.new(mapped_zip)
@zip_fs_dir.file = @zip_fs_file
@zip_fs_file.dir = @zip_fs_dir
end
# 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
# 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
# invoked
def dir
@zip_fs_dir
end
# 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
# 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
# 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
class File # :nodoc:
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 = 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.dup : ::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
include FileSystem
end
end

View File

@ -1,86 +0,0 @@
# 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

@ -1,48 +0,0 @@
# 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

View File

@ -1,262 +0,0 @@
# 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

@ -1,110 +0,0 @@
# 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

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

View File

@ -1,6 +1,3 @@
# 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,
@ -40,8 +37,9 @@ module Zip
#
# java.util.zip.ZipInputStream is the original inspiration for this
# class.
class InputStream
CHUNK_SIZE = 32_768 # :nodoc:
CHUNK_SIZE = 32_768
include ::Zip::IOExtras::AbstractInputStream
@ -51,35 +49,34 @@ 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, dep_offset = 0, dep_decrypter = nil, offset: 0, decrypter: nil)
super()
@archive_io = get_io(context, offset)
@decompressor = ::Zip::NullDecompressor
@decrypter = decrypter || ::Zip::NullDecrypter.new
@current_entry = nil
@complete_entry = nil
if !dep_offset.zero? || !dep_decrypter.nil?
Zip.warn_about_v3_api('Zip::InputStream.new')
end
offset = dep_offset if offset.zero?
@archive_io = get_io(context, offset)
@decompressor = ::Zip::NullDecompressor
@decrypter = decrypter || dep_decrypter || ::Zip::NullDecrypter.new
@current_entry = nil
end
# Close this InputStream. All further IO will raise an IOError.
def close
@archive_io.close
end
# 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.
# 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.
def get_next_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
@archive_io.seek(@current_entry.next_header_offset, IO::SEEK_SET) if @current_entry
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?
@ -94,19 +91,18 @@ 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: offset, decrypter: decrypter)
def open(filename_or_io, dep_offset = 0, dep_decrypter = nil, offset: 0, decrypter: nil)
if !dep_offset.zero? || !dep_decrypter.nil?
Zip.warn_about_v3_api('Zip::InputStream.new')
end
offset = dep_offset if offset.zero?
zio = new(filename_or_io, offset: offset, decrypter: decrypter || dep_decrypter)
return zio unless block_given?
begin
@ -115,11 +111,17 @@ module Zip
zio.close if zio
end
end
def open_buffer(filename_or_io, offset = 0)
Zip.warn_about_v3_api('Zip::InputStream.open_buffer')
::Zip::InputStream.open(filename_or_io, offset)
end
end
protected
def get_io(io_or_file, offset = 0) # :nodoc:
def get_io(io_or_file, offset = 0)
if io_or_file.respond_to?(:seek)
io = io_or_file.dup
io.seek(offset, ::IO::SEEK_SET)
@ -131,58 +133,56 @@ module Zip
end
end
def open_entry # :nodoc:
def open_entry
@current_entry = ::Zip::Entry.read_local_entry(@archive_io)
return if @current_entry.nil?
if @current_entry.encrypted? && @decrypter.kind_of?(NullDecrypter)
raise Error,
'A password is required to decode this zip file'
if @current_entry && @current_entry.encrypted? && @decrypter.kind_of?(NullEncrypter)
raise Error, 'password required to decode zip file'
end
if @current_entry.incomplete? && @current_entry.compressed_size == 0 && !@complete_entry
raise StreamingError, @current_entry
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'
end
@decrypted_io = get_decrypted_io
@decompressor = get_decompressor
flush
@current_entry
end
def get_decrypted_io # :nodoc:
def get_decrypted_io
header = @archive_io.read(@decrypter.header_bytesize)
@decrypter.reset!(header)
::Zip::DecryptedIo.new(@archive_io, @decrypter)
end
def get_decompressor # :nodoc:
def get_decompressor
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, @current_entry.compression_method
raise ::Zip::CompressionMethodError,
"Unsupported compression method #{@current_entry.compression_method}"
end
decompressor_class.new(@decrypted_io, decompressed_size)
end
def produce_input # :nodoc:
def produce_input
@decompressor.read(CHUNK_SIZE)
end
def input_finished? # :nodoc:
def input_finished?
@decompressor.eof
end
end

View File

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

View File

@ -1,11 +1,9 @@
# frozen_string_literal: true
module Zip
module IOExtras # :nodoc:
module IOExtras
# 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 # :nodoc:
module AbstractInputStream
include Enumerable
include FakeIO
@ -13,22 +11,22 @@ module Zip
super
@lineno = 0
@pos = 0
@output_buffer = +''
@output_buffer = ''.b
end
attr_accessor :lineno
attr_reader :pos
def read(number_of_bytes = nil, buf = +'')
def read(number_of_bytes = nil, buf = ''.b)
tbuf = if @output_buffer.bytesize > 0
if number_of_bytes && number_of_bytes <= @output_buffer.bytesize
if number_of_bytes <= @output_buffer.bytesize
@output_buffer.slice!(0, number_of_bytes)
else
number_of_bytes -= @output_buffer.bytesize if number_of_bytes
rbuf = sysread(number_of_bytes, buf)
out = @output_buffer
out << rbuf if rbuf
@output_buffer = ''
@output_buffer = ''.b
out
end
else
@ -36,7 +34,7 @@ module Zip
end
if tbuf.nil? || tbuf.empty?
return nil if number_of_bytes&.positive?
return nil if number_of_bytes
return ''
end
@ -76,18 +74,15 @@ 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
@ -98,7 +93,7 @@ module Zip
def flush
ret_val = @output_buffer
@output_buffer = +''
@output_buffer = ''.b
ret_val
end

View File

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

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
module Zip
class NullCompressor < Compressor #:nodoc:all
include Singleton

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
module Zip
module NullDecompressor #:nodoc:all
module_function

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
module Zip
module NullInputStream #:nodoc:all
include ::Zip::NullDecompressor

View File

@ -1,8 +1,3 @@
# 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
@ -21,49 +16,57 @@ module Zip
#
# java.util.zip.ZipOutputStream is the original inspiration for this
# class.
class OutputStream
extend Forwardable
include ::Zip::IOExtras::AbstractOutputStream
def_delegators :@cdir, :comment, :comment=
attr_accessor :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, dep_stream = false, dep_encrypter = nil, stream: false, encrypter: nil)
super()
Zip.warn_about_v3_api('Zip::OutputStream.new') if dep_stream || !dep_encrypter.nil?
@file_name = file_name
@output_stream = if stream
iostream = Zip::RUNNING_ON_WINDOWS ? @file_name : @file_name.dup
@output_stream = if stream || dep_stream
iostream = @file_name.dup
iostream.reopen(@file_name)
iostream.rewind
iostream
else
::File.new(@file_name, 'wb')
end
@cdir = ::Zip::CentralDirectory.new
@entry_set = ::Zip::EntrySet.new
@compressor = ::Zip::NullCompressor.instance
@encrypter = encrypter || ::Zip::NullEncrypter.new
@encrypter = encrypter || dep_encrypter || ::Zip::NullEncrypter.new
@closed = false
@current_entry = nil
@comment = nil
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(file_name, encrypter: nil)
class << self
def open(file_name, dep_encrypter = nil, encrypter: nil)
return new(file_name) unless block_given?
zos = new(file_name, stream: false, encrypter: encrypter)
Zip.warn_about_v3_api('Zip::OutputStream.open') unless dep_encrypter.nil?
zos = new(file_name, stream: false, encrypter: (encrypter || dep_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, dep_encrypter = nil, encrypter: nil)
Zip.warn_about_v3_api('Zip::OutputStream.write_buffer') unless dep_encrypter.nil?
io.binmode if io.respond_to?(:binmode)
zos = new(io, stream: true, encrypter: encrypter)
zos = new(io, stream: true, encrypter: (encrypter || dep_encrypter))
yield zos
zos.close_buffer
end
@ -75,7 +78,7 @@ module Zip
finalize_current_entry
update_local_headers
@cdir.write_to_stream(@output_stream)
write_central_directory
@output_stream.close
@closed = true
end
@ -86,41 +89,37 @@ module Zip
finalize_current_entry
update_local_headers
@cdir.write_to_stream(@output_stream)
write_central_directory
@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 = '', extra = ExtraField.new,
compression_method = Entry::DEFLATED, level = Zip.default_compression
)
def put_next_entry(entry_name, comment = nil, extra = nil, 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.kind_of?(StreamableStream)
new_entry = if entry_name.kind_of?(Entry)
entry_name
else
Entry.new(
@file_name, entry_name.to_s, comment: comment, extra: extra,
compression_method: compression_method, compression_level: level
)
Entry.new(@file_name, entry_name.to_s)
end
init_next_entry(new_entry)
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)
@current_entry = new_entry
end
def copy_raw_entry(entry) # :nodoc:
def copy_raw_entry(entry)
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
@cdir << entry
@entry_set << entry
src_pos = entry.local_header_offset
entry.write_local_entry(@output_stream)
@compressor = NullCompressor.instance
@ -139,53 +138,55 @@ 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)
def init_next_entry(entry, level = Zip.default_compression)
finalize_current_entry
@cdir << entry
@entry_set << entry
entry.write_local_entry(@output_stream)
@encrypter.reset!
@output_stream << @encrypter.header(entry.mtime)
@compressor = get_compressor(entry)
@compressor = get_compressor(entry, level)
end
def get_compressor(entry)
def get_compressor(entry, level)
case entry.compression_method
when Entry::DEFLATED
::Zip::Deflater.new(@output_stream, entry.compression_level, @encrypter)
::Zip::Deflater.new(@output_stream, level, @encrypter)
when Entry::STORED
::Zip::PassThruCompressor.new(@output_stream)
else
raise ::Zip::CompressionMethodError, entry.compression_method
raise ::Zip::CompressionMethodError,
"Invalid compression method: '#{entry.compression_method}'"
end
end
def update_local_headers
pos = @output_stream.pos
@cdir.each do |entry|
@entry_set.each do |entry|
@output_stream.pos = entry.local_header_offset
entry.write_local_entry(@output_stream, rewrite: true)
entry.write_local_entry(@output_stream, 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 # :nodoc:
def finish
@compressor.finish
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
# frozen_string_literal: true
require_relative 'lib/zip/version'
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'zip/version'
Gem::Specification.new do |s|
s.name = 'rubyzip'
s.version = Zip::VERSION
s.version = ::Zip::VERSION
s.authors = ['Robert Haines', 'John Lees-Miller', 'Alexander Simonov']
s.email = [
'hainesr@gmail.com', 'jdleesmiller@gmail.com', 'alex@simonov.me'
@ -12,11 +12,9 @@ Gem::Specification.new do |s|
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.files = Dir.glob('{samples,lib}/**/*.rb') + %w[README.md TODO Rakefile]
s.require_paths = ['lib']
s.license = 'BSD-2-Clause'
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",
@ -25,15 +23,35 @@ Gem::Specification.new do |s|
'wiki_uri' => 'https://github.com/rubyzip/rubyzip/wiki',
'rubygems_mfa_required' => 'true'
}
s.required_ruby_version = '>= 2.4'
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.post_install_message = <<~ENDBANNER
RubyZip 3.0 is coming!
**********************
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'
The public API of some Rubyzip classes has been modernized to use named
parameters for optional arguments. Please check your usage of the
following classes:
* `Zip::File`
* `Zip::Entry`
* `Zip::InputStream`
* `Zip::OutputStream`
* `Zip::DOSTime`
Run your test suite with the `RUBYZIP_V3_API_WARN` environment
variable set to see warnings about usage of the old API. This will
help you to identify any changes that you need to make to your code.
See https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x for
more information.
Please ensure that your Gemfiles and .gemspecs are suitably restrictive
to avoid an unexpected breakage when 3.0 is released (e.g. ~> 2.3.0).
See https://github.com/rubyzip/rubyzip for details. The Changelog also
lists other enhancements and bugfixes that have been implemented since
version 2.3.0.
ENDBANNER
end

4
samples/.cvsignore Normal file
View File

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

View File

@ -1,5 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH << '../lib'
system('zip example.zip example.rb gtk_ruby_zip.rb')
@ -21,8 +20,7 @@ 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.
@ -72,11 +70,8 @@ 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,5 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH << '../lib'
@ -7,9 +6,9 @@ require 'zip/filesystem'
EXAMPLE_ZIP = 'filesystem.zip'
FileUtils.rm_f(EXAMPLE_ZIP)
File.delete(EXAMPLE_ZIP) if File.exist?(EXAMPLE_ZIP)
Zip::File.open(EXAMPLE_ZIP, create: true) do |zf|
Zip::File.open(EXAMPLE_ZIP, Zip::File::CREATE) 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,5 +1,3 @@
# frozen_string_literal: true
require 'zip'
# This is a simple example which uses rubyzip to
@ -23,7 +21,7 @@ class ZipFileGenerator
def write
entries = Dir.entries(@input_dir) - %w[. ..]
::Zip::File.open(@output_file, create: true) do |zipfile|
::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
write_entries entries, '', zipfile
end
end

View File

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

View File

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

View File

@ -1,11 +1,10 @@
#!/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,5 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$VERBOSE = true

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'test_helper'
class BasicZipFileTest < MiniTest::Test
@ -10,35 +8,44 @@ class BasicZipFileTest < MiniTest::Test
end
def test_entries
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)
assert_equal(TestZipFile::TEST_ZIP2.entry_names.sort,
@zip_file.entries.entries.sort.map(&:name))
end
def test_each
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)
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)
end
def test_foreach
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)
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)
end
def test_get_input_stream
expected_entry_names = TestZipFile::TEST_ZIP2.entry_names
actual_entry_names = []
count = 0
visited = {}
@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(expected_entry_names.sort, actual_entry_names.sort)
assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count)
end
def test_get_input_stream_block

View File

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

View File

@ -1,25 +1,21 @@
# 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']
].freeze
SRC_FILES = [['test/data/file1.txt', 'testfile.rb'],
['test/data/file2.txt', 'testFILE.rb']]
def teardown
::Zip.reset!
::Zip.case_insensitive_match = false
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, create: true)
SRC_FILES.each { |fn, _en| assert(::File.exist?(fn)) }
zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
SRC_FILES.each { |fn, en| zf.add(en, fn) }
zf.close
@ -37,21 +33,20 @@ 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, create: true)
SRC_FILES.each { |fn, _en| assert(::File.exist?(fn)) }
zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
error = assert_raises Zip::EntryExistsError do
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, create: true)
SRC_FILES.each { |fn, _en| assert(::File.exist?(fn)) }
zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
SRC_FILES.each { |fn, en| zf.add(en, fn) }
zf.close

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'test_helper'
class ZipCentralDirectoryEntryTest < MiniTest::Test
@ -59,43 +57,13 @@ class ZipCentralDirectoryEntryTest < MiniTest::Test
end
end
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
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(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))
entry.read_c_dir_entry(fragment)
raise 'ZipError expected'
rescue ::Zip::Error
end
end

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'test_helper'
class ZipCentralDirectoryTest < MiniTest::Test
@ -9,11 +7,12 @@ class ZipCentralDirectoryTest < MiniTest::Test
def test_read_from_stream
::File.open(TestZipFile::TEST_ZIP2.zip_name, 'rb') do |zip_file|
cdir = ::Zip::CentralDirectory.new
cdir.read_from_stream(zip_file)
cdir = ::Zip::CentralDirectory.read_from_stream(zip_file)
assert_equal(TestZipFile::TEST_ZIP2.entry_names.size, cdir.size)
assert_equal(cdir.entries.map(&:name).sort, TestZipFile::TEST_ZIP2.entry_names.sort)
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(TestZipFile::TEST_ZIP2.comment, cdir.comment)
end
end
@ -27,52 +26,21 @@ class ZipCentralDirectoryTest < MiniTest::Test
rescue ::Zip::Error
end
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
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
end
def test_write_to_stream
entries = [
::Zip::Entry.new(
'file.zip', 'flimse',
comment: 'myComment', extra: 'somethingExtra'
),
entries = [::Zip::Entry.new('file.zip', 'flimse', 'myComment', 'somethingExtra'),
::Zip::Entry.new('file.zip', 'secondEntryName'),
::Zip::Entry.new('file.zip', 'lastEntry.txt', comment: 'Has a comment')
]
::Zip::Entry.new('file.zip', 'lastEntry.txt', 'Has a comment too')]
cdir = ::Zip::CentralDirectory.new(entries, 'my zip comment')
File.open('test/data/generated/cdirtest.bin', 'wb') do |f|
@ -87,60 +55,50 @@ class ZipCentralDirectoryTest < MiniTest::Test
assert_equal(cdir.entries.sort, cdir_readback.entries.sort)
end
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")
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
end
cdir = Zip::CentralDirectory.new(entries)
cdir = ::Zip::CentralDirectory.new(entries, 'zip comment')
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(0x10000, cdir_readback.size)
assert_equal(Zip::VERSION_NEEDED_TO_EXTRACT_ZIP64, cdir_readback.instance_variable_get(:@version_needed_for_extract))
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))
end
def test_equality
cdir1 = ::Zip::CentralDirectory.new(
[
::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
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', extra: 'somethingExtra'),
::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', extra: 'somethingExtra'),
::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', extra: 'somethingExtra'),
::Zip::Entry.new('file.zip', 'lastEntry.txt')
],
'comment?'
)
::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?')
assert_equal(cdir1, cdir1)
assert_equal(cdir1, cdir2)

View File

@ -1,8 +1,12 @@
# frozen_string_literal: true
require 'test_helper'
class ConstantsTest < MiniTest::Test
def test_version_number_format
# Should at least start with MAJoR.MINOR.PATCH, but
# allow for additional version information after that.
assert_match(/\A\d+\.\d+\.\d+/, Zip::VERSION)
end
def test_compression_methods
assert_equal(0, Zip::COMPRESSION_METHOD_STORE)
assert_equal(1, Zip::COMPRESSION_METHOD_SHRINK)

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'test_helper'
class DeflaterTest < MiniTest::Test
@ -10,10 +8,6 @@ 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)
@ -49,7 +43,7 @@ class DeflaterTest < MiniTest::Test
private
def load_file(filename)
File.binread(filename)
File.open(filename, 'rb', &:read)
end
def deflate(data, filename)

View File

@ -1,86 +0,0 @@
# 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,68 +1,42 @@
# 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.reset!
Zip.default_compression = @default_compression
end
def test_encrypt
content = File.read(INPUT_FILE1)
test_filename = 'top_secret_file.txt'
test_file = ::File.open(ENCRYPT_ZIP_TEST_FILE, 'rb').read
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
@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
end
end
assert_match(/Zlib error \('.+'\) while inflating\./, error.message)
@output.unpack('C*').each_with_index do |c, i|
assert_equal test_file[i].ord, c
end
end
def test_decrypt
Zip::InputStream.open(
ENCRYPT_ZIP_TEST_FILE,
decrypter: Zip::TraditionalDecrypter.new('password')
) do |zis|
Zip::InputStream.open(ENCRYPT_ZIP_TEST_FILE, 0, Zip::TraditionalDecrypter.new('password')) do |zis|
entry = zis.get_next_entry
assert_equal 'file1.txt', entry.name
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
assert_equal 1327, entry.size
assert_equal ::File.open(INPUT_FILE1, 'r').read, zis.read
end
end
end

View File

@ -1,16 +1,14 @@
# frozen_string_literal: true
require 'test_helper'
class ZipEntrySetTest < MiniTest::Test
ZIP_ENTRIES = [
::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
::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')
]
def setup
@zip_entry_set = ::Zip::EntrySet.new(ZIP_ENTRIES)
@ -22,17 +20,13 @@ 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', comment: 'aComment')
)
)
assert(!@zip_entry_set.include?(::Zip::Entry.new('different.zip', 'different', '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', comment: 'c')
@zip_entry_set << ::Zip::Entry.new('a', 'b', 'c')
assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.length)
end
@ -60,19 +54,11 @@ 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
@ -80,9 +66,7 @@ class ZipEntrySetTest < MiniTest::Test
end
def test_find_entry
entries = [
::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', comment: 'comment1')
]
entries = [::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', 'comment1')]
::Zip.case_insensitive_match = true
zip_entry_set = ::Zip::EntrySet.new(entries)
@ -109,20 +93,10 @@ 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', comment: "new entry's comment"
)
new_entry = ::Zip::Entry.new('zf.zip', 'new entry', "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,23 +1,18 @@
# 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,
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
)
entry = ::Zip::Entry.new(TEST_ZIPFILE,
TEST_NAME,
TEST_COMMENT,
TEST_EXTRA,
TEST_COMPRESSED_SIZE,
TEST_CRC,
TEST_COMPRESSIONMETHOD,
TEST_SIZE,
TEST_TIME)
assert_equal(TEST_COMMENT, entry.comment)
assert_equal(TEST_COMPRESSED_SIZE, entry.compressed_size)
@ -26,10 +21,7 @@ class ZipEntryTest < MiniTest::Test
assert_equal(TEST_COMPRESSIONMETHOD, entry.compression_method)
assert_equal(TEST_NAME, entry.name)
assert_equal(TEST_SIZE, entry.size)
# Reverse times when testing because we need to use DOSTime#== for the
# comparison, not Time#==.
assert_equal(entry.time, TEST_TIME)
assert_equal(TEST_TIME, entry.time)
end
def test_is_directory_and_is_file
@ -47,54 +39,30 @@ class ZipEntryTest < MiniTest::Test
end
def test_equality
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
)
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)
assert_equal(entry1, entry1)
assert_equal(entry1, entry2)
@ -150,53 +118,39 @@ class ZipEntryTest < MiniTest::Test
end
def test_entry_name_cannot_start_with_slash
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)
assert_raises(::Zip::EntryNameError) { ::Zip::Entry.new('zf.zip', '/hej/der') }
end
def test_store_file_without_compression
Dir.mktmpdir do |tmp|
tmp_zip = File.join(tmp, 'no_compress.zip')
File.delete('/tmp/no_compress.zip') if File.exist?('/tmp/no_compress.zip')
files = Dir[File.join('test/data/globTest', '**', '**')]
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
zipfile = Zip::File.open('/tmp/no_compress.zip', Zip::File::CREATE)
mimetype_entry = Zip::Entry.new(zipfile, # @zipfile
'mimetype', # @name
compression_method: Zip::Entry::STORED
)
'', # @comment
'', # @extra
0, # @compressed_size
0, # @crc
Zip::Entry::STORED) # @comppressed_method
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')
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
end
def test_encrypted?
entry = Zip::Entry.new
@ -215,137 +169,4 @@ 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

33
test/errors_test.rb Normal file
View File

@ -0,0 +1,33 @@
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,56 +1,30 @@
# 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('foo', extra['Unknown'].to_c_dir_bin)
assert_equal(extra['Unknown'], 'foo')
extra.merge('a')
assert_equal('fooa', extra['Unknown'].to_c_dir_bin)
assert_equal(extra['Unknown'], 'fooa')
extra.merge('barbaz')
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)
assert_equal(extra.to_s, 'fooabarbaz')
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
@ -78,9 +52,9 @@ class ZipExtraFieldTest < MiniTest::Test
extra = ::Zip::ExtraField.new(str)
assert_instance_of(String, extra.to_s)
extra_len = extra.to_s.length
extra.merge('foo', local: true)
assert_equal(extra_len + 3, extra.to_s.length)
s = extra.to_s
extra.merge('foo')
assert_equal(s.length + 3, extra.to_s.length)
end
def test_equality
@ -99,26 +73,4 @@ 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

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

View File

@ -1,11 +1,8 @@
# frozen_string_literal: true
require 'test_helper'
class ZipFileExtractTest < MiniTest::Test
include CommonZipFileFixture
EXTRACTED_FILENAME = 'test/data/generated/extEntry'
EXTRACTED_FILENAME_ABS = ::File.absolute_path(EXTRACTED_FILENAME)
ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entry_names.reverse
def setup
@ -38,14 +35,13 @@ class ZipFileExtractTest < MiniTest::Test
def test_extract_exists
text = 'written text'
::File.write(EXTRACTED_FILENAME, text)
::File.open(EXTRACTED_FILENAME, 'w') { |f| f.write(text) }
assert_raises(::Zip::DestinationExistsError) do
assert_raises(::Zip::DestinationFileExistsError) do
::Zip::File.open(TEST_ZIP.zip_name) do |zf|
zf.extract(zf.entries.first, EXTRACTED_FILENAME)
end
end
File.open(EXTRACTED_FILENAME, 'r') do |f|
assert_equal(text, f.read)
end
@ -53,13 +49,13 @@ class ZipFileExtractTest < MiniTest::Test
def test_extract_exists_overwrite
text = 'written text'
::File.write(EXTRACTED_FILENAME, text)
::File.open(EXTRACTED_FILENAME, 'w') { |f| f.write(text) }
called_correctly = false
::Zip::File.open(TEST_ZIP.zip_name) do |zf|
zf.extract(zf.entries.first, EXTRACTED_FILENAME) do |entry, extract_loc|
called_correctly = zf.entries.first == entry &&
extract_loc == EXTRACTED_FILENAME_ABS
extract_loc == EXTRACTED_FILENAME
true
end
end
@ -77,7 +73,7 @@ class ZipFileExtractTest < MiniTest::Test
zf.close if zf
end
def test_extract_another_non_entry
def test_extract_non_entry_2
out_file = 'outfile'
assert_raises(Errno::ENOENT) do
zf = ::Zip::File.new(TEST_ZIP.zip_name)
@ -90,8 +86,6 @@ class ZipFileExtractTest < MiniTest::Test
end
def test_extract_incorrect_size
Zip.write_zip64_support = false
# The uncompressed size fields in the zip file cannot be trusted. This makes
# it harder for callers to validate the sizes of the files they are
# extracting, which can lead to denial of service. See also
@ -103,7 +97,7 @@ class ZipFileExtractTest < MiniTest::Test
true_size = 500_000
fake_size = 1
::Zip::File.open(real_zip, create: true) do |zf|
::Zip::File.open(real_zip, ::Zip::File::CREATE) do |zf|
zf.get_output_stream(file_name) do |os|
os.write 'a' * true_size
end
@ -116,14 +110,16 @@ class ZipFileExtractTest < MiniTest::Test
assert_equal true_size, a_entry.size
end
true_size_bytes = [compressed_size, true_size, file_name.size].pack('VVv')
fake_size_bytes = [compressed_size, fake_size, file_name.size].pack('VVv')
true_size_bytes = [compressed_size, true_size, file_name.size].pack('LLS')
fake_size_bytes = [compressed_size, fake_size, file_name.size].pack('LLS')
data = File.binread(real_zip)
assert data.include?(true_size_bytes)
data.gsub! true_size_bytes, fake_size_bytes
File.binwrite(fake_zip, data)
File.open(fake_zip, 'wb') do |file|
file.write data
end
Dir.chdir tmp do
::Zip::File.open(fake_zip) do |zf|
@ -131,7 +127,7 @@ class ZipFileExtractTest < MiniTest::Test
assert_equal fake_size, a_entry.size
::Zip.validate_entry_sizes = false
assert_output('', /.+'a'.+1B.+/) do
assert_output('', /.+\'a\'.+1B.+/) do
a_entry.extract
end
assert_equal true_size, File.size(file_name)
@ -141,72 +137,9 @@ class ZipFileExtractTest < MiniTest::Test
error = assert_raises ::Zip::EntrySizeError do
a_entry.extract
end
assert_equal(
"Entry 'a' should be 1B, but is larger when inflated.",
assert_equal \
"entry 'a' should be 1B, but is larger when inflated.",
error.message
)
end
end
end
end
def test_extract_incorrect_size_zip64
# The uncompressed size fields in the zip file cannot be trusted. This makes
# it harder for callers to validate the sizes of the files they are
# extracting, which can lead to denial of service. See also
# https://en.wikipedia.org/wiki/Zip_bomb
#
# This version of the test ensures that fraudulent sizes in the ZIP64
# extensions are caught.
Dir.mktmpdir do |tmp|
real_zip = File.join(tmp, 'real.zip')
fake_zip = File.join(tmp, 'fake.zip')
file_name = 'a'
true_size = 500_000
fake_size = 1
::Zip::File.open(real_zip, create: true) do |zf|
zf.get_output_stream(file_name) do |os|
os.write 'a' * true_size
end
end
compressed_size = nil
::Zip::File.open(real_zip) do |zf|
a_entry = zf.find_entry(file_name)
compressed_size = a_entry.compressed_size
assert_equal true_size, a_entry.size
end
true_size_bytes = [0x1, 16, true_size, compressed_size].pack('vvQ<Q<')
fake_size_bytes = [0x1, 16, fake_size, compressed_size].pack('vvQ<Q<')
data = File.binread(real_zip)
assert data.include?(true_size_bytes)
data.gsub! true_size_bytes, fake_size_bytes
File.binwrite(fake_zip, data)
Dir.chdir tmp do
::Zip::File.open(fake_zip) do |zf|
a_entry = zf.find_entry(file_name)
assert_equal fake_size, a_entry.size
::Zip.validate_entry_sizes = false
assert_output('', /.+'a'.+1B.+/) do
a_entry.extract
end
assert_equal true_size, File.size(file_name)
FileUtils.rm file_name
::Zip.validate_entry_sizes = true
error = assert_raises ::Zip::EntrySizeError do
a_entry.extract
end
assert_equal(
"Entry 'a' should be 1B, but is larger when inflated.",
error.message
)
end
end
end

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