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

View File

@ -1,20 +1,9 @@
require:
- rubocop-performance
- rubocop-rake
inherit_from: .rubocop_todo.yml inherit_from: .rubocop_todo.yml
# Set this to the minimum supported ruby in the gemspec. Otherwise # Set this to the minimum supported ruby in the gemspec. Otherwise
# we get errors if our ruby version doesn't match. # we get errors if our ruby version doesn't match.
AllCops: AllCops:
SuggestExtensions: false TargetRubyVersion: 2.4
TargetRubyVersion: 3.0
NewCops: enable
# Allow this in this file because adding the extra lines is pointless.
Layout/EmptyLineBetweenDefs:
Exclude:
- 'lib/zip/errors.rb'
Layout/HashAlignment: Layout/HashAlignment:
EnforcedHashRocketStyle: table EnforcedHashRocketStyle: table
@ -23,13 +12,10 @@ Layout/HashAlignment:
# Set a workable line length, given the current state of the code, # Set a workable line length, given the current state of the code,
# and turn off for the tests. # and turn off for the tests.
Layout/LineLength: Layout/LineLength:
Max: 100 Max: 135
Exclude: Exclude:
- 'test/**/*.rb' - 'test/**/*.rb'
Lint/EmptyClass:
Enabled: false
# In some cases we just need to catch an exception, rather than # In some cases we just need to catch an exception, rather than
# actually handle it. Allow the tests to make use of this shortcut. # actually handle it. Allow the tests to make use of this shortcut.
Lint/SuppressedException: Lint/SuppressedException:
@ -37,11 +23,6 @@ Lint/SuppressedException:
Exclude: Exclude:
- 'test/**/*.rb' - '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 # Turn off ABC metrics for the tests and set a workable max given
# the current state of the code. # the current state of the code.
Metrics/AbcSize: Metrics/AbcSize:
@ -49,10 +30,9 @@ Metrics/AbcSize:
Exclude: Exclude:
- 'test/**/*.rb' - 'test/**/*.rb'
# Turn block length metrics off for the tests and gemspec. # Turn block length metrics off for the tests.
Metrics/BlockLength: Metrics/BlockLength:
Exclude: Exclude:
- 'rubyzip.gemspec'
- 'test/**/*.rb' - 'test/**/*.rb'
# Turn class length metrics off for the tests. # Turn class length metrics off for the tests.
@ -65,31 +45,10 @@ Metrics/MethodLength:
Exclude: Exclude:
- 'test/**/*.rb' - '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. # Set a consistent way of checking types.
Style/ClassCheck: Style/ClassCheck:
EnforcedStyle: kind_of? 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 # Allow this multi-line block chain as it actually reads better
# than the alternatives. # than the alternatives.
Style/MultilineBlockChain: Style/MultilineBlockChain:

View File

@ -1,61 +1,54 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `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 # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
Gemspec/DevelopmentDependencies: # Offense count: 15
Enabled: false # Configuration parameters: CountComments.
# Offense count: 7
Lint/MissingSuper:
Exclude:
- 'lib/zip/extra_field.rb'
- 'lib/zip/extra_field/ntfs.rb'
- 'lib/zip/extra_field/old_unix.rb'
- 'lib/zip/extra_field/universal_time.rb'
- 'lib/zip/extra_field/unix.rb'
- 'lib/zip/extra_field/zip64.rb'
- 'lib/zip/extra_field/zip64_placeholder.rb'
# Offense count: 5
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength: Metrics/ClassLength:
Max: 650 Max: 600
# Offense count: 21 # Offense count: 26
# Configuration parameters: IgnoredMethods.
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 14 Max: 14
# Offense count: 47 # Offense count: 120
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. # Configuration parameters: CountComments, ExcludedMethods.
Metrics/MethodLength: Metrics/MethodLength:
Max: 34 Max: 32
# Offense count: 5 # Offense count: 2
# Configuration parameters: CountKeywordArgs. # Configuration parameters: CountKeywordArgs.
Metrics/ParameterLists: Metrics/ParameterLists:
Max: 11 Max: 10
MaxOptionalParameters: 9
# Offense count: 14 # Offense count: 21
# Configuration parameters: IgnoredMethods.
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 15 Max: 15
# Offense count: 7 # Offense count: 9
Naming/AccessorMethodName: Naming/AccessorMethodName:
Exclude: Exclude:
- 'lib/zip/entry.rb' - 'lib/zip/entry.rb'
- 'lib/zip/filesystem.rb'
- 'lib/zip/input_stream.rb' - 'lib/zip/input_stream.rb'
- 'lib/zip/streamable_stream.rb' - 'lib/zip/streamable_stream.rb'
# Offense count: 7 # Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # 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 # SupportedStyles: nested, compact
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Exclude: Exclude:
@ -64,49 +57,71 @@ Style/ClassAndModuleChildren:
- 'lib/zip/extra_field/old_unix.rb' - 'lib/zip/extra_field/old_unix.rb'
- 'lib/zip/extra_field/universal_time.rb' - 'lib/zip/extra_field/universal_time.rb'
- 'lib/zip/extra_field/unix.rb' - 'lib/zip/extra_field/unix.rb'
- 'lib/zip/extra_field/unknown.rb'
- 'lib/zip/extra_field/zip64.rb' - 'lib/zip/extra_field/zip64.rb'
- 'lib/zip/extra_field/zip64_placeholder.rb' - 'lib/zip/extra_field/zip64_placeholder.rb'
# Offense count: 22 # Offense count: 26
# Configuration parameters: AllowedConstants.
Style/Documentation: Style/Documentation:
Enabled: false 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. # Cop supports --auto-correct.
Style/IfUnlessModifier: Style/IfUnlessModifier:
Exclude: Exclude:
- 'lib/zip/entry.rb' - 'lib/zip/entry.rb'
- 'lib/zip/file_split.rb' - 'lib/zip/extra_field/generic.rb'
- 'lib/zip/filesystem/dir.rb' - 'lib/zip/file.rb'
- 'lib/zip/filesystem/file.rb' - 'lib/zip/filesystem.rb'
- 'lib/zip/input_stream.rb'
- 'lib/zip/pass_thru_decompressor.rb' - 'lib/zip/pass_thru_decompressor.rb'
- 'lib/zip/streamable_stream.rb' - 'lib/zip/streamable_stream.rb'
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. # 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. # Configuration parameters: EnforcedStyle.
# SupportedStyles: literals, strict # SupportedStyles: literals, strict
Style/MutableConstant: Style/MutableConstant:
Exclude: Enabled: false
- 'lib/zip/extra_field.rb'
# Offense count: 21 # Offense count: 23
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, IgnoredMethods. # Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
# SupportedStyles: predicate, comparison # SupportedStyles: predicate, comparison
Style/NumericPredicate: Style/NumericPredicate:
Exclude: Exclude:
- 'spec/**/*'
- 'lib/zip/entry.rb' - 'lib/zip/entry.rb'
- 'lib/zip/extra_field/old_unix.rb' - 'lib/zip/extra_field/old_unix.rb'
- 'lib/zip/extra_field/universal_time.rb' - 'lib/zip/extra_field/universal_time.rb'
- 'lib/zip/extra_field/unix.rb' - 'lib/zip/extra_field/unix.rb'
- 'lib/zip/file.rb' - 'lib/zip/file.rb'
- 'lib/zip/filesystem/file.rb' - 'lib/zip/filesystem.rb'
- 'lib/zip/input_stream.rb' - 'lib/zip/input_stream.rb'
- 'lib/zip/ioextras.rb' - 'lib/zip/ioextras.rb'
- 'lib/zip/ioextras/abstract_input_stream.rb' - 'lib/zip/ioextras/abstract_input_stream.rb'
- 'test/file_split_test.rb'
- 'test/test_helper.rb'
# Offense count: 17 # Offense count: 17
# Cop supports --auto-correct. # 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) # X.X.X (Next)
- Fix de facto regression for input streams.
- Fix `File#write_buffer` to always return the given `io`.
- Add `Entry#absolute_time?` and `DOSTime#absolute_time?` methods.
- Use explicit named parameters for `File` methods.
- Ensure that entries can be extracted safely without path traversal. [#540](https://github.com/rubyzip/rubyzip/issues/540)
- Enable Zip64 by default.
- Rename `GPFBit3Error` to `StreamingError`.
- Ensure that `Entry.ftype` is correct via `InputStream`. [#533](https://github.com/rubyzip/rubyzip/issues/533)
- Add `Entry#zip64?` as a better way detect Zip64 entries.
- Implement `Zip::FileSystem::ZipFsFile#symlink?`.
- Remove `File::add_buffer` from the API.
- Fix `OutputStream#put_next_entry` to preserve `StreamableStream`s. [#503](https://github.com/rubyzip/rubyzip/issues/503)
- Ensure `File.open_buffer` doesn't rewrite unchanged data.
- Add `CentralDirectory#count_entries` and `File::count_entries`.
- Fix reading unknown extra fields. [#505](https://github.com/rubyzip/rubyzip/issues/505)
- Fix reading zip files with max length file comment. [#508](https://github.com/rubyzip/rubyzip/issues/508)
- Fix reading zip64 files with max length file comment. [#509](https://github.com/rubyzip/rubyzip/issues/509)
- Don't silently alter zip files opened with `Zip::sort_entries`. [#329](https://github.com/rubyzip/rubyzip/issues/329)
- Use named parameters for optional arguments in the public API.
- Raise an error if entry names exceed 65,535 characters. [#247](https://github.com/rubyzip/rubyzip/issues/247)
- Remove the `ZipXError` v1 legacy classes.
- Raise an error on reading a split archive with `InputStream`. [#349](https://github.com/rubyzip/rubyzip/issues/349)
- Ensure `InputStream` raises `GPFBit3Error` for OSX Archive files. [#493](https://github.com/rubyzip/rubyzip/issues/493)
- Improve documentation and error messages for `InputStream`. [#196](https://github.com/rubyzip/rubyzip/issues/196)
- Fix zip file-level comment is not read from zip64 files. [#492](https://github.com/rubyzip/rubyzip/issues/492)
- Fix `Zip::OutputStream.write_buffer` doesn't work with Tempfiles. [#265](https://github.com/rubyzip/rubyzip/issues/265)
- Reinstate normalising pathname separators to /. [#487](https://github.com/rubyzip/rubyzip/pull/487)
- Fix restore options consistency. [#486](https://github.com/rubyzip/rubyzip/pull/486)
- View and/or preserve original date created, date modified? (Windows). [#336](https://github.com/rubyzip/rubyzip/issues/336)
- Fix frozen string literal error. [#475](https://github.com/rubyzip/rubyzip/pull/475)
- Set the default `Entry` time to the file's mtime on Windows. [#465](https://github.com/rubyzip/rubyzip/issues/465)
- Ensure that `Entry#time=` sets times as `DOSTime` objects. [#481](https://github.com/rubyzip/rubyzip/issues/481)
- Replace and deprecate `Zip::DOSTime#dos_equals`. [#464](https://github.com/rubyzip/rubyzip/pull/464)
- Fix loading extra fields. [#459](https://github.com/rubyzip/rubyzip/pull/459)
- Set compression level on a per-zipfile basis. [#448](https://github.com/rubyzip/rubyzip/pull/448)
- Fix input stream partial read error. [#462](https://github.com/rubyzip/rubyzip/pull/462)
- Fix zlib deflate buffer growth. [#447](https://github.com/rubyzip/rubyzip/pull/447)
Tooling/internal:
- Add a test to ensure correct version number format.
- Update the README with new Ruby version compatability information.
- Fix various issues with JRuby tests.
- Update gem dependency versions.
- Add Ruby 3.4 to the CI.
- Fix mispelled variable names in the crypto classes.
- Only use the Zip64 CDIR end locator if needed.
- Prevent unnecessary Zip64 data being stored.
- Abstract marking various things as 'dirty' into `Dirtyable` for reuse.
- Properly test `File#mkdir`.
- Remove unused private method `File#directory?`.
- Expose the `EntrySet` more cleanly through `CentralDirectory`.
- `Zip::File` no longer subclasses `Zip::CentralDirectory`.
- Configure Coveralls to not report a failure on minor decreases of test coverage. [#491](https://github.com/rubyzip/rubyzip/issues/491)
- Extract the file splitting code out into its own module.
- Refactor, and tidy up, the `Zip::Filesystem` classes for improved maintainability.
- Fix Windows tests. [#489](https://github.com/rubyzip/rubyzip/pull/489)
- Refactor `assert_forwarded` so it does not need `ObjectSpace._id2ref` or `eval`. [#483](https://github.com/rubyzip/rubyzip/pull/483)
- Add GitHub Actions CI infrastructure. [#469](https://github.com/rubyzip/rubyzip/issues/469)
- Add Ruby 3.0 to CI. [#474](https://github.com/rubyzip/rubyzip/pull/474)
- Fix the compression level tests to compare relative sizes. [#473](https://github.com/rubyzip/rubyzip/pull/473)
- Simplify assertions in basic_zip_file_test. [#470](https://github.com/rubyzip/rubyzip/pull/470)
- Remove compare_enumerables from test_helper.rb. [#468](https://github.com/rubyzip/rubyzip/pull/468)
- Use correct SPDX license identifier. [#458](https://github.com/rubyzip/rubyzip/pull/458)
- Enable truffle ruby in Travis CI. [#450](https://github.com/rubyzip/rubyzip/pull/450)
- Update rubocop again and run it in CI. [#444](https://github.com/rubyzip/rubyzip/pull/444)
- Fix a test that was incorrect on big-endian architectures. [#445](https://github.com/rubyzip/rubyzip/pull/445)
# 2.4.1 (2025-01-05) # 2.4.1 (2025-01-05)
@ -93,11 +25,11 @@ Tooling:
# 2.3.2 (2021-07-05) # 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) # 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) # 2.3.0 (2020-03-14)

View File

@ -1,12 +1,3 @@
# frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
gemspec 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 guard :minitest do
# with Minitest::Unit # 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{^lib/zip/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
watch(%r{^test/test_helper\.rb$}) { 'test' } watch(%r{^test/test_helper\.rb$}) { 'test' }
end 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.

174
README.md
View File

@ -2,21 +2,23 @@
[![Gem Version](https://badge.fury.io/rb/rubyzip.svg)](http://badge.fury.io/rb/rubyzip) [![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) [![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) [![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) [![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. 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 ### 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` * `File`
* `Entry` * `Entry`
* `InputStream` * `InputStream`
* `OutputStream` * `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.** **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" 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| input_filenames.each do |filename|
# Two arguments: # Two arguments:
# - The name of the file as it will appear in the archive # - The name of the file as it will appear in the archive
@ -94,7 +96,7 @@ class ZipFileGenerator
def write def write
entries = Dir.entries(@input_dir) - %w[. ..] 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 write_entries entries, '', zipfile
end end
end end
@ -127,9 +129,9 @@ class ZipFileGenerator
end 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/ Vegetable/
@ -143,7 +145,7 @@ fruit/mango
fruit/orange 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 ### Default permissions of zip archives
@ -179,71 +181,28 @@ Zip::File.open('foo.zip') do |zip_file|
end 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`). If `::Zip::InputStream` finds such entry in the zip archive it will raise an exception.
`Zip::InputStream` is not designed to be used for random access in a zip file. When performing any operations on an entry that you are accessing via `Zip::InputStream.get_next_entry` then you should complete any such operations before the next call to `get_next_entry`.
```ruby
zip_stream = Zip::InputStream.new(File.open('file.zip'))
while entry = zip_stream.get_next_entry
# All required operations on `entry` go here.
end
```
Any attempt to move about in a zip file opened with `Zip::InputStream` could result in the incorrect entry being accessed and/or Zlib buffer errors. If you need random access in a zip file, use `Zip::File`.
### Password Protection (Experimental) ### 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.: 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 ```ruby
# Writing. Zip::OutputStream.write_buffer(::StringIO.new, Zip::TraditionalEncrypter.new('password')) do |out|
enc = Zip::TraditionalEncrypter.new('password') out.put_next_entry("my_file.txt")
buffer = Zip::OutputStream.write_buffer(::StringIO.new(''), enc) do |output| out.write my_data
output.put_next_entry("my_file.txt") end.string
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
``` ```
#### Version 3.x This is an experimental feature and the interface for encryption may change in future versions.
```ruby
# Writing.
enc = Zip::TraditionalEncrypter.new('password')
buffer = Zip::OutputStream.write_buffer(encrypter: enc) do |output|
output.put_next_entry("my_file.txt")
output.write my_data
end
# Reading.
dec = Zip::TraditionalDecrypter.new('password')
Zip::InputStream.open(buffer, decrypter: dec) do |input|
entry = input.get_next_entry
puts "Contents of '#{entry.name}':"
puts input.read
end
```
_This is an experimental feature and the interface for encryption may change in future versions._
## Known issues ## Known issues
@ -257,7 +216,7 @@ buffer = Zip::OutputStream.write_buffer do |out|
unless [DOCUMENT_FILE_PATH, RELS_FILE_PATH].include?(e.name) unless [DOCUMENT_FILE_PATH, RELS_FILE_PATH].include?(e.name)
out.put_next_entry(e.name) out.put_next_entry(e.name)
out.write e.get_input_stream.read out.write e.get_input_stream.read
end end
end end
out.put_next_entry(DOCUMENT_FILE_PATH) out.put_next_entry(DOCUMENT_FILE_PATH)
@ -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. 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 set the default compression level like so:
You can configure the default compression level with:
```ruby ```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. It defaults to `Zlib::DEFAULT_COMPRESSION`. Possible values are `Zlib::BEST_COMPRESSION`, `Zlib::DEFAULT_COMPRESSION` and `Zlib::NO_COMPRESSION`
This can also be set for each archive as an option to `Zip::File`:
```ruby
Zip::File.open('foo.zip', create:true, compression_level: 9) do |zip|
zip.add ...
end
```
### Zip64 Support ### 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 ```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 you will enable Zip64 writing then you will need zip extractor with Zip64 support to extract archive.
_NOTE_: If Zip64 write support is enabled then any extractor subsequently used may also require Zip64 support to read from the resultant archive.
### Block Form ### Block Form
@ -382,50 +329,15 @@ You can set multiple settings at the same time by using a block:
end 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 ## Developing
Install the dependencies: To run the test you need to do this:
```shell
bundle install
``` ```
bundle install
Run the tests with `rake`:
```shell
rake 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 ## Website and Project Home
http://github.com/rubyzip/rubyzip http://github.com/rubyzip/rubyzip
@ -434,29 +346,17 @@ http://rdoc.info/github/rubyzip/rubyzip/master/frames
## Authors ## 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) Thomas Sondergaard (thomas at sondergaard.cc)
* John Lees-Miller (@jdleesmiller)
* Oleksandr Simonov (@simonoff)
### Original author Technorama Ltd. (oss-ruby-zip at technorama.net)
* Thomas Sondergaard extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org)
## License ## 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. Rubyzip is distributed under the same license as ruby. See
http://www.ruby-lang.org/en/LICENSE.txt
## Research notice
Please note that this repository is participating in a study into sustainability
of open source projects. Data will be gathered about this repository for
approximately the next 12 months, starting from June 2021.
Data collected will include number of contributors, number of PRs, time taken to
close/merge these PRs, and issues closed.
For more information, please visit
[our informational page](https://sustainable-open-science-and-software.github.io/) or download our [participant information sheet](https://sustainable-open-science-and-software.github.io/assets/PIS_sustainable_software.pdf).

View File

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

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'English' require 'English'
require 'delegate' require 'delegate'
require 'singleton' require 'singleton'
@ -35,12 +33,26 @@ require 'zip/streamable_stream'
require 'zip/streamable_directory' require 'zip/streamable_directory'
require 'zip/errors' 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 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 extend self
attr_accessor :unicode_names, attr_accessor :unicode_names,
:on_exists_proc, :on_exists_proc,
@ -53,27 +65,19 @@ module Zip
:force_entry_names_encoding, :force_entry_names_encoding,
:validate_entry_sizes :validate_entry_sizes
DEFAULT_RESTORE_OPTIONS = { def reset!
restore_ownership: false,
restore_permissions: true,
restore_times: true
}.freeze # :nodoc:
def reset! # :nodoc:
@_ran_once = false @_ran_once = false
@unicode_names = false @unicode_names = false
@on_exists_proc = false @on_exists_proc = false
@continue_on_exists_proc = false @continue_on_exists_proc = false
@sort_entries = false @sort_entries = false
@default_compression = Zlib::DEFAULT_COMPRESSION @default_compression = ::Zlib::DEFAULT_COMPRESSION
@write_zip64_support = true @write_zip64_support = false
@warn_invalid_date = true @warn_invalid_date = true
@case_insensitive_match = false @case_insensitive_match = false
@force_entry_names_encoding = nil
@validate_entry_sizes = true @validate_entry_sizes = true
end end
# Set options for RubyZip in one block.
def setup def setup
yield self unless @_ran_once yield self unless @_ran_once
@_ran_once = true @_ran_once = true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
module Zip module Zip
class Deflater < Compressor # :nodoc:all class Deflater < Compressor #:nodoc:all
def initialize(output_stream, level = Zip.default_compression, encrypter = NullEncrypter.new) def initialize(output_stream, level = Zip.default_compression, encrypter = NullEncrypter.new)
super() super()
@output_stream = output_stream @output_stream = output_stream
@ -15,16 +13,16 @@ module Zip
val = data.to_s val = data.to_s
@crc = Zlib.crc32(val, @crc) @crc = Zlib.crc32(val, @crc)
@size += val.bytesize @size += val.bytesize
buffer = @zlib_deflater.deflate(data, Zlib::SYNC_FLUSH) buffer = @zlib_deflater.deflate(data)
return @output_stream if buffer.empty? if buffer.empty?
@output_stream
@output_stream << @encrypter.encrypt(buffer) else
@output_stream << @encrypter.encrypt(buffer)
end
end end
def finish def finish
buffer = @zlib_deflater.finish @output_stream << @encrypter.encrypt(@zlib_deflater.finish) until @zlib_deflater.finished?
@output_stream << @encrypter.encrypt(buffer) unless buffer.empty?
@zlib_deflater.close
end end
attr_reader :size, :crc 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,9 +1,5 @@
# frozen_string_literal: true
require 'rubygems'
module Zip module Zip
class DOSTime < Time # :nodoc:all class DOSTime < Time #:nodoc:all
# MS-DOS File Date and Time format as used in Interrupt 21H Function 57H: # MS-DOS File Date and Time format as used in Interrupt 21H Function 57H:
# Register CX, the Time: # Register CX, the Time:
@ -16,14 +12,6 @@ module Zip
# bits 5-8 month (1-12) # bits 5-8 month (1-12)
# bits 9-15 year (four digit year minus 1980) # 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 def to_binary_dos_time
(sec / 2) + (sec / 2) +
(min << 5) + (min << 5) +
@ -37,7 +25,8 @@ module Zip
end end
def dos_equals(other) def dos_equals(other)
warn 'Zip::DOSTime#dos_equals is deprecated. Use `==` instead.' Zip.warn_about_v3_api('DOSTime#dos_equals')
self == other self == other
end end
@ -61,35 +50,7 @@ module Zip
month = (0b111100000 & bin_dos_date) >> 5 month = (0b111100000 & bin_dos_date) >> 5
year = ((0b1111111000000000 & bin_dos_date) >> 9) + 1980 year = ((0b1111111000000000 & bin_dos_date) >> 9) + 1980
time = local(year, month, day, hour, minute, second) local(year, month, day, hour, minute, second)
time.absolute_time = false
time
end
if defined? JRUBY_VERSION && Gem::Version.new(JRUBY_VERSION) < '9.2.18.0'
module JRubyCMP # :nodoc:
def ==(other)
(self <=> other).zero?
end
def <(other)
(self <=> other).negative?
end
def <=(other)
(self <=> other) <= 0
end
def >(other)
(self <=> other).positive?
end
def >=(other)
(self <=> other) >= 0
end
end
include JRubyCMP
end end
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
module Zip module Zip
class ExtraField::Generic # :nodoc: class ExtraField::Generic
def self.register_map def self.register_map
return unless const_defined?(:HEADER_ID) return unless const_defined?(:HEADER_ID)
@ -21,17 +19,26 @@ module Zip
return false return false
end 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 end
def to_local_bin def to_local_bin
s = pack_for_local 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 end
def to_c_dir_bin def to_c_dir_bin
s = pack_for_c_dir 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 end
end end

View File

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

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
module Zip module Zip
# Olf Info-ZIP Extra for UNIX uid/gid and file timestampes # 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' HEADER_ID = 'UX'
register_map register_map

View File

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

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
module Zip module Zip
# Info-ZIP Extra for UNIX uid/gid # Info-ZIP Extra for UNIX uid/gid
class ExtraField::IUnix < ExtraField::Generic # :nodoc: class ExtraField::IUnix < ExtraField::Generic
HEADER_ID = 'Ux' HEADER_ID = 'Ux'
register_map register_map
@ -22,8 +20,8 @@ module Zip
return if !size || size == 0 return if !size || size == 0
uid, gid = content.unpack('vv') uid, gid = content.unpack('vv')
@uid = uid @uid ||= uid
@gid = gid @gid ||= gid # rubocop:disable Naming/MemoizedInstanceVariableName
end end
def ==(other) 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 module Zip
# Info-ZIP Extra for Zip64 size # Info-ZIP Extra for Zip64 size
class ExtraField::Zip64 < ExtraField::Generic # :nodoc: class ExtraField::Zip64 < ExtraField::Generic
attr_accessor :compressed_size, :disk_start_number, attr_accessor :original_size, :compressed_size, :relative_header_offset, :disk_start_number
:original_size, :relative_header_offset
HEADER_ID = ['0100'].pack('H*') HEADER_ID = ['0100'].pack('H*')
register_map register_map
@ -40,9 +36,7 @@ module Zip
def parse(original_size, compressed_size, relative_header_offset = nil, disk_start_number = nil) def parse(original_size, compressed_size, relative_header_offset = nil, disk_start_number = nil)
@original_size = extract(8, 'Q<') if original_size == 0xFFFFFFFF @original_size = extract(8, 'Q<') if original_size == 0xFFFFFFFF
@compressed_size = extract(8, 'Q<') if compressed_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<') if relative_header_offset && relative_header_offset == 0xFFFFFFFF
@relative_header_offset = extract(8, 'Q<')
end
@disk_start_number = extract(4, 'V') if disk_start_number && disk_start_number == 0xFFFF @disk_start_number = extract(4, 'V') if disk_start_number && disk_start_number == 0xFFFF
@content = nil @content = nil
[@original_size || original_size, [@original_size || original_size,
@ -57,8 +51,7 @@ module Zip
private :extract private :extract
def pack_for_local def pack_for_local
# Local header entries must contain original size and compressed size; # local header entries must contain original size and compressed size; other fields do not apply
# other fields do not apply.
return '' unless @original_size && @compressed_size return '' unless @original_size && @compressed_size
[@original_size, @compressed_size].pack('Q<Q<') [@original_size, @compressed_size].pack('Q<Q<')
@ -66,7 +59,7 @@ module Zip
def pack_for_c_dir def pack_for_c_dir
# central directory entries contain only fields that didn't fit in the main entry part # 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 << [@original_size].pack('Q<') if @original_size
packed << [@compressed_size].pack('Q<') if @compressed_size packed << [@compressed_size].pack('Q<') if @compressed_size
packed << [@relative_header_offset].pack('Q<') if @relative_header_offset 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 module Zip
# Zip::File is modeled after java.util.zip.ZipFile from the Java SDK. # ZipFile is modeled after java.util.zip.ZipFile from the Java SDK.
# The most important methods are those for accessing information about # The most important methods are those inherited from
# the entries in # ZipCentralDirectory for accessing information about the entries in
# the archive and methods such as `get_input_stream` and # the archive and methods such as get_input_stream and
# `get_output_stream` for reading from and writing entries to the # get_output_stream for reading from and writing entries to the
# archive. The class includes a few convenience methods such as # archive. The class includes a few convenience methods such as
# `extract` for extracting entries to the filesystem, and `remove`, # #extract for extracting entries to the filesystem, and #remove,
# `replace`, `rename` and `mkdir` for making simple modifications to # #replace, #rename and #mkdir for making simple modifications to
# the archive. # the archive.
# #
# Modifications to a zip archive are not committed until `commit` or # Modifications to a zip archive are not committed until #commit or
# `close` is called. The method `open` accepts a block following # #close is called. The method #open accepts a block following
# the pattern from ::File.open offering a simple way to # the pattern from File.open offering a simple way to
# automatically close the archive when the block returns. # 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 # (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. # to it.
# #
# ``` # require 'zip'
# require 'zip'
# #
# Zip::File.open('my.zip', create: true) do |zipfile| # Zip::File.open("my.zip", Zip::File::CREATE) {
# zipfile.get_output_stream('first.txt') { |f| f.puts 'Hello from Zip::File' } # |zipfile|
# zipfile.mkdir('a_dir') # zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" }
# end # zipfile.mkdir("a_dir")
# ``` # }
# #
# The next example reopens `my.zip`, writes the contents of # The next example reopens <code>my.zip</code> writes the contents of
# `first.txt` to standard out and deletes the entry from # <code>first.txt</code> to standard out and deletes the entry from
# the archive. # the archive.
# #
# ``` # require 'zip'
# require 'zip'
# #
# Zip::File.open('my.zip', create: true) do |zipfile| # Zip::File.open("my.zip", Zip::File::CREATE) {
# puts zipfile.read('first.txt') # |zipfile|
# zipfile.remove('first.txt') # puts zipfile.read("first.txt")
# end # zipfile.remove("first.txt")
# }
# #
# Zip::FileSystem offers an alternative API that emulates ruby's # ZipFileSystem offers an alternative API that emulates ruby's
# interface for accessing the filesystem, ie. the ::File and ::Dir classes. # interface for accessing the filesystem, ie. the File and Dir classes.
class File
extend Forwardable
extend FileSplit
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 attr_reader :name
# default -> false. # default -> false.
attr_accessor :restore_ownership attr_accessor :restore_ownership
# default -> true. # default -> false, but will be set to true in a future version.
attr_accessor :restore_permissions attr_accessor :restore_permissions
# default -> true. # default -> false, but will be set to true in a future version.
attr_accessor :restore_times 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. # a new archive if it doesn't exist already.
def initialize(path_or_io, create: false, buffer: false, def initialize(path_or_io, dep_create = false, dep_buffer = false,
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership], create: false, buffer: false, **options)
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
compression_level: ::Zip.default_compression)
super() 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 @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 if buffer
@restore_permissions = restore_permissions read_from_stream(path_or_io)
@restore_times = restore_times else
@compression_level = compression_level ::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 end
class << self class << self
# Similar to ::new. If a block is passed the Zip::File object is passed # 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 # to the block and is automatically closed afterwards, just as with
# ruby's builtin File::open method. # ruby's builtin File::open method.
def open(file_name, create: false, def open(file_name, dep_create = false, create: false, **options)
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership], Zip.warn_about_v3_api('Zip::File.open') if dep_create
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)
zf = ::Zip::File.new(file_name, create: (dep_create || create), buffer: false, **options)
return zf unless block_given? return zf unless block_given?
begin begin
@ -113,29 +136,31 @@ module Zip
end end
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 # Like #open, but reads zip archive contents from a String or open IO
# stream, and outputs data to a buffer. # stream, and outputs data to a buffer.
# (This can be used to extract data from a # (This can be used to extract data from a
# downloaded zip archive without first saving it to disk.) # downloaded zip archive without first saving it to disk.)
def open_buffer(io = ::StringIO.new, create: false, def open_buffer(io, **options)
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
compression_level: ::Zip.default_compression)
unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String) 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' \ raise "Zip::File.open_buffer expects a String or IO-like argument (responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
"(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
end end
io = ::StringIO.new(io) if io.kind_of?(::String) io = ::StringIO.new(io) if io.kind_of?(::String)
zf = ::Zip::File.new(io, create: create, buffer: true, # https://github.com/rubyzip/rubyzip/issues/119
restore_ownership: restore_ownership, io.binmode if io.respond_to?(:binmode)
restore_permissions: restore_permissions,
restore_times: restore_times,
compression_level: compression_level)
zf = ::Zip::File.new(io, create: true, buffer: true, **options)
return zf unless block_given? return zf unless block_given?
yield zf yield zf
@ -159,19 +184,89 @@ module Zip
end end
end end
# Count the entries in a zip archive without reading the whole set of def get_segment_size_for_split(segment_size)
# entry data into memory. if MIN_SEGMENT_SIZE > segment_size
def count_entries(path_or_io) MIN_SEGMENT_SIZE
cdir = ::Zip::CentralDirectory.new elsif MAX_SEGMENT_SIZE < segment_size
MAX_SEGMENT_SIZE
if path_or_io.kind_of?(String)
::File.open(path_or_io, 'rb') do |f|
cdir.count_entries(f)
end
else else
cdir.count_entries(path_or_io) segment_size
end end
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 end
# Returns an input stream to the specified entry. If a block is passed # 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 # 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 # the stream is automatically closed afterwards just as with ruby's builtin
# File.open method. # 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, extra: nil, compressed_size: nil, crc: nil,
compression_method: nil, compression_level: nil, compression_method: nil, size: nil, time: nil,
size: nil, time: nil, &a_proc) &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 = new_entry =
if entry.kind_of?(Entry) if entry.kind_of?(Entry)
entry entry
else else
Entry.new( Entry.new(@name, entry.to_s,
@name, entry.to_s, comment: comment, extra: extra, comment: (comment || dep_comment),
compressed_size: compressed_size, crc: crc, size: size, extra: (extra || dep_extra),
compression_method: compression_method, compressed_size: (compressed_size || dep_compressed_size),
compression_level: compression_level, time: time crc: (crc || dep_crc),
) compression_method: (compression_method || dep_compression_method),
size: (size || dep_size),
time: (time || dep_time))
end end
if new_entry.directory? if new_entry.directory?
raise ArgumentError, raise ArgumentError,
"cannot open stream to directory entry - '#{new_entry}'" "cannot open stream to directory entry - '#{new_entry}'"
end end
new_entry.unix_perms = permissions new_entry.unix_perms = (permission_int || dep_permission_int)
zip_streamable_entry = StreamableStream.new(new_entry) zip_streamable_entry = StreamableStream.new(new_entry)
@cdir << zip_streamable_entry @entry_set << zip_streamable_entry
zip_streamable_entry.get_output_stream(&a_proc) zip_streamable_entry.get_output_stream(&a_proc)
end end
# rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
# Returns the name of the zip archive # Returns the name of the zip archive
def to_s def to_s
@ -226,39 +335,31 @@ module Zip
def add(entry, src_path, &continue_on_exists_proc) def add(entry, src_path, &continue_on_exists_proc)
continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc } continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
check_entry_exists(entry, continue_on_exists_proc, 'add') check_entry_exists(entry, continue_on_exists_proc, 'add')
new_entry = if entry.kind_of?(::Zip::Entry) new_entry = entry.kind_of?(::Zip::Entry) ? entry : ::Zip::Entry.new(@name, entry.to_s)
entry
else
::Zip::Entry.new(
@name, entry.to_s,
compression_level: @compression_level
)
end
new_entry.gather_fileinfo_from_srcpath(src_path) new_entry.gather_fileinfo_from_srcpath(src_path)
@cdir << new_entry new_entry.dirty = true
@entry_set << new_entry
end end
# Convenience method for adding the contents of a file to the archive # Convenience method for adding the contents of a file to the archive
# in Stored format (uncompressed) # in Stored format (uncompressed)
def add_stored(entry, src_path, &continue_on_exists_proc) def add_stored(entry, src_path, &continue_on_exists_proc)
entry = ::Zip::Entry.new( entry = ::Zip::Entry.new(@name, entry.to_s, nil, nil, nil, nil, ::Zip::Entry::STORED)
@name, entry.to_s, compression_method: ::Zip::Entry::STORED
)
add(entry, src_path, &continue_on_exists_proc) add(entry, src_path, &continue_on_exists_proc)
end end
# Removes the specified entry. # Removes the specified entry.
def remove(entry) def remove(entry)
@cdir.delete(get_entry(entry)) @entry_set.delete(get_entry(entry))
end end
# Renames the specified entry. # Renames the specified entry.
def rename(entry, new_name, &continue_on_exists_proc) def rename(entry, new_name, &continue_on_exists_proc)
found_entry = get_entry(entry) found_entry = get_entry(entry)
check_entry_exists(new_name, continue_on_exists_proc, 'rename') check_entry_exists(new_name, continue_on_exists_proc, 'rename')
@cdir.delete(found_entry) @entry_set.delete(found_entry)
found_entry.name = new_name found_entry.name = new_name
@cdir << found_entry @entry_set << found_entry
end end
# Replaces the specified entry with the contents of src_path (from # Replaces the specified entry with the contents of src_path (from
@ -269,16 +370,25 @@ module Zip
add(entry, src_path) add(entry, src_path)
end 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` # Extracts `entry` to a file at `entry_path`, with `destination_directory`
# as the base location in the filesystem. # as the base location in the filesystem.
# #
# NB: The caller is responsible for making sure `destination_directory` is # NB: The caller is responsible for making sure `destination_directory` is
# safe, if it is passed. # 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 } block ||= proc { ::Zip.on_exists_proc }
found_entry = get_entry(entry) found_entry = get_entry(entry)
entry_path ||= found_entry.name 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 end
# Commits changes that has been made since the previous commit to # Commits changes that has been made since the previous commit to
@ -288,15 +398,16 @@ module Zip
on_success_replace do |tmp_file| on_success_replace do |tmp_file|
::Zip::OutputStream.open(tmp_file) do |zos| ::Zip::OutputStream.open(tmp_file) do |zos|
@cdir.each do |e| @entry_set.each do |e|
e.write_to_zip_output_stream(zos) e.write_to_zip_output_stream(zos)
e.dirty = false
e.clean_up e.clean_up
end end
zos.comment = comment zos.comment = comment
end end
true true
end end
initialize_cdir(@name) initialize(name)
end end
# Write buffer write changes to buffer and return # Write buffer write changes to buffer and return
@ -304,7 +415,7 @@ module Zip
return io unless commit_required? return io unless commit_required?
::Zip::OutputStream.write_buffer(io) do |zos| ::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 zos.comment = comment
end end
end end
@ -317,19 +428,16 @@ module Zip
# Returns true if any changes has been made to this archive since # Returns true if any changes has been made to this archive since
# the previous commit # the previous commit
def commit_required? def commit_required?
return true if @create || @cdir.dirty? @entry_set.each do |e|
return true if e.dirty
@cdir.each do |e|
return true if e.dirty?
end end
@comment != @stored_comment || @entry_set != @stored_entries || @create
false
end end
# Searches for entry with the specified name. Returns nil if # Searches for entry with the specified name. Returns nil if
# no entry is found. See also get_entry # no entry is found. See also get_entry
def find_entry(entry_name) 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? return if selected_entry.nil?
selected_entry.restore_ownership = @restore_ownership selected_entry.restore_ownership = @restore_ownership
@ -338,6 +446,11 @@ module Zip
selected_entry selected_entry
end 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 # Searches for an entry just as find_entry, but throws Errno::ENOENT
# if no entry is found. # if no entry is found.
def get_entry(entry) def get_entry(entry)
@ -353,50 +466,33 @@ module Zip
entry_name = entry_name.dup.to_s entry_name = entry_name.dup.to_s
entry_name << '/' unless entry_name.end_with?('/') 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 end
private private
def initialize_cdir(path_or_io, buffer: false) def directory?(new_entry, src_path)
@cdir = ::Zip::CentralDirectory.new path_is_directory = ::File.directory?(src_path)
if new_entry.directory? && !path_is_directory
if ::File.size?(@name.to_s) raise ArgumentError,
# There is a file, which exists, that is associated with this zip. "entry name '#{new_entry}' indicates directory entry, but " \
@create = false "'#{src_path}' is not a directory"
@file_permissions = ::File.stat(@name).mode elsif !new_entry.directory? && path_is_directory
new_entry.name += '/'
if buffer
# https://github.com/rubyzip/rubyzip/issues/119
path_or_io.binmode if path_or_io.respond_to?(:binmode)
@cdir.read_from_stream(path_or_io)
else
::File.open(@name, 'rb') do |f|
@cdir.read_from_stream(f)
end
end
elsif buffer && path_or_io.size > 0
# This zip is probably a non-empty StringIO.
@create = false
@cdir.read_from_stream(path_or_io)
elsif !@create && ::File.empty?(@name)
# A file exists, but it is empty, and we've said we're
# NOT creating a new zip.
raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
elsif !@create
# If we get here, and we're not creating a new zip, then
# everything is wrong.
raise Error, "File #{@name} not found"
end end
new_entry.directory? && path_is_directory
end end
def check_entry_exists(entry_name, continue_on_exists_proc, proc_name) 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 } 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)
remove get_entry(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 end
def check_file(path) def check_file(path)
@ -406,12 +502,14 @@ module Zip
def on_success_replace def on_success_replace
dirname, basename = ::File.split(name) dirname, basename = ::File.split(name)
::Dir::Tmpname.create(basename, dirname) do |tmp_filename| ::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
if yield tmp_filename begin
::File.rename(tmp_filename, name) if yield tmp_filename
::File.chmod(@file_permissions, name) unless @create ::File.rename(tmp_filename, name)
::File.chmod(@file_permissions, name) unless @create
end
ensure
::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
end end
ensure
::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
end end
end end
end end

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 'zip'
require_relative 'filesystem/zip_file_name_mapper'
require_relative 'filesystem/directory_iterator'
require_relative 'filesystem/dir'
require_relative 'filesystem/file'
module Zip module Zip
# The ZipFileSystem API provides an API for accessing entries in # 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> # <code>first.txt</code>, a directory entry named <code>mydir</code>
# and finally another normal entry named <code>second.txt</code> # and finally another normal entry named <code>second.txt</code>
# #
# ``` # require 'zip/filesystem'
# require 'zip/filesystem'
# #
# Zip::File.open('my.zip', create: true) do |zipfile| # Zip::File.open("my.zip", Zip::File::CREATE) {
# zipfile.file.open('first.txt', 'w') { |f| f.puts 'Hello world' } # |zipfile|
# zipfile.dir.mkdir('mydir') # zipfile.file.open("first.txt", "w") { |f| f.puts "Hello world" }
# zipfile.file.open('mydir/second.txt', 'w') { |f| f.puts 'Hello again' } # zipfile.dir.mkdir("mydir")
# end # zipfile.file.open("mydir/second.txt", "w") { |f| f.puts "Hello again" }
# ``` # }
# #
# Reading is as easy as writing, as the following example shows. The # Reading is as easy as writing, as the following example shows. The
# example writes the contents of <code>first.txt</code> from zip archive # example writes the contents of <code>first.txt</code> from zip archive
# <code>my.zip</code> to standard out. # <code>my.zip</code> to standard out.
# #
# ``` # require 'zip/filesystem'
# require 'zip/filesystem'
# #
# Zip::File.open('my.zip') do |zipfile| # Zip::File.open("my.zip") {
# puts zipfile.file.read('first.txt') # |zipfile|
# end # puts zipfile.file.read("first.txt")
# ``` # }
module FileSystem module FileSystem
def initialize # :nodoc: def initialize # :nodoc:
mapped_zip = ZipFileNameMapper.new(self) mapped_zip = ZipFileNameMapper.new(self)
@zip_fs_dir = Dir.new(mapped_zip) @zip_fs_dir = ZipFsDir.new(mapped_zip)
@zip_fs_file = File.new(mapped_zip) @zip_fs_file = ZipFsFile.new(mapped_zip)
@zip_fs_dir.file = @zip_fs_file @zip_fs_dir.file = @zip_fs_file
@zip_fs_file.dir = @zip_fs_dir @zip_fs_file.dir = @zip_fs_dir
end end
# Returns a Zip::FileSystem::Dir which is much like ruby's builtin Dir # Returns a ZipFsDir which is much like ruby's builtin Dir (class)
# (class) object, except it works on the Zip::File on which this method is # object, except it works on the Zip::File on which this method is
# invoked # invoked
def dir def dir
@zip_fs_dir @zip_fs_dir
end end
# Returns a Zip::FileSystem::File which is much like ruby's builtin File # Returns a ZipFsFile which is much like ruby's builtin File (class)
# (class) object, except it works on the Zip::File on which this method is # object, except it works on the Zip::File on which this method is
# invoked # invoked
def file def file
@zip_fs_file @zip_fs_file
end end
# Instances of this class are normally accessed via the accessor
# Zip::File::file. An instance of ZipFsFile behaves like ruby's
# builtin File (class) object, except it works on Zip::File entries.
#
# The individual methods are not documented due to their
# similarity with the methods in File
class ZipFsFile
attr_writer :dir
# protected :dir
class ZipFsStat
class << self
def delegate_to_fs_file(*methods)
methods.each do |method|
class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
def #{method} # def file?
@zip_fs_file.#{method}(@entry_name) # @zip_fs_file.file?(@entry_name)
end # end
END_EVAL
end
end
end
def initialize(zip_fs_file, entry_name)
@zip_fs_file = zip_fs_file
@entry_name = entry_name
end
def kind_of?(type)
super || type == ::File::Stat
end
delegate_to_fs_file :file?, :directory?, :pipe?, :chardev?, :symlink?,
:socket?, :blockdev?, :readable?, :readable_real?, :writable?, :ctime,
:writable_real?, :executable?, :executable_real?, :sticky?, :owned?,
:grpowned?, :setuid?, :setgid?, :zero?, :size, :size?, :mtime, :atime
def blocks
nil
end
def get_entry
@zip_fs_file.__send__(:get_entry, @entry_name)
end
private :get_entry
def gid
e = get_entry
if e.extra.member? 'IUnix'
e.extra['IUnix'].gid || 0
else
0
end
end
def uid
e = get_entry
if e.extra.member? 'IUnix'
e.extra['IUnix'].uid || 0
else
0
end
end
def ino
0
end
def dev
0
end
def rdev
0
end
def rdev_major
0
end
def rdev_minor
0
end
def ftype
if file?
'file'
elsif directory?
'directory'
else
raise StandardError, 'Unknown file type'
end
end
def nlink
1
end
def blksize
nil
end
def mode
e = get_entry
if e.fstype == 3
e.external_file_attributes >> 16
else
33_206 # 33206 is equivalent to -rw-rw-rw-
end
end
end
def initialize(mapped_zip)
@mapped_zip = mapped_zip
end
def get_entry(filename)
unless exists?(filename)
raise Errno::ENOENT, "No such file or directory - #{filename}"
end
@mapped_zip.find_entry(filename)
end
private :get_entry
def unix_mode_cmp(filename, mode)
e = get_entry(filename)
e.fstype == 3 && ((e.external_file_attributes >> 16) & mode) != 0
rescue Errno::ENOENT
false
end
private :unix_mode_cmp
def exists?(filename)
expand_path(filename) == '/' || !@mapped_zip.find_entry(filename).nil?
end
alias exist? exists?
# Permissions not implemented, so if the file exists it is accessible
alias owned? exists?
alias grpowned? exists?
def readable?(filename)
unix_mode_cmp(filename, 0o444)
end
alias readable_real? readable?
def writable?(filename)
unix_mode_cmp(filename, 0o222)
end
alias writable_real? writable?
def executable?(filename)
unix_mode_cmp(filename, 0o111)
end
alias executable_real? executable?
def setuid?(filename)
unix_mode_cmp(filename, 0o4000)
end
def setgid?(filename)
unix_mode_cmp(filename, 0o2000)
end
def sticky?(filename)
unix_mode_cmp(filename, 0o1000)
end
def umask(*args)
::File.umask(*args)
end
def truncate(_filename, _len)
raise StandardError, 'truncate not supported'
end
def directory?(filename)
entry = @mapped_zip.find_entry(filename)
expand_path(filename) == '/' || (!entry.nil? && entry.directory?)
end
def open(filename, mode = 'r', permissions = 0o644, &block)
mode = 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 end
class File # :nodoc: class File
include FileSystem include FileSystem
end end
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 module Zip
class Inflater < Decompressor # :nodoc:all class Inflater < Decompressor #:nodoc:all
def initialize(*args) def initialize(*args)
super super
@buffer = +'' @buffer = ''.b
@zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS) @zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS)
end end
def read(length = nil, outbuf = +'') def read(length = nil, outbuf = ''.b)
return (length.nil? || length.zero? ? '' : nil) if eof return (length.nil? || length.zero? ? '' : nil) if eof
while length.nil? || (@buffer.bytesize < length) while length.nil? || (@buffer.bytesize < length)
@ -39,8 +37,8 @@ module Zip
retried += 1 retried += 1
retry retry
end end
rescue Zlib::Error => e rescue Zlib::Error
raise ::Zip::DecompressionError, e raise(::Zip::DecompressionError, 'zlib error while inflating')
end end
def input_finished? def input_finished?

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +1,21 @@
# frozen_string_literal: true lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require_relative 'lib/zip/version' require 'zip/version'
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = 'rubyzip' s.name = 'rubyzip'
s.version = Zip::VERSION s.version = ::Zip::VERSION
s.authors = ['Robert Haines', 'John Lees-Miller', 'Alexander Simonov'] s.authors = ['Robert Haines', 'John Lees-Miller', 'Alexander Simonov']
s.email = [ s.email = [
'hainesr@gmail.com', 'jdleesmiller@gmail.com', 'alex@simonov.me' 'hainesr@gmail.com', 'jdleesmiller@gmail.com', 'alex@simonov.me'
] ]
s.homepage = 'http://github.com/rubyzip/rubyzip' s.homepage = 'http://github.com/rubyzip/rubyzip'
s.platform = Gem::Platform::RUBY s.platform = Gem::Platform::RUBY
s.summary = 'rubyzip is a ruby module for reading and writing zip files' s.summary = 'rubyzip is a ruby module for reading and writing zip files'
s.files = Dir.glob('{samples,lib}/**/*.rb') + s.files = Dir.glob('{samples,lib}/**/*.rb') + %w[README.md TODO Rakefile]
%w[LICENSE.md README.md Changelog.md Rakefile rubyzip.gemspec] s.require_paths = ['lib']
s.require_paths = ['lib'] s.license = 'BSD 2-Clause'
s.license = 'BSD-2-Clause' s.metadata = {
s.metadata = {
'bug_tracker_uri' => 'https://github.com/rubyzip/rubyzip/issues', 'bug_tracker_uri' => 'https://github.com/rubyzip/rubyzip/issues',
'changelog_uri' => "https://github.com/rubyzip/rubyzip/blob/v#{s.version}/Changelog.md", 'changelog_uri' => "https://github.com/rubyzip/rubyzip/blob/v#{s.version}/Changelog.md",
'documentation_uri' => "https://www.rubydoc.info/gems/rubyzip/#{s.version}", 'documentation_uri' => "https://www.rubydoc.info/gems/rubyzip/#{s.version}",
@ -25,15 +23,35 @@ Gem::Specification.new do |s|
'wiki_uri' => 'https://github.com/rubyzip/rubyzip/wiki', 'wiki_uri' => 'https://github.com/rubyzip/rubyzip/wiki',
'rubygems_mfa_required' => 'true' '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' The public API of some Rubyzip classes has been modernized to use named
s.add_development_dependency 'rake', '~> 13.2' parameters for optional arguments. Please check your usage of the
s.add_development_dependency 'rdoc', '~> 6.11' following classes:
s.add_development_dependency 'rubocop', '~> 1.61.0' * `Zip::File`
s.add_development_dependency 'rubocop-performance', '~> 1.20.0' * `Zip::Entry`
s.add_development_dependency 'rubocop-rake', '~> 0.6.0' * `Zip::InputStream`
s.add_development_dependency 'simplecov', '~> 0.22.0' * `Zip::OutputStream`
s.add_development_dependency 'simplecov-lcov', '~> 0.8' * `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 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 #!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH << '../lib' $LOAD_PATH << '../lib'
system('zip example.zip example.rb gtk_ruby_zip.rb') system('zip example.zip example.rb gtk_ruby_zip.rb')
@ -21,8 +20,7 @@ end
zf = Zip::File.new('example.zip') zf = Zip::File.new('example.zip')
zf.each_with_index do |entry, index| zf.each_with_index do |entry, index|
puts "entry #{index} is #{entry.name}, size = #{entry.size}, " \ puts "entry #{index} is #{entry.name}, size = #{entry.size}, compressed size = #{entry.compressed_size}"
"compressed size = #{entry.compressed_size}"
# use zf.get_input_stream(entry) to get a ZipInputStream for the entry # 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 # entry can be the ZipEntry object or any object which has a to_s method that
# returns the name of the entry. # 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" puts "Zip file splitted in #{part_zips_count} parts"
# Track splitting an archive # Track splitting an archive
Zip::File.split( Zip::File.split('large_zip_file.zip', 1_048_576, true, 'part_zip_file') do |part_count, part_index, chunk_bytes, segment_bytes|
'large_zip_file.zip', 1_048_576, true, 'part_zip_file' puts "#{part_index} of #{part_count} part splitting: #{(chunk_bytes.to_f / segment_bytes * 100).to_i}%"
) 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 end
# For other examples, look at zip.rb and ziptest.rb # For other examples, look at zip.rb and ziptest.rb

View File

@ -1,5 +1,4 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH << '../lib' $LOAD_PATH << '../lib'
@ -7,9 +6,9 @@ require 'zip/filesystem'
EXAMPLE_ZIP = 'filesystem.zip' 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.file.open('file1.txt', 'w') { |os| os.write 'first file1.txt' }
zf.dir.mkdir('dir1') zf.dir.mkdir('dir1')
zf.dir.chdir('dir1') zf.dir.chdir('dir1')

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'zip' require 'zip'
# This is a simple example which uses rubyzip to # This is a simple example which uses rubyzip to
@ -23,7 +21,7 @@ class ZipFileGenerator
def write def write
entries = Dir.entries(@input_dir) - %w[. ..] 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 write_entries entries, '', zipfile
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,12 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ConstantsTest < MiniTest::Test 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 def test_compression_methods
assert_equal(0, Zip::COMPRESSION_METHOD_STORE) assert_equal(0, Zip::COMPRESSION_METHOD_STORE)
assert_equal(1, Zip::COMPRESSION_METHOD_SHRINK) assert_equal(1, Zip::COMPRESSION_METHOD_SHRINK)

View File

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

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class TraditionalEncrypterTest < MiniTest::Test 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 #!/usr/bin/env ruby
# frozen_string_literal: true
class NotZippedRuby class NotZippedRuby
def return_true def return_true

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class DeflaterTest < MiniTest::Test class DeflaterTest < MiniTest::Test
@ -10,10 +8,6 @@ class DeflaterTest < MiniTest::Test
DEFAULT_COMP_FILE = 'test/data/generated/compressiontest_default_compression.bin' DEFAULT_COMP_FILE = 'test/data/generated/compressiontest_default_compression.bin'
NO_COMP_FILE = 'test/data/generated/compressiontest_no_compression.bin' NO_COMP_FILE = 'test/data/generated/compressiontest_no_compression.bin'
def teardown
Zip.reset!
end
def test_output_operator def test_output_operator
txt = load_file('test/data/file2.txt') txt = load_file('test/data/file2.txt')
deflate(txt, DEFLATER_TEST_FILE) deflate(txt, DEFLATER_TEST_FILE)
@ -49,7 +43,7 @@ class DeflaterTest < MiniTest::Test
private private
def load_file(filename) def load_file(filename)
File.binread(filename) File.open(filename, 'rb', &:read)
end end
def deflate(data, filename) 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' require 'test_helper'
class EncryptionTest < MiniTest::Test class EncryptionTest < MiniTest::Test
ENCRYPT_ZIP_TEST_FILE = 'test/data/zipWithEncryption.zip' ENCRYPT_ZIP_TEST_FILE = 'test/data/zipWithEncryption.zip'
INPUT_FILE1 = 'test/data/file1.txt' INPUT_FILE1 = 'test/data/file1.txt'
INPUT_FILE2 = 'test/data/file2.txt'
def setup def setup
@default_compression = Zip.default_compression
Zip.default_compression = ::Zlib::DEFAULT_COMPRESSION Zip.default_compression = ::Zlib::DEFAULT_COMPRESSION
end end
def teardown def teardown
Zip.reset! Zip.default_compression = @default_compression
end end
def test_encrypt def test_encrypt
content = File.read(INPUT_FILE1) test_file = ::File.open(ENCRYPT_ZIP_TEST_FILE, 'rb').read
test_filename = 'top_secret_file.txt'
password = 'swordfish' @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
encrypted_zip = Zip::OutputStream.write_buffer( Random.stub(:rand, ->(_range) { @rand.shift }) do
::StringIO.new, Zip::OutputStream.write_buffer(::StringIO.new, Zip::TraditionalEncrypter.new('password')) do |zos|
encrypter: Zip::TraditionalEncrypter.new(password) zos.put_next_entry('file1.txt')
) do |out| zos.write ::File.open(INPUT_FILE1).read
out.put_next_entry(test_filename) end.string
out.write content
end
Zip::InputStream.open(
encrypted_zip, decrypter: Zip::TraditionalDecrypter.new(password)
) do |zis|
entry = zis.get_next_entry
assert_equal test_filename, entry.name
assert_equal 1_327, entry.size
assert_equal content, zis.read
end
error = assert_raises(Zip::DecompressionError) do
Zip::InputStream.open(
encrypted_zip,
decrypter: Zip::TraditionalDecrypter.new("#{password}wrong")
) do |zis|
zis.get_next_entry
assert_equal content, zis.read
end end
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 end
def test_decrypt def test_decrypt
Zip::InputStream.open( Zip::InputStream.open(ENCRYPT_ZIP_TEST_FILE, 0, Zip::TraditionalDecrypter.new('password')) do |zis|
ENCRYPT_ZIP_TEST_FILE,
decrypter: Zip::TraditionalDecrypter.new('password')
) do |zis|
entry = zis.get_next_entry entry = zis.get_next_entry
assert_equal 'file1.txt', entry.name assert_equal 'file1.txt', entry.name
assert_equal 1_327, entry.size assert_equal 1327, entry.size
assert_equal ::File.read(INPUT_FILE1), zis.read assert_equal ::File.open(INPUT_FILE1, 'r').read, zis.read
entry = zis.get_next_entry
assert_equal 'file2.txt', entry.name
assert_equal 41_234, entry.size
assert_equal ::File.read(INPUT_FILE2), zis.read
end end
end end
end end

View File

@ -1,16 +1,14 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ZipEntrySetTest < MiniTest::Test class ZipEntrySetTest < MiniTest::Test
ZIP_ENTRIES = [ ZIP_ENTRIES = [
::Zip::Entry.new('zipfile.zip', 'name1', comment: 'comment1'), ::Zip::Entry.new('zipfile.zip', 'name1', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name3', comment: 'comment1'), ::Zip::Entry.new('zipfile.zip', 'name3', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name2', comment: 'comment1'), ::Zip::Entry.new('zipfile.zip', 'name2', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name4', comment: 'comment1'), ::Zip::Entry.new('zipfile.zip', 'name4', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name5', comment: 'comment1'), ::Zip::Entry.new('zipfile.zip', 'name5', 'comment1'),
::Zip::Entry.new('zipfile.zip', 'name6', comment: 'comment1') ::Zip::Entry.new('zipfile.zip', 'name6', 'comment1')
].freeze ]
def setup def setup
@zip_entry_set = ::Zip::EntrySet.new(ZIP_ENTRIES) @zip_entry_set = ::Zip::EntrySet.new(ZIP_ENTRIES)
@ -22,17 +20,13 @@ class ZipEntrySetTest < MiniTest::Test
def test_include def test_include
assert(@zip_entry_set.include?(ZIP_ENTRIES.first)) assert(@zip_entry_set.include?(ZIP_ENTRIES.first))
assert( assert(!@zip_entry_set.include?(::Zip::Entry.new('different.zip', 'different', 'aComment')))
!@zip_entry_set.include?(
::Zip::Entry.new('different.zip', 'different', comment: 'aComment')
)
)
end end
def test_size def test_size
assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size) assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size)
assert_equal(ZIP_ENTRIES.size, @zip_entry_set.length) 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) assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.length)
end end
@ -60,19 +54,11 @@ class ZipEntrySetTest < MiniTest::Test
def test_each def test_each
# Used each instead each_with_index due the bug in jRuby # Used each instead each_with_index due the bug in jRuby
count = 0 count = 0
new_size = 200
@zip_entry_set.each do |entry| @zip_entry_set.each do |entry|
assert(ZIP_ENTRIES.include?(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 count += 1
end end
assert_equal(ZIP_ENTRIES.size, count) 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 end
def test_entries def test_entries
@ -80,9 +66,7 @@ class ZipEntrySetTest < MiniTest::Test
end end
def test_find_entry def test_find_entry
entries = [ entries = [::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', 'comment1')]
::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', comment: 'comment1')
]
::Zip.case_insensitive_match = true ::Zip.case_insensitive_match = true
zip_entry_set = ::Zip::EntrySet.new(entries) zip_entry_set = ::Zip::EntrySet.new(entries)
@ -109,20 +93,10 @@ class ZipEntrySetTest < MiniTest::Test
arr << entry arr << entry
end end
assert_equal(ZIP_ENTRIES.sort, arr) 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 end
def test_compound def test_compound
new_entry = ::Zip::Entry.new( new_entry = ::Zip::Entry.new('zf.zip', 'new entry', "new entry's comment")
'zf.zip', 'new entry', comment: "new entry's comment"
)
assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size) assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size)
@zip_entry_set << new_entry @zip_entry_set << new_entry
assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.size) assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.size)

View File

@ -1,23 +1,18 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ZipEntryTest < MiniTest::Test class ZipEntryTest < MiniTest::Test
include ZipEntryData include ZipEntryData
def teardown
::Zip.reset!
end
def test_constructor_and_getters def test_constructor_and_getters
entry = ::Zip::Entry.new( entry = ::Zip::Entry.new(TEST_ZIPFILE,
TEST_ZIPFILE, TEST_NAME, TEST_NAME,
comment: TEST_COMMENT, extra: TEST_EXTRA, TEST_COMMENT,
compressed_size: TEST_COMPRESSED_SIZE, TEST_EXTRA,
crc: TEST_CRC, size: TEST_SIZE, time: TEST_TIME, TEST_COMPRESSED_SIZE,
compression_method: TEST_COMPRESSIONMETHOD, TEST_CRC,
compression_level: TEST_COMPRESSIONLEVEL TEST_COMPRESSIONMETHOD,
) TEST_SIZE,
TEST_TIME)
assert_equal(TEST_COMMENT, entry.comment) assert_equal(TEST_COMMENT, entry.comment)
assert_equal(TEST_COMPRESSED_SIZE, entry.compressed_size) 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_COMPRESSIONMETHOD, entry.compression_method)
assert_equal(TEST_NAME, entry.name) assert_equal(TEST_NAME, entry.name)
assert_equal(TEST_SIZE, entry.size) assert_equal(TEST_SIZE, entry.size)
assert_equal(TEST_TIME, entry.time)
# Reverse times when testing because we need to use DOSTime#== for the
# comparison, not Time#==.
assert_equal(entry.time, TEST_TIME)
end end
def test_is_directory_and_is_file def test_is_directory_and_is_file
@ -47,54 +39,30 @@ class ZipEntryTest < MiniTest::Test
end end
def test_equality def test_equality
entry1 = ::Zip::Entry.new( entry1 = ::Zip::Entry.new('file.zip', 'name', 'isNotCompared',
'file.zip', 'name', 'something extra', 123, 1234,
comment: 'isNotCompared', extra: 'something extra', ::Zip::Entry::DEFLATED, 10_000)
compressed_size: 123, crc: 1234, size: 10_000 entry2 = ::Zip::Entry.new('file.zip', 'name', 'isNotComparedXXX',
) 'something extra', 123, 1234,
::Zip::Entry::DEFLATED, 10_000)
entry2 = ::Zip::Entry.new( entry3 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
'file.zip', 'name', 'something extra', 123, 1234,
comment: 'isNotComparedXXX', extra: 'something extra', ::Zip::Entry::DEFLATED, 10_000)
compressed_size: 123, crc: 1234, size: 10_000 entry4 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
) 'something extraXX', 123, 1234,
::Zip::Entry::DEFLATED, 10_000)
entry3 = ::Zip::Entry.new( entry5 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
'file.zip', 'name2', 'something extraXX', 12, 1234,
comment: 'isNotComparedXXX', extra: 'something extra', ::Zip::Entry::DEFLATED, 10_000)
compressed_size: 123, crc: 1234, size: 10_000 entry6 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
) 'something extraXX', 12, 123,
::Zip::Entry::DEFLATED, 10_000)
entry4 = ::Zip::Entry.new( entry7 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
'file.zip', 'name2', 'something extraXX', 12, 123,
comment: 'isNotComparedXXX', extra: 'something extraXX', ::Zip::Entry::STORED, 10_000)
compressed_size: 123, crc: 1234, size: 10_000 entry8 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
) 'something extraXX', 12, 123,
::Zip::Entry::STORED, 100_000)
entry5 = ::Zip::Entry.new(
'file.zip', 'name2',
comment: 'isNotComparedXXX', extra: 'something extraXX',
compressed_size: 12, crc: 1234, size: 10_000
)
entry6 = ::Zip::Entry.new(
'file.zip', 'name2',
comment: 'isNotComparedXXX', extra: 'something extraXX',
compressed_size: 12, crc: 123, size: 10_000
)
entry7 = ::Zip::Entry.new(
'file.zip', 'name2', comment: 'isNotComparedXXX',
extra: 'something extraXX', compressed_size: 12, crc: 123, size: 10_000,
compression_method: ::Zip::Entry::STORED
)
entry8 = ::Zip::Entry.new(
'file.zip', 'name2',
comment: 'isNotComparedXXX', extra: 'something extraXX',
compressed_size: 12, crc: 123, size: 100_000,
compression_method: ::Zip::Entry::STORED
)
assert_equal(entry1, entry1) assert_equal(entry1, entry1)
assert_equal(entry1, entry2) assert_equal(entry1, entry2)
@ -150,52 +118,38 @@ class ZipEntryTest < MiniTest::Test
end end
def test_entry_name_cannot_start_with_slash def test_entry_name_cannot_start_with_slash
error = assert_raises(::Zip::EntryNameError) do assert_raises(::Zip::EntryNameError) { ::Zip::Entry.new('zf.zip', '/hej/der') }
::Zip::Entry.new('zf.zip', '/hej/der')
end
assert_match(/'\/hej\/der'/, error.message)
end
def test_entry_name_cannot_be_too_long
name = 'a' * 65_535
::Zip::Entry.new('', name) # Should not raise anything.
error = assert_raises(::Zip::EntryNameError) do
::Zip::Entry.new('', "a#{name}")
end
assert_match(/65,536/, error.message)
end end
def test_store_file_without_compression def test_store_file_without_compression
Dir.mktmpdir do |tmp| File.delete('/tmp/no_compress.zip') if File.exist?('/tmp/no_compress.zip')
tmp_zip = File.join(tmp, 'no_compress.zip') files = Dir[File.join('test/data/globTest', '**', '**')]
Zip.setup do |z| Zip.setup do |z|
z.write_zip64_support = false z.write_zip64_support = false
end
zipfile = Zip::File.open(tmp_zip, create: true)
mimetype_entry = Zip::Entry.new(
zipfile, # @zipfile
'mimetype', # @name
compression_method: Zip::Entry::STORED
)
zipfile.add(mimetype_entry, 'test/data/mimetype')
files = Dir[File.join('test/data/globTest', '**', '**')]
files.each do |file|
zipfile.add(file.sub('test/data/globTest/', ''), file)
end
zipfile.close
f = File.open(tmp_zip, 'rb')
first_100_bytes = f.read(100)
f.close
assert_match(/mimetypeapplication\/epub\+zip/, first_100_bytes)
end end
zipfile = Zip::File.open('/tmp/no_compress.zip', Zip::File::CREATE)
mimetype_entry = Zip::Entry.new(zipfile, # @zipfile
'mimetype', # @name
'', # @comment
'', # @extra
0, # @compressed_size
0, # @crc
Zip::Entry::STORED) # @comppressed_method
zipfile.add(mimetype_entry, 'test/data/mimetype')
files.each do |file|
zipfile.add(file.sub('test/data/globTest/', ''), file)
end
zipfile.close
f = File.open('/tmp/no_compress.zip', 'rb')
first_100_bytes = f.read(100)
f.close
assert_match(/mimetypeapplication\/epub\+zip/, first_100_bytes)
end end
def test_encrypted? def test_encrypted?
@ -215,137 +169,4 @@ class ZipEntryTest < MiniTest::Test
entry.gp_flags = 0 entry.gp_flags = 0
assert_equal(false, entry.incomplete?) assert_equal(false, entry.incomplete?)
end 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 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' require 'test_helper'
class ZipExtraFieldTest < MiniTest::Test class ZipExtraFieldTest < MiniTest::Test
def test_new def test_new
extra_pure = ::Zip::ExtraField.new('') extra_pure = ::Zip::ExtraField.new('')
extra_withstr = ::Zip::ExtraField.new('foo') 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_pure)
assert_instance_of(::Zip::ExtraField, extra_withstr) 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 end
def test_unknownfield def test_unknownfield
extra = ::Zip::ExtraField.new('foo') extra = ::Zip::ExtraField.new('foo')
assert_equal('foo', extra['Unknown'].to_c_dir_bin) assert_equal(extra['Unknown'], 'foo')
extra.merge('a') extra.merge('a')
assert_equal('fooa', extra['Unknown'].to_c_dir_bin) assert_equal(extra['Unknown'], 'fooa')
extra.merge('barbaz') extra.merge('barbaz')
assert_equal('fooabarbaz', extra['Unknown'].to_c_dir_bin) assert_equal(extra.to_s, 'fooabarbaz')
extra.merge('bar', local: true)
assert_equal('bar', extra['Unknown'].to_local_bin)
assert_equal('fooabarbaz', extra['Unknown'].to_c_dir_bin)
end
def test_bad_header_id
str = "ut\x5\0\x3\250$\r@"
ut = nil
assert_output('', /WARNING/) do
ut = ::Zip::ExtraField::UniversalTime.new(str)
end
assert_instance_of(::Zip::ExtraField::UniversalTime, ut)
assert_nil(ut.mtime)
end end
def test_ntfs 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) extra = ::Zip::ExtraField.new(str)
assert(extra.member?('NTFS')) assert(extra.member?('NTFS'))
t = ::Zip::DOSTime.at(1_410_496_497.405178) t = ::Zip::DOSTime.at(1_410_496_497.405178)
assert_equal(t, extra['NTFS'].mtime) assert_equal(t, extra['NTFS'].mtime)
assert_equal(t, extra['NTFS'].atime) assert_equal(t, extra['NTFS'].atime)
assert_equal(t, extra['NTFS'].ctime) assert_equal(t, extra['NTFS'].ctime)
assert_equal(str.force_encoding('BINARY'), extra.to_local_bin)
end end
def test_merge def test_merge
@ -78,9 +52,9 @@ class ZipExtraFieldTest < MiniTest::Test
extra = ::Zip::ExtraField.new(str) extra = ::Zip::ExtraField.new(str)
assert_instance_of(String, extra.to_s) assert_instance_of(String, extra.to_s)
extra_len = extra.to_s.length s = extra.to_s
extra.merge('foo', local: true) extra.merge('foo')
assert_equal(extra_len + 3, extra.to_s.length) assert_equal(s.length + 3, extra.to_s.length)
end end
def test_equality def test_equality
@ -99,26 +73,4 @@ class ZipExtraFieldTest < MiniTest::Test
extra1.create('IUnix') extra1.create('IUnix')
assert_equal(extra1, extra3) assert_equal(extra1, extra3)
end 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 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' require 'test_helper'
class ZipExtraFieldUTTest < MiniTest::Test 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\x05PS>APS>A", 0b101, true, false, false],
["UT\x09\x00\x06PS>APS>A", 0b110, false, false, true], ["UT\x09\x00\x06PS>APS>A", 0b110, false, false, true],
["UT\x13\x00\x07PS>APS>APS>A", 0b111, false, false, false] ["UT\x13\x00\x07PS>APS>APS>A", 0b111, false, false, false]
].freeze ]
def test_parse def test_parse
PARSE_TESTS.each do |bin, flags, a, c, m| PARSE_TESTS.each do |bin, flags, a, c, m|

View File

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

View File

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

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