Compare commits
38 Commits
Author | SHA1 | Date |
---|---|---|
|
6c4b7a9f90 | |
|
3b4c2bfa22 | |
|
e3eb62491b | |
|
c09352b546 | |
|
71bb069049 | |
|
bb06f99b14 | |
|
3d95a8204f | |
|
56954b0b59 | |
|
6ff40f7a78 | |
|
e05dc9b978 | |
|
a4c3f5bddb | |
|
57cff3338f | |
|
0001864cfe | |
|
385ebd054a | |
|
46689d7350 | |
|
ef89a62b70 | |
|
900db76760 | |
|
14efdd1cc4 | |
|
02faddaf44 | |
|
9aa5964262 | |
|
13f4ea766f | |
|
3910870f3a | |
|
07d833ca78 | |
|
6bbda380fe | |
|
a6e6c3c469 | |
|
b691a4b72c | |
|
f68877920a | |
|
13781b20d3 | |
|
4b1cfba9db | |
|
5e9ee53c70 | |
|
cb0505d735 | |
|
49950d924c | |
|
a17da19e0c | |
|
7c1a8fbf8f | |
|
77611045f1 | |
|
2f1c1ea400 | |
|
16de339666 | |
|
84d7a66abc |
|
@ -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
|
|
@ -8,15 +8,12 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu]
|
||||
ruby: ['3.0', '3.1', '3.2', '3.3', '3.4', head, jruby, jruby-head, truffleruby, truffleruby-head]
|
||||
ruby: ['2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', head, jruby, truffleruby]
|
||||
include:
|
||||
- { os: macos , ruby: '3.0' }
|
||||
- { os: windows, ruby: '3.0' }
|
||||
# head builds
|
||||
- { os: windows, ruby: ucrt }
|
||||
- { os: windows, ruby: mswin }
|
||||
- os: macos
|
||||
ruby: '2.6'
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.os == 'windows' }}
|
||||
continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
|
||||
steps:
|
||||
- name: Checkout rubyzip code
|
||||
uses: actions/checkout@v4
|
||||
|
@ -25,7 +22,7 @@ jobs:
|
|||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby }}
|
||||
rubygems: latest
|
||||
rubygems: '3.2.3'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Run the tests
|
||||
|
@ -35,13 +32,29 @@ jobs:
|
|||
FULL_ZIP64_TEST: 1
|
||||
run: bundle exec rake
|
||||
|
||||
- name: Coveralls
|
||||
if: matrix.os == 'ubuntu' && !endsWith(matrix.ruby, 'head')
|
||||
uses: coverallsapp/github-action@v2
|
||||
test-frozen-string-literal:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu]
|
||||
ruby: ['3.3', '3.4', head]
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout rubyzip code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install and set up ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
flag-name: ${{ matrix.ruby }}
|
||||
parallel: true
|
||||
ruby-version: ${{ matrix.ruby }}
|
||||
bundler-cache: true
|
||||
|
||||
- name: Run the tests
|
||||
env:
|
||||
RUBYOPT: --enable-frozen-string-literal
|
||||
FULL_ZIP64_TEST: 1
|
||||
run: bundle exec rake
|
||||
|
||||
test-yjit:
|
||||
strategy:
|
||||
|
@ -66,13 +79,3 @@ jobs:
|
|||
RUBYOPT: --enable-yjit -v
|
||||
FULL_ZIP64_TEST: 1
|
||||
run: bundle exec rake
|
||||
|
||||
finish:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
parallel-finished: true
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
Gemfile.lock
|
||||
+samples/*.zip
|
||||
+samples/*.zip.*
|
||||
samples/zipdialogui.rb
|
||||
coverage
|
||||
html/
|
||||
pkg/
|
||||
.ruby-gemset
|
||||
.ruby-version
|
||||
|
|
47
.rubocop.yml
47
.rubocop.yml
|
@ -1,20 +1,9 @@
|
|||
require:
|
||||
- rubocop-performance
|
||||
- rubocop-rake
|
||||
|
||||
inherit_from: .rubocop_todo.yml
|
||||
|
||||
# Set this to the minimum supported ruby in the gemspec. Otherwise
|
||||
# we get errors if our ruby version doesn't match.
|
||||
AllCops:
|
||||
SuggestExtensions: false
|
||||
TargetRubyVersion: 3.0
|
||||
NewCops: enable
|
||||
|
||||
# Allow this in this file because adding the extra lines is pointless.
|
||||
Layout/EmptyLineBetweenDefs:
|
||||
Exclude:
|
||||
- 'lib/zip/errors.rb'
|
||||
TargetRubyVersion: 2.4
|
||||
|
||||
Layout/HashAlignment:
|
||||
EnforcedHashRocketStyle: table
|
||||
|
@ -23,13 +12,10 @@ Layout/HashAlignment:
|
|||
# Set a workable line length, given the current state of the code,
|
||||
# and turn off for the tests.
|
||||
Layout/LineLength:
|
||||
Max: 100
|
||||
Max: 135
|
||||
Exclude:
|
||||
- 'test/**/*.rb'
|
||||
|
||||
Lint/EmptyClass:
|
||||
Enabled: false
|
||||
|
||||
# In some cases we just need to catch an exception, rather than
|
||||
# actually handle it. Allow the tests to make use of this shortcut.
|
||||
Lint/SuppressedException:
|
||||
|
@ -37,11 +23,6 @@ Lint/SuppressedException:
|
|||
Exclude:
|
||||
- 'test/**/*.rb'
|
||||
|
||||
# Allow this "useless" test, as we are testing <=> here.
|
||||
Lint/BinaryOperatorWithIdenticalOperands:
|
||||
Exclude:
|
||||
- 'test/entry_test.rb'
|
||||
|
||||
# Turn off ABC metrics for the tests and set a workable max given
|
||||
# the current state of the code.
|
||||
Metrics/AbcSize:
|
||||
|
@ -49,10 +30,9 @@ Metrics/AbcSize:
|
|||
Exclude:
|
||||
- 'test/**/*.rb'
|
||||
|
||||
# Turn block length metrics off for the tests and gemspec.
|
||||
# Turn block length metrics off for the tests.
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- 'rubyzip.gemspec'
|
||||
- 'test/**/*.rb'
|
||||
|
||||
# Turn class length metrics off for the tests.
|
||||
|
@ -65,31 +45,10 @@ Metrics/MethodLength:
|
|||
Exclude:
|
||||
- 'test/**/*.rb'
|
||||
|
||||
# These tests are just better with snake_case numbers.
|
||||
Naming/VariableNumber:
|
||||
Exclude:
|
||||
- 'test/file_permissions_test.rb'
|
||||
|
||||
# Need to allow accessors in Entry to be separated for doc purposes.
|
||||
Style/AccessorGrouping:
|
||||
Exclude:
|
||||
- 'lib/zip/entry.rb'
|
||||
|
||||
# Set a consistent way of checking types.
|
||||
Style/ClassCheck:
|
||||
EnforcedStyle: kind_of?
|
||||
|
||||
Style/HashEachMethods:
|
||||
Enabled: true
|
||||
|
||||
Style/HashTransformValues:
|
||||
Enabled: true
|
||||
|
||||
# Allow non-default behaviour for Zip.
|
||||
Style/ModuleFunction:
|
||||
Exclude:
|
||||
- 'lib/zip.rb'
|
||||
|
||||
# Allow this multi-line block chain as it actually reads better
|
||||
# than the alternatives.
|
||||
Style/MultilineBlockChain:
|
||||
|
|
|
@ -1,61 +1,54 @@
|
|||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2021-06-18 14:28:03 UTC using RuboCop version 1.12.1.
|
||||
# on 2020-02-08 14:58:51 +0000 using RuboCop version 0.79.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
Gemspec/DevelopmentDependencies:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 7
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'lib/zip/extra_field.rb'
|
||||
- 'lib/zip/extra_field/ntfs.rb'
|
||||
- 'lib/zip/extra_field/old_unix.rb'
|
||||
- 'lib/zip/extra_field/universal_time.rb'
|
||||
- 'lib/zip/extra_field/unix.rb'
|
||||
- 'lib/zip/extra_field/zip64.rb'
|
||||
- 'lib/zip/extra_field/zip64_placeholder.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Configuration parameters: CountComments, CountAsOne.
|
||||
# Offense count: 15
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/ClassLength:
|
||||
Max: 650
|
||||
Max: 600
|
||||
|
||||
# Offense count: 21
|
||||
# Configuration parameters: IgnoredMethods.
|
||||
# Offense count: 26
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 14
|
||||
|
||||
# Offense count: 47
|
||||
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
||||
# Offense count: 120
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
Metrics/MethodLength:
|
||||
Max: 34
|
||||
Max: 32
|
||||
|
||||
# Offense count: 5
|
||||
# Offense count: 2
|
||||
# Configuration parameters: CountKeywordArgs.
|
||||
Metrics/ParameterLists:
|
||||
Max: 11
|
||||
MaxOptionalParameters: 9
|
||||
Max: 10
|
||||
|
||||
# Offense count: 14
|
||||
# Configuration parameters: IgnoredMethods.
|
||||
# Offense count: 21
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 15
|
||||
|
||||
# Offense count: 7
|
||||
# Offense count: 9
|
||||
Naming/AccessorMethodName:
|
||||
Exclude:
|
||||
- 'lib/zip/entry.rb'
|
||||
- 'lib/zip/filesystem.rb'
|
||||
- 'lib/zip/input_stream.rb'
|
||||
- 'lib/zip/streamable_stream.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: inline, group
|
||||
Style/AccessModifierDeclarations:
|
||||
Exclude:
|
||||
- 'lib/zip/central_directory.rb'
|
||||
- 'lib/zip/extra_field/zip64.rb'
|
||||
- 'lib/zip/filesystem.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
||||
# SupportedStyles: nested, compact
|
||||
Style/ClassAndModuleChildren:
|
||||
Exclude:
|
||||
|
@ -64,49 +57,71 @@ Style/ClassAndModuleChildren:
|
|||
- 'lib/zip/extra_field/old_unix.rb'
|
||||
- 'lib/zip/extra_field/universal_time.rb'
|
||||
- 'lib/zip/extra_field/unix.rb'
|
||||
- 'lib/zip/extra_field/unknown.rb'
|
||||
- 'lib/zip/extra_field/zip64.rb'
|
||||
- 'lib/zip/extra_field/zip64_placeholder.rb'
|
||||
|
||||
# Offense count: 22
|
||||
# Configuration parameters: AllowedConstants.
|
||||
# Offense count: 26
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 13
|
||||
# Offense count: 3
|
||||
# Configuration parameters: .
|
||||
# SupportedStyles: annotated, template, unannotated
|
||||
Style/FormatStringToken:
|
||||
EnforcedStyle: unannotated
|
||||
|
||||
# Offense count: 95
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: always, never
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 17
|
||||
# Cop supports --auto-correct.
|
||||
Style/IfUnlessModifier:
|
||||
Exclude:
|
||||
- 'lib/zip/entry.rb'
|
||||
- 'lib/zip/file_split.rb'
|
||||
- 'lib/zip/filesystem/dir.rb'
|
||||
- 'lib/zip/filesystem/file.rb'
|
||||
- 'lib/zip/extra_field/generic.rb'
|
||||
- 'lib/zip/file.rb'
|
||||
- 'lib/zip/filesystem.rb'
|
||||
- 'lib/zip/input_stream.rb'
|
||||
- 'lib/zip/pass_thru_decompressor.rb'
|
||||
- 'lib/zip/streamable_stream.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, Autocorrect.
|
||||
# SupportedStyles: module_function, extend_self
|
||||
Style/ModuleFunction:
|
||||
Exclude:
|
||||
- 'lib/zip.rb'
|
||||
|
||||
# Offense count: 56
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: literals, strict
|
||||
Style/MutableConstant:
|
||||
Exclude:
|
||||
- 'lib/zip/extra_field.rb'
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 21
|
||||
# Offense count: 23
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, IgnoredMethods.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
|
||||
# SupportedStyles: predicate, comparison
|
||||
Style/NumericPredicate:
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'lib/zip/entry.rb'
|
||||
- 'lib/zip/extra_field/old_unix.rb'
|
||||
- 'lib/zip/extra_field/universal_time.rb'
|
||||
- 'lib/zip/extra_field/unix.rb'
|
||||
- 'lib/zip/file.rb'
|
||||
- 'lib/zip/filesystem/file.rb'
|
||||
- 'lib/zip/filesystem.rb'
|
||||
- 'lib/zip/input_stream.rb'
|
||||
- 'lib/zip/ioextras.rb'
|
||||
- 'lib/zip/ioextras/abstract_input_stream.rb'
|
||||
- 'test/file_split_test.rb'
|
||||
- 'test/test_helper.rb'
|
||||
|
||||
# Offense count: 17
|
||||
# Cop supports --auto-correct.
|
||||
|
|
22
.simplecov
22
.simplecov
|
@ -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
|
74
Changelog.md
74
Changelog.md
|
@ -1,72 +1,4 @@
|
|||
# 3.0.0 (Next)
|
||||
|
||||
- Fix de facto regression for input streams.
|
||||
- Fix `File#write_buffer` to always return the given `io`.
|
||||
- Add `Entry#absolute_time?` and `DOSTime#absolute_time?` methods.
|
||||
- Use explicit named parameters for `File` methods.
|
||||
- Ensure that entries can be extracted safely without path traversal. [#540](https://github.com/rubyzip/rubyzip/issues/540)
|
||||
- Enable Zip64 by default.
|
||||
- Rename `GPFBit3Error` to `StreamingError`.
|
||||
- Ensure that `Entry.ftype` is correct via `InputStream`. [#533](https://github.com/rubyzip/rubyzip/issues/533)
|
||||
- Add `Entry#zip64?` as a better way detect Zip64 entries.
|
||||
- Implement `Zip::FileSystem::ZipFsFile#symlink?`.
|
||||
- Remove `File::add_buffer` from the API.
|
||||
- Fix `OutputStream#put_next_entry` to preserve `StreamableStream`s. [#503](https://github.com/rubyzip/rubyzip/issues/503)
|
||||
- Ensure `File.open_buffer` doesn't rewrite unchanged data.
|
||||
- Add `CentralDirectory#count_entries` and `File::count_entries`.
|
||||
- Fix reading unknown extra fields. [#505](https://github.com/rubyzip/rubyzip/issues/505)
|
||||
- Fix reading zip files with max length file comment. [#508](https://github.com/rubyzip/rubyzip/issues/508)
|
||||
- Fix reading zip64 files with max length file comment. [#509](https://github.com/rubyzip/rubyzip/issues/509)
|
||||
- Don't silently alter zip files opened with `Zip::sort_entries`. [#329](https://github.com/rubyzip/rubyzip/issues/329)
|
||||
- Use named parameters for optional arguments in the public API.
|
||||
- Raise an error if entry names exceed 65,535 characters. [#247](https://github.com/rubyzip/rubyzip/issues/247)
|
||||
- Remove the `ZipXError` v1 legacy classes.
|
||||
- Raise an error on reading a split archive with `InputStream`. [#349](https://github.com/rubyzip/rubyzip/issues/349)
|
||||
- Ensure `InputStream` raises `GPFBit3Error` for OSX Archive files. [#493](https://github.com/rubyzip/rubyzip/issues/493)
|
||||
- Improve documentation and error messages for `InputStream`. [#196](https://github.com/rubyzip/rubyzip/issues/196)
|
||||
- Fix zip file-level comment is not read from zip64 files. [#492](https://github.com/rubyzip/rubyzip/issues/492)
|
||||
- Fix `Zip::OutputStream.write_buffer` doesn't work with Tempfiles. [#265](https://github.com/rubyzip/rubyzip/issues/265)
|
||||
- Reinstate normalising pathname separators to /. [#487](https://github.com/rubyzip/rubyzip/pull/487)
|
||||
- Fix restore options consistency. [#486](https://github.com/rubyzip/rubyzip/pull/486)
|
||||
- View and/or preserve original date created, date modified? (Windows). [#336](https://github.com/rubyzip/rubyzip/issues/336)
|
||||
- Fix frozen string literal error. [#475](https://github.com/rubyzip/rubyzip/pull/475)
|
||||
- Set the default `Entry` time to the file's mtime on Windows. [#465](https://github.com/rubyzip/rubyzip/issues/465)
|
||||
- Ensure that `Entry#time=` sets times as `DOSTime` objects. [#481](https://github.com/rubyzip/rubyzip/issues/481)
|
||||
- Replace and deprecate `Zip::DOSTime#dos_equals`. [#464](https://github.com/rubyzip/rubyzip/pull/464)
|
||||
- Fix loading extra fields. [#459](https://github.com/rubyzip/rubyzip/pull/459)
|
||||
- Set compression level on a per-zipfile basis. [#448](https://github.com/rubyzip/rubyzip/pull/448)
|
||||
- Fix input stream partial read error. [#462](https://github.com/rubyzip/rubyzip/pull/462)
|
||||
- Fix zlib deflate buffer growth. [#447](https://github.com/rubyzip/rubyzip/pull/447)
|
||||
|
||||
Tooling/internal:
|
||||
|
||||
- Add a test to ensure correct version number format.
|
||||
- Update the README with new Ruby version compatability information.
|
||||
- Fix various issues with JRuby tests.
|
||||
- Update gem dependency versions.
|
||||
- Add Ruby 3.4 to the CI.
|
||||
- Fix mispelled variable names in the crypto classes.
|
||||
- Only use the Zip64 CDIR end locator if needed.
|
||||
- Prevent unnecessary Zip64 data being stored.
|
||||
- Abstract marking various things as 'dirty' into `Dirtyable` for reuse.
|
||||
- Properly test `File#mkdir`.
|
||||
- Remove unused private method `File#directory?`.
|
||||
- Expose the `EntrySet` more cleanly through `CentralDirectory`.
|
||||
- `Zip::File` no longer subclasses `Zip::CentralDirectory`.
|
||||
- Configure Coveralls to not report a failure on minor decreases of test coverage. [#491](https://github.com/rubyzip/rubyzip/issues/491)
|
||||
- Extract the file splitting code out into its own module.
|
||||
- Refactor, and tidy up, the `Zip::Filesystem` classes for improved maintainability.
|
||||
- Fix Windows tests. [#489](https://github.com/rubyzip/rubyzip/pull/489)
|
||||
- Refactor `assert_forwarded` so it does not need `ObjectSpace._id2ref` or `eval`. [#483](https://github.com/rubyzip/rubyzip/pull/483)
|
||||
- Add GitHub Actions CI infrastructure. [#469](https://github.com/rubyzip/rubyzip/issues/469)
|
||||
- Add Ruby 3.0 to CI. [#474](https://github.com/rubyzip/rubyzip/pull/474)
|
||||
- Fix the compression level tests to compare relative sizes. [#473](https://github.com/rubyzip/rubyzip/pull/473)
|
||||
- Simplify assertions in basic_zip_file_test. [#470](https://github.com/rubyzip/rubyzip/pull/470)
|
||||
- Remove compare_enumerables from test_helper.rb. [#468](https://github.com/rubyzip/rubyzip/pull/468)
|
||||
- Use correct SPDX license identifier. [#458](https://github.com/rubyzip/rubyzip/pull/458)
|
||||
- Enable truffle ruby in Travis CI. [#450](https://github.com/rubyzip/rubyzip/pull/450)
|
||||
- Update rubocop again and run it in CI. [#444](https://github.com/rubyzip/rubyzip/pull/444)
|
||||
- Fix a test that was incorrect on big-endian architectures. [#445](https://github.com/rubyzip/rubyzip/pull/445)
|
||||
# X.X.X (Next)
|
||||
|
||||
# 2.4.1 (2025-01-05)
|
||||
|
||||
|
@ -93,11 +25,11 @@ Tooling:
|
|||
|
||||
# 2.3.2 (2021-07-05)
|
||||
|
||||
- A "dummy" release to warn about breaking changes coming in version 3.0. This updated version uses the Gem `post_install_message` instead of printing to `STDERR`.
|
||||
- This is a dummy release to warn about breaking changes coming in version 3.0. This updated version uses the Gem `post_install_message` instead of printing to `STDERR`.
|
||||
|
||||
# 2.3.1 (2021-07-03)
|
||||
|
||||
- A "dummy" release to warn about breaking changes coming in version 3.0.
|
||||
- This is a dummy release to warn about breaking changes coming in version 3.0.
|
||||
|
||||
# 2.3.0 (2020-03-14)
|
||||
|
||||
|
|
9
Gemfile
9
Gemfile
|
@ -1,12 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
|
||||
# TODO: remove when JRuby 9.4.10.0 will be released and available on CI
|
||||
# Ref: https://github.com/jruby/jruby/issues/7262
|
||||
if RUBY_PLATFORM.include?('java')
|
||||
gem 'jar-dependencies', '0.4.1'
|
||||
gem 'ruby-maven', '3.3.13'
|
||||
end
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
guard :minitest do
|
||||
# with Minitest::Unit
|
||||
watch(%r{^test/(.*)/?(.*)_test\.rb$})
|
||||
watch(%r{^test/(.*)\/?(.*)_test\.rb$})
|
||||
watch(%r{^lib/zip/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
|
||||
watch(%r{^test/test_helper\.rb$}) { 'test' }
|
||||
end
|
||||
|
|
24
LICENSE.md
24
LICENSE.md
|
@ -1,24 +0,0 @@
|
|||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2002-2025, The Rubyzip Developers
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
172
README.md
172
README.md
|
@ -2,21 +2,23 @@
|
|||
|
||||
[](http://badge.fury.io/rb/rubyzip)
|
||||
[](https://github.com/rubyzip/rubyzip/actions/workflows/tests.yml)
|
||||
[](https://github.com/rubyzip/rubyzip/actions/workflows/lint.yml)
|
||||
[](https://codeclimate.com/github/rubyzip/rubyzip)
|
||||
[](https://coveralls.io/r/rubyzip/rubyzip?branch=master)
|
||||
|
||||
Rubyzip is a ruby library for reading and writing zip files.
|
||||
|
||||
## Important notes
|
||||
## Important note
|
||||
|
||||
Rubyzip 2.4 is intended to be the last release in the 2.x series. Please get ready for version 3.0.
|
||||
|
||||
### Updating to version 3.0
|
||||
|
||||
The public API of some classes has been modernized to use named parameters for optional arguments. Please check your usage of the following Rubyzip classes:
|
||||
The public API of some classes has been modernized to use named parameters for optional arguments. Also some methods have been changed or removed. Please check your usage of the following Rubyzip classes:
|
||||
* `File`
|
||||
* `Entry`
|
||||
* `InputStream`
|
||||
* `OutputStream`
|
||||
* `DOSTime`
|
||||
|
||||
**Please see [Updating to version 3.x](https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x) in the wiki for details.**
|
||||
|
||||
|
@ -55,7 +57,7 @@ input_filenames = ['image.jpg', 'description.txt', 'stats.csv']
|
|||
|
||||
zipfile_name = "/Users/me/Desktop/archive.zip"
|
||||
|
||||
Zip::File.open(zipfile_name, create: true) do |zipfile|
|
||||
Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
|
||||
input_filenames.each do |filename|
|
||||
# Two arguments:
|
||||
# - The name of the file as it will appear in the archive
|
||||
|
@ -94,7 +96,7 @@ class ZipFileGenerator
|
|||
def write
|
||||
entries = Dir.entries(@input_dir) - %w[. ..]
|
||||
|
||||
::Zip::File.open(@output_file, create: true) do |zipfile|
|
||||
::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
|
||||
write_entries entries, '', zipfile
|
||||
end
|
||||
end
|
||||
|
@ -127,9 +129,9 @@ class ZipFileGenerator
|
|||
end
|
||||
```
|
||||
|
||||
### Save zip archive entries sorted by name
|
||||
### Save zip archive entries in sorted by name state
|
||||
|
||||
To save zip archives with their entries sorted by name (see below), set `::Zip.sort_entries` to `true`
|
||||
To save zip archives in sorted order like below, you need to set `::Zip.sort_entries` to `true`
|
||||
|
||||
```
|
||||
Vegetable/
|
||||
|
@ -143,7 +145,7 @@ fruit/mango
|
|||
fruit/orange
|
||||
```
|
||||
|
||||
Opening an existing zip file with this option set will not change the order of the entries automatically. Altering the zip file - adding an entry, renaming an entry, adding or changing the archive comment, etc - will cause the ordering to be applied when closing the file.
|
||||
After this, entries in the zip archive will be saved in ordered state.
|
||||
|
||||
### Default permissions of zip archives
|
||||
|
||||
|
@ -179,71 +181,28 @@ Zip::File.open('foo.zip') do |zip_file|
|
|||
end
|
||||
```
|
||||
|
||||
### Notes on `Zip::InputStream`
|
||||
#### Notice about ::Zip::InputStream
|
||||
|
||||
`Zip::InputStream` can be used for faster reading of zip file content because it does not read the Central directory up front.
|
||||
`::Zip::InputStream` usable for fast reading zip file content because it not read Central directory.
|
||||
|
||||
There is one exception where it can not work however, and this is if the file does not contain enough information in the local entry headers to extract an entry. This is indicated in an entry by the General Purpose Flag bit 3 being set.
|
||||
But there is one exception when it is not working - General Purpose Flag Bit 3.
|
||||
|
||||
> If bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written. The fields in the local header are filled with zero, and the CRC-32 and size are appended in a 12-byte structure (optionally preceded by a 4-byte signature) immediately after the compressed data.
|
||||
> If bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written. The fields in the local header are filled with zero, and the CRC-32 and size are appended in a 12-byte structure (optionally preceded by a 4-byte signature) immediately after the compressed data
|
||||
|
||||
If `Zip::InputStream` finds such an entry in the zip archive it will raise an exception (`Zip::StreamingError`).
|
||||
|
||||
`Zip::InputStream` is not designed to be used for random access in a zip file. When performing any operations on an entry that you are accessing via `Zip::InputStream.get_next_entry` then you should complete any such operations before the next call to `get_next_entry`.
|
||||
|
||||
```ruby
|
||||
zip_stream = Zip::InputStream.new(File.open('file.zip'))
|
||||
|
||||
while entry = zip_stream.get_next_entry
|
||||
# All required operations on `entry` go here.
|
||||
end
|
||||
```
|
||||
|
||||
Any attempt to move about in a zip file opened with `Zip::InputStream` could result in the incorrect entry being accessed and/or Zlib buffer errors. If you need random access in a zip file, use `Zip::File`.
|
||||
If `::Zip::InputStream` finds such entry in the zip archive it will raise an exception.
|
||||
|
||||
### Password Protection (Experimental)
|
||||
|
||||
Rubyzip supports reading/writing zip files with traditional zip encryption (a.k.a. "ZipCrypto"). AES encryption is not yet supported. It can be used with buffer streams, e.g.:
|
||||
|
||||
#### Version 2.x
|
||||
|
||||
```ruby
|
||||
# Writing.
|
||||
enc = Zip::TraditionalEncrypter.new('password')
|
||||
buffer = Zip::OutputStream.write_buffer(::StringIO.new(''), enc) do |output|
|
||||
output.put_next_entry("my_file.txt")
|
||||
output.write my_data
|
||||
end
|
||||
|
||||
# Reading.
|
||||
dec = Zip::TraditionalDecrypter.new('password')
|
||||
Zip::InputStream.open(buffer, 0, dec) do |input|
|
||||
entry = input.get_next_entry
|
||||
puts "Contents of '#{entry.name}':"
|
||||
puts input.read
|
||||
end
|
||||
Zip::OutputStream.write_buffer(::StringIO.new, Zip::TraditionalEncrypter.new('password')) do |out|
|
||||
out.put_next_entry("my_file.txt")
|
||||
out.write my_data
|
||||
end.string
|
||||
```
|
||||
|
||||
#### Version 3.x
|
||||
|
||||
```ruby
|
||||
# Writing.
|
||||
enc = Zip::TraditionalEncrypter.new('password')
|
||||
buffer = Zip::OutputStream.write_buffer(encrypter: enc) do |output|
|
||||
output.put_next_entry("my_file.txt")
|
||||
output.write my_data
|
||||
end
|
||||
|
||||
# Reading.
|
||||
dec = Zip::TraditionalDecrypter.new('password')
|
||||
Zip::InputStream.open(buffer, decrypter: dec) do |input|
|
||||
entry = input.get_next_entry
|
||||
puts "Contents of '#{entry.name}':"
|
||||
puts input.read
|
||||
end
|
||||
```
|
||||
|
||||
_This is an experimental feature and the interface for encryption may change in future versions._
|
||||
This is an experimental feature and the interface for encryption may change in future versions.
|
||||
|
||||
## Known issues
|
||||
|
||||
|
@ -337,37 +296,25 @@ Zip.validate_entry_sizes = false
|
|||
|
||||
Note that if you use the lower level `Zip::InputStream` interface, `rubyzip` does *not* check the entry `size`s. In this case, the caller is responsible for making sure it does not read more data than expected from the input stream.
|
||||
|
||||
### Compression level
|
||||
### Default Compression
|
||||
|
||||
When adding entries to a zip archive you can set the compression level to trade-off compressed size against compression speed. By default this is set to the same as the underlying Zlib library's default (`Zlib::DEFAULT_COMPRESSION`), which is somewhere in the middle.
|
||||
|
||||
You can configure the default compression level with:
|
||||
You can set the default compression level like so:
|
||||
|
||||
```ruby
|
||||
Zip.default_compression = X
|
||||
Zip.default_compression = Zlib::DEFAULT_COMPRESSION
|
||||
```
|
||||
|
||||
Where X is an integer between 0 and 9, inclusive. If this option is set to 0 (`Zlib::NO_COMPRESSION`) then entries will be stored in the zip archive uncompressed. A value of 1 (`Zlib::BEST_SPEED`) gives the fastest compression and 9 (`Zlib::BEST_COMPRESSION`) gives the smallest compressed file size.
|
||||
|
||||
This can also be set for each archive as an option to `Zip::File`:
|
||||
|
||||
```ruby
|
||||
Zip::File.open('foo.zip', create:true, compression_level: 9) do |zip|
|
||||
zip.add ...
|
||||
end
|
||||
```
|
||||
It defaults to `Zlib::DEFAULT_COMPRESSION`. Possible values are `Zlib::BEST_COMPRESSION`, `Zlib::DEFAULT_COMPRESSION` and `Zlib::NO_COMPRESSION`
|
||||
|
||||
### Zip64 Support
|
||||
|
||||
Since version 3.0, Zip64 support is enabled for writing by default. To disable it do this:
|
||||
By default, Zip64 support is disabled for writing. To enable it do this:
|
||||
|
||||
```ruby
|
||||
Zip.write_zip64_support = false
|
||||
Zip.write_zip64_support = true
|
||||
```
|
||||
|
||||
Prior to version 3.0, Zip64 support is disabled for writing by default.
|
||||
|
||||
_NOTE_: If Zip64 write support is enabled then any extractor subsequently used may also require Zip64 support to read from the resultant archive.
|
||||
_NOTE_: If you will enable Zip64 writing then you will need zip extractor with Zip64 support to extract archive.
|
||||
|
||||
### Block Form
|
||||
|
||||
|
@ -382,50 +329,15 @@ You can set multiple settings at the same time by using a block:
|
|||
end
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
Rubyzip is known to run on a number of platforms and under a number of different Ruby versions.
|
||||
|
||||
### Version 2.3.x
|
||||
|
||||
Rubyzip 2.3 is known to work on MRI 2.4 to 3.4 on Linux and Mac, and JRuby and Truffleruby on Linux. There are known issues with Windows which have been fixed on the development branch. Please [let us know](https://github.com/rubyzip/rubyzip/pulls) if you know Rubyzip 2.3 works on a platform/Ruby combination not listed here, or [raise an issue](https://github.com/rubyzip/rubyzip/issues) if you see a failure where we think it should work.
|
||||
|
||||
### Next (version 3.0.0)
|
||||
|
||||
Please see the table below for what we think the current situation is. Note: an empty cell means "unknown", not "does not work".
|
||||
|
||||
| OS/Ruby | 3.0 | 3.1 | 3.2 | 3.3 | 3.4 | Head | JRuby 9.4.9.0 | JRuby Head | Truffleruby 24.1.1 | Truffleruby Head |
|
||||
|---------|-----|-----|-----|-----|-----|------|---------------|------------|--------------------|------------------|
|
||||
|Ubuntu 22.04| CI | CI | CI | CI | CI | ci | CI | ci | CI | ci |
|
||||
|Mac OS 14.7.2| CI | CI | CI | CI | CI | ci | x | | x | |
|
||||
|Windows Server 2022| CI | | | | CI mswin</br>CI ucrt | | | | | |
|
||||
|
||||
Key: `CI` - tested in CI, should work; `ci` - tested in CI, might fail; `x` - known working; `o` - known failing.
|
||||
|
||||
Rubies 3.1+ are also tested separately with YJIT turned on (Ubuntu and Mac OS).
|
||||
|
||||
See [the Actions tab](https://github.com/rubyzip/rubyzip/actions) in GitHub for full details.
|
||||
|
||||
Please [raise a PR](https://github.com/rubyzip/rubyzip/pulls) if you know Rubyzip works on a platform/Ruby combination not listed here, or [raise an issue](https://github.com/rubyzip/rubyzip/issues) if you see a failure where we think it should work.
|
||||
|
||||
## Developing
|
||||
|
||||
Install the dependencies:
|
||||
To run the test you need to do this:
|
||||
|
||||
```shell
|
||||
bundle install
|
||||
```
|
||||
|
||||
Run the tests with `rake`:
|
||||
|
||||
```shell
|
||||
bundle install
|
||||
rake
|
||||
```
|
||||
|
||||
Please also run `rubocop` over your changes.
|
||||
|
||||
Our CI runs on [GitHub Actions](https://github.com/rubyzip/rubyzip/actions). Please note that `rubocop` is run as part of the CI configuration and will fail a build if errors are found.
|
||||
|
||||
## Website and Project Home
|
||||
|
||||
http://github.com/rubyzip/rubyzip
|
||||
|
@ -434,29 +346,17 @@ http://rdoc.info/github/rubyzip/rubyzip/master/frames
|
|||
|
||||
## Authors
|
||||
|
||||
See https://github.com/rubyzip/rubyzip/graphs/contributors for a comprehensive list.
|
||||
Alexander Simonov ( alex at simonov.me)
|
||||
|
||||
### Current maintainers
|
||||
Alan Harper ( alan at aussiegeek.net)
|
||||
|
||||
* Robert Haines (@hainesr)
|
||||
* John Lees-Miller (@jdleesmiller)
|
||||
* Oleksandr Simonov (@simonoff)
|
||||
Thomas Sondergaard (thomas at sondergaard.cc)
|
||||
|
||||
### Original author
|
||||
Technorama Ltd. (oss-ruby-zip at technorama.net)
|
||||
|
||||
* Thomas Sondergaard
|
||||
extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org)
|
||||
|
||||
## License
|
||||
|
||||
Rubyzip is distributed under the same license as Ruby. In practice this means you can use it under the terms of the Ruby License or the 2-Clause BSD License. See https://www.ruby-lang.org/en/about/license.txt and LICENSE.md for details.
|
||||
|
||||
## Research notice
|
||||
Please note that this repository is participating in a study into sustainability
|
||||
of open source projects. Data will be gathered about this repository for
|
||||
approximately the next 12 months, starting from June 2021.
|
||||
|
||||
Data collected will include number of contributors, number of PRs, time taken to
|
||||
close/merge these PRs, and issues closed.
|
||||
|
||||
For more information, please visit
|
||||
[our informational page](https://sustainable-open-science-and-software.github.io/) or download our [participant information sheet](https://sustainable-open-science-and-software.github.io/assets/PIS_sustainable_software.pdf).
|
||||
Rubyzip is distributed under the same license as ruby. See
|
||||
http://www.ruby-lang.org/en/LICENSE.txt
|
||||
|
|
18
Rakefile
18
Rakefile
|
@ -1,8 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler/gem_tasks'
|
||||
require 'rake/testtask'
|
||||
require 'rdoc/task'
|
||||
require 'rubocop/rake_task'
|
||||
|
||||
task default: :test
|
||||
|
@ -14,12 +11,11 @@ Rake::TestTask.new(:test) do |test|
|
|||
test.verbose = true
|
||||
end
|
||||
|
||||
RDoc::Task.new do |rdoc|
|
||||
rdoc.main = 'README.md'
|
||||
rdoc.rdoc_files.include('README.md', 'lib/**/*.rb')
|
||||
rdoc.options << '--markup=markdown'
|
||||
rdoc.options << '--tab-width=2'
|
||||
rdoc.options << "-t Rubyzip version #{Zip::VERSION}"
|
||||
end
|
||||
|
||||
RuboCop::RakeTask.new
|
||||
|
||||
# Rake::TestTask.new(:zip64_full_test) do |test|
|
||||
# test.libs << File.join(File.dirname(__FILE__), 'lib')
|
||||
# test.libs << File.join(File.dirname(__FILE__), 'test')
|
||||
# test.pattern = File.join(File.dirname(__FILE__), 'test/zip64_full_test.rb')
|
||||
# test.verbose = true
|
||||
# end
|
||||
|
|
|
@ -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
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler/setup'
|
||||
require 'zip'
|
||||
|
|
40
lib/zip.rb
40
lib/zip.rb
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'English'
|
||||
require 'delegate'
|
||||
require 'singleton'
|
||||
|
@ -35,12 +33,26 @@ require 'zip/streamable_stream'
|
|||
require 'zip/streamable_directory'
|
||||
require 'zip/errors'
|
||||
|
||||
# Rubyzip is a ruby module for reading and writing zip files.
|
||||
#
|
||||
# The main entry points are File, InputStream and OutputStream. For a
|
||||
# file/directory interface in the style of the standard ruby ::File and
|
||||
# ::Dir APIs then `require 'zip/filesystem'` and see FileSystem.
|
||||
module Zip
|
||||
V3_API_WARNING_MSG = <<~END_MSG
|
||||
You have called '%s' (from %s).
|
||||
This method is changing or deprecated in version 3.0.0. Please see
|
||||
https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x
|
||||
for more information.
|
||||
END_MSG
|
||||
|
||||
def self.warn_about_v3_api(method)
|
||||
return unless ENV['RUBYZIP_V3_API_WARN']
|
||||
|
||||
loc = caller_locations(2, 1)[0]
|
||||
from = "#{loc.path.split('/').last}:#{loc.lineno}"
|
||||
warn format(V3_API_WARNING_MSG, method, from)
|
||||
end
|
||||
|
||||
if ENV['RUBYZIP_V3_API_WARN'] && RUBY_VERSION < '3.0'
|
||||
warn 'RubyZip 3.0 will require Ruby 3.0 or later.'
|
||||
end
|
||||
|
||||
extend self
|
||||
attr_accessor :unicode_names,
|
||||
:on_exists_proc,
|
||||
|
@ -53,27 +65,19 @@ module Zip
|
|||
:force_entry_names_encoding,
|
||||
:validate_entry_sizes
|
||||
|
||||
DEFAULT_RESTORE_OPTIONS = {
|
||||
restore_ownership: false,
|
||||
restore_permissions: true,
|
||||
restore_times: true
|
||||
}.freeze # :nodoc:
|
||||
|
||||
def reset! # :nodoc:
|
||||
def reset!
|
||||
@_ran_once = false
|
||||
@unicode_names = false
|
||||
@on_exists_proc = false
|
||||
@continue_on_exists_proc = false
|
||||
@sort_entries = false
|
||||
@default_compression = Zlib::DEFAULT_COMPRESSION
|
||||
@write_zip64_support = true
|
||||
@default_compression = ::Zlib::DEFAULT_COMPRESSION
|
||||
@write_zip64_support = false
|
||||
@warn_invalid_date = true
|
||||
@case_insensitive_match = false
|
||||
@force_entry_names_encoding = nil
|
||||
@validate_entry_sizes = true
|
||||
end
|
||||
|
||||
# Set options for RubyZip in one block.
|
||||
def setup
|
||||
yield self unless @_ran_once
|
||||
@_ran_once = true
|
||||
|
|
|
@ -1,42 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
|
||||
require_relative 'dirtyable'
|
||||
|
||||
module Zip
|
||||
class CentralDirectory # :nodoc:
|
||||
extend Forwardable
|
||||
include Dirtyable
|
||||
|
||||
END_OF_CD_SIG = 0x06054b50
|
||||
ZIP64_END_OF_CD_SIG = 0x06064b50
|
||||
ZIP64_EOCD_LOCATOR_SIG = 0x07064b50
|
||||
class CentralDirectory
|
||||
include Enumerable
|
||||
|
||||
END_OF_CDS = 0x06054b50
|
||||
ZIP64_END_OF_CDS = 0x06064b50
|
||||
ZIP64_EOCD_LOCATOR = 0x07064b50
|
||||
MAX_END_OF_CDS_SIZE = 65_536 + 18
|
||||
STATIC_EOCD_SIZE = 22
|
||||
ZIP64_STATIC_EOCD_SIZE = 56
|
||||
ZIP64_EOCD_LOC_SIZE = 20
|
||||
MAX_FILE_COMMENT_SIZE = (1 << 16) - 1
|
||||
MAX_END_OF_CD_SIZE =
|
||||
MAX_FILE_COMMENT_SIZE + STATIC_EOCD_SIZE + ZIP64_EOCD_LOC_SIZE
|
||||
|
||||
attr_accessor :comment
|
||||
attr_reader :comment
|
||||
|
||||
def_delegators :@entry_set,
|
||||
:<<, :delete, :each, :entries, :find_entry, :glob,
|
||||
:include?, :size
|
||||
|
||||
mark_dirty :<<, :comment=, :delete
|
||||
|
||||
def initialize(entries = EntrySet.new, comment = '') # :nodoc:
|
||||
super(dirty_on_create: false)
|
||||
@entry_set = entries.kind_of?(EntrySet) ? entries : EntrySet.new(entries)
|
||||
@comment = comment
|
||||
# Returns an Enumerable containing the entries.
|
||||
def entries
|
||||
@entry_set.entries
|
||||
end
|
||||
|
||||
def read_from_stream(io)
|
||||
read_eocds(io)
|
||||
read_central_directory_entries(io)
|
||||
def initialize(entries = EntrySet.new, comment = '') #:nodoc:
|
||||
super()
|
||||
@entry_set = entries.kind_of?(EntrySet) ? entries : EntrySet.new(entries)
|
||||
@comment = comment
|
||||
end
|
||||
|
||||
def write_to_stream(io) #:nodoc:
|
||||
|
@ -44,34 +26,20 @@ module Zip
|
|||
@entry_set.each { |entry| entry.write_c_dir_entry(io) }
|
||||
eocd_offset = io.tell
|
||||
cdir_size = eocd_offset - cdir_offset
|
||||
if Zip.write_zip64_support &&
|
||||
(cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF)
|
||||
if ::Zip.write_zip64_support
|
||||
need_zip64_eocd = cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF
|
||||
need_zip64_eocd ||= @entry_set.any? { |entry| entry.extra['Zip64'] }
|
||||
if need_zip64_eocd
|
||||
write_64_e_o_c_d(io, cdir_offset, cdir_size)
|
||||
write_64_eocd_locator(io, eocd_offset)
|
||||
end
|
||||
end
|
||||
write_e_o_c_d(io, cdir_offset, cdir_size)
|
||||
end
|
||||
|
||||
# Reads the End of Central Directory Record (and the Zip64 equivalent if
|
||||
# needs be) and returns the number of entries in the archive. This is a
|
||||
# convenience method that avoids reading in all of the entry data to get a
|
||||
# very quick entry count.
|
||||
def count_entries(io)
|
||||
read_eocds(io)
|
||||
@size
|
||||
end
|
||||
|
||||
def ==(other) # :nodoc:
|
||||
return false unless other.kind_of?(CentralDirectory)
|
||||
|
||||
@entry_set.entries.sort == other.entries.sort && comment == other.comment
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def write_e_o_c_d(io, offset, cdir_size) #:nodoc:
|
||||
tmp = [
|
||||
END_OF_CD_SIG,
|
||||
END_OF_CDS,
|
||||
0, # @numberOfThisDisk
|
||||
0, # @numberOfDiskWithStartOfCDir
|
||||
@entry_set ? [@entry_set.size, 0xFFFF].min : 0,
|
||||
|
@ -84,9 +52,11 @@ module Zip
|
|||
io << @comment
|
||||
end
|
||||
|
||||
private :write_e_o_c_d
|
||||
|
||||
def write_64_e_o_c_d(io, offset, cdir_size) #:nodoc:
|
||||
tmp = [
|
||||
ZIP64_END_OF_CD_SIG,
|
||||
ZIP64_END_OF_CDS,
|
||||
44, # size of zip64 end of central directory record (excludes signature and field itself)
|
||||
VERSION_MADE_BY,
|
||||
VERSION_NEEDED_TO_EXTRACT_ZIP64,
|
||||
|
@ -100,9 +70,11 @@ module Zip
|
|||
io << tmp.pack('VQ<vvVVQ<Q<Q<Q<')
|
||||
end
|
||||
|
||||
private :write_64_e_o_c_d
|
||||
|
||||
def write_64_eocd_locator(io, zip64_eocd_offset)
|
||||
tmp = [
|
||||
ZIP64_EOCD_LOCATOR_SIG,
|
||||
ZIP64_EOCD_LOCATOR,
|
||||
0, # number of disk containing the start of zip64 eocd record
|
||||
zip64_eocd_offset, # offset of the start of zip64 eocd record in its disk
|
||||
1 # total number of disks
|
||||
|
@ -110,145 +82,127 @@ module Zip
|
|||
io << tmp.pack('VVQ<V')
|
||||
end
|
||||
|
||||
def unpack_64_e_o_c_d(buffer) # :nodoc:
|
||||
_, # ZIP64_END_OF_CD_SIG. We know we have this at this point.
|
||||
@size_of_zip64_e_o_c_d,
|
||||
@version_made_by,
|
||||
@version_needed_for_extract,
|
||||
@number_of_this_disk,
|
||||
@number_of_disk_with_start_of_cdir,
|
||||
@total_number_of_entries_in_cdir_on_this_disk,
|
||||
@size,
|
||||
@size_in_bytes,
|
||||
@cdir_offset = buffer.unpack('VQ<vvVVQ<Q<Q<Q<')
|
||||
private :write_64_eocd_locator
|
||||
|
||||
zip64_extensible_data_size =
|
||||
@size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
|
||||
@zip64_extensible_data = if zip64_extensible_data_size.zero?
|
||||
''
|
||||
def read_64_e_o_c_d(buf) #:nodoc:
|
||||
buf = get_64_e_o_c_d(buf)
|
||||
@size_of_zip64_e_o_c_d = Entry.read_zip_64_long(buf)
|
||||
@version_made_by = Entry.read_zip_short(buf)
|
||||
@version_needed_for_extract = Entry.read_zip_short(buf)
|
||||
@number_of_this_disk = Entry.read_zip_long(buf)
|
||||
@number_of_disk_with_start_of_cdir = Entry.read_zip_long(buf)
|
||||
@total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_64_long(buf)
|
||||
@size = Entry.read_zip_64_long(buf)
|
||||
@size_in_bytes = Entry.read_zip_64_long(buf)
|
||||
@cdir_offset = Entry.read_zip_64_long(buf)
|
||||
@zip_64_extensible = buf.slice!(0, buf.bytesize)
|
||||
raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
|
||||
end
|
||||
|
||||
def read_e_o_c_d(buf) #:nodoc:
|
||||
buf = get_e_o_c_d(buf)
|
||||
@number_of_this_disk = Entry.read_zip_short(buf)
|
||||
@number_of_disk_with_start_of_cdir = Entry.read_zip_short(buf)
|
||||
@total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_short(buf)
|
||||
@size = Entry.read_zip_short(buf)
|
||||
@size_in_bytes = Entry.read_zip_long(buf)
|
||||
@cdir_offset = Entry.read_zip_long(buf)
|
||||
comment_length = Entry.read_zip_short(buf)
|
||||
@comment = if comment_length.to_i <= 0
|
||||
buf.slice!(0, buf.size)
|
||||
else
|
||||
buffer.slice(
|
||||
ZIP64_STATIC_EOCD_SIZE,
|
||||
zip64_extensible_data_size
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def unpack_64_eocd_locator(buffer) # :nodoc:
|
||||
_, # ZIP64_EOCD_LOCATOR_SIG. We know we have this at this point.
|
||||
_, zip64_eocd_offset, = buffer.unpack('VVQ<V')
|
||||
|
||||
zip64_eocd_offset
|
||||
end
|
||||
|
||||
def unpack_e_o_c_d(buffer) # :nodoc:
|
||||
_, # END_OF_CD_SIG. We know we have this at this point.
|
||||
num_disk,
|
||||
num_disk_cdir,
|
||||
num_cdir_disk,
|
||||
num_entries,
|
||||
size_in_bytes,
|
||||
cdir_offset,
|
||||
comment_length = buffer.unpack('VvvvvVVv')
|
||||
|
||||
@number_of_this_disk = num_disk unless num_disk == 0xFFFF
|
||||
@number_of_disk_with_start_of_cdir = num_disk_cdir unless num_disk_cdir == 0xFFFF
|
||||
@total_number_of_entries_in_cdir_on_this_disk = num_cdir_disk unless num_cdir_disk == 0xFFFF
|
||||
@size = num_entries unless num_entries == 0xFFFF
|
||||
@size_in_bytes = size_in_bytes unless size_in_bytes == 0xFFFFFFFF
|
||||
@cdir_offset = cdir_offset unless cdir_offset == 0xFFFFFFFF
|
||||
|
||||
@comment = if comment_length.positive?
|
||||
buffer.slice(STATIC_EOCD_SIZE, comment_length)
|
||||
else
|
||||
''
|
||||
buf.read(comment_length)
|
||||
end
|
||||
raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
|
||||
end
|
||||
|
||||
def read_central_directory_entries(io) #:nodoc:
|
||||
# `StringIO` doesn't raise `EINVAL` if you seek beyond the current end,
|
||||
# so we need to catch that *and* query `io#eof?` here.
|
||||
eof = false
|
||||
begin
|
||||
io.seek(@cdir_offset, IO::SEEK_SET)
|
||||
rescue Errno::EINVAL
|
||||
eof = true
|
||||
raise Error, 'Zip consistency problem while reading central directory entry'
|
||||
end
|
||||
raise Error, 'Zip consistency problem while reading central directory entry' if eof || io.eof?
|
||||
|
||||
@entry_set = EntrySet.new
|
||||
@size.times do
|
||||
entry = Entry.read_c_dir_entry(io)
|
||||
next unless entry
|
||||
@entry_set << Entry.read_c_dir_entry(io)
|
||||
end
|
||||
end
|
||||
|
||||
offset = if entry.zip64?
|
||||
entry.extra['Zip64'].relative_header_offset
|
||||
def read_from_stream(io) #:nodoc:
|
||||
buf = start_buf(io)
|
||||
if zip64_file?(buf)
|
||||
read_64_e_o_c_d(buf)
|
||||
else
|
||||
entry.local_header_offset
|
||||
read_e_o_c_d(buf)
|
||||
end
|
||||
read_central_directory_entries(io)
|
||||
end
|
||||
|
||||
unless offset.nil?
|
||||
io_save = io.tell
|
||||
io.seek(offset, IO::SEEK_SET)
|
||||
entry.read_extra_field(read_local_extra_field(io), local: true)
|
||||
io.seek(io_save, IO::SEEK_SET)
|
||||
def get_e_o_c_d(buf) #:nodoc:
|
||||
sig_index = buf.rindex([END_OF_CDS].pack('V'))
|
||||
raise Error, 'Zip end of central directory signature not found' unless sig_index
|
||||
|
||||
buf = buf.slice!((sig_index + 4)..(buf.bytesize))
|
||||
|
||||
def buf.read(count)
|
||||
slice!(0, count)
|
||||
end
|
||||
|
||||
@entry_set << entry
|
||||
end
|
||||
buf
|
||||
end
|
||||
|
||||
def read_local_extra_field(io)
|
||||
buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
|
||||
return '' unless buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
|
||||
|
||||
head, _, _, _, _, _, _, _, _, _, n_len, e_len = buf.unpack('VCCvvvvVVVvv')
|
||||
return '' unless head == ::Zip::LOCAL_ENTRY_SIGNATURE
|
||||
|
||||
io.seek(n_len, IO::SEEK_CUR) # Skip over the entry name.
|
||||
io.read(e_len)
|
||||
def zip64_file?(buf)
|
||||
buf.rindex([ZIP64_END_OF_CDS].pack('V')) && buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
|
||||
end
|
||||
|
||||
def read_eocds(io) # :nodoc:
|
||||
base_location, data = eocd_data(io)
|
||||
|
||||
eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
|
||||
raise Error, 'Zip end of central directory signature not found' unless eocd_location
|
||||
|
||||
zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'))
|
||||
|
||||
if zip64_eocd_locator
|
||||
zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'))
|
||||
|
||||
zip64_eocd_data =
|
||||
if zip64_eocd_location
|
||||
data.slice(zip64_eocd_location..zip64_eocd_locator)
|
||||
else
|
||||
zip64_eocd_location = unpack_64_eocd_locator(
|
||||
data.slice(zip64_eocd_locator..eocd_location)
|
||||
)
|
||||
unless zip64_eocd_location
|
||||
raise Error, 'Zip64 end of central directory signature not found'
|
||||
end
|
||||
|
||||
io.seek(zip64_eocd_location, IO::SEEK_SET)
|
||||
io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
|
||||
end
|
||||
|
||||
unpack_64_e_o_c_d(zip64_eocd_data)
|
||||
end
|
||||
|
||||
unpack_e_o_c_d(data.slice(eocd_location..-1))
|
||||
end
|
||||
|
||||
def eocd_data(io)
|
||||
def start_buf(io)
|
||||
begin
|
||||
io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
|
||||
io.seek(-MAX_END_OF_CDS_SIZE, IO::SEEK_END)
|
||||
rescue Errno::EINVAL
|
||||
io.seek(0, IO::SEEK_SET)
|
||||
end
|
||||
io.read
|
||||
end
|
||||
|
||||
[io.tell, io.read]
|
||||
def get_64_e_o_c_d(buf) #:nodoc:
|
||||
zip_64_start = buf.rindex([ZIP64_END_OF_CDS].pack('V'))
|
||||
raise Error, 'Zip64 end of central directory signature not found' unless zip_64_start
|
||||
|
||||
zip_64_locator = buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
|
||||
raise Error, 'Zip64 end of central directory signature locator not found' unless zip_64_locator
|
||||
|
||||
buf = buf.slice!((zip_64_start + 4)..zip_64_locator)
|
||||
|
||||
def buf.read(count)
|
||||
slice!(0, count)
|
||||
end
|
||||
|
||||
buf
|
||||
end
|
||||
|
||||
# For iterating over the entries.
|
||||
def each(&a_proc)
|
||||
@entry_set.each(&a_proc)
|
||||
end
|
||||
|
||||
# Returns the number of entries in the central directory (and
|
||||
# consequently in the zip archive).
|
||||
def size
|
||||
@entry_set.size
|
||||
end
|
||||
|
||||
def self.read_from_stream(io) #:nodoc:
|
||||
cdir = new
|
||||
cdir.read_from_stream(io)
|
||||
cdir
|
||||
rescue Error
|
||||
nil
|
||||
end
|
||||
|
||||
def ==(other) #:nodoc:
|
||||
return false unless other.kind_of?(CentralDirectory)
|
||||
|
||||
@entry_set.entries.sort == other.entries.sort && comment == other.comment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class Compressor #:nodoc:all
|
||||
def finish; end
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
# :stopdoc:
|
||||
|
||||
RUNNING_ON_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/i
|
||||
|
||||
CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
|
||||
|
@ -15,8 +11,6 @@ module Zip
|
|||
VERSION_NEEDED_TO_EXTRACT = 20
|
||||
VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45
|
||||
|
||||
SPLIT_FILE_SIGNATURE = 0x08074b50
|
||||
|
||||
FILE_TYPE_FILE = 0o10
|
||||
FILE_TYPE_DIR = 0o04
|
||||
FILE_TYPE_SYMLINK = 0o12
|
||||
|
@ -44,27 +38,27 @@ module Zip
|
|||
FSTYPE_ATHEOS = 30
|
||||
|
||||
FSTYPES = {
|
||||
FSTYPE_FAT => 'FAT',
|
||||
FSTYPE_AMIGA => 'Amiga',
|
||||
FSTYPE_VMS => 'VMS (Vax or Alpha AXP)',
|
||||
FSTYPE_UNIX => 'Unix',
|
||||
FSTYPE_VM_CMS => 'VM/CMS',
|
||||
FSTYPE_ATARI => 'Atari ST',
|
||||
FSTYPE_HPFS => 'OS/2 or NT HPFS',
|
||||
FSTYPE_MAC => 'Macintosh',
|
||||
FSTYPE_Z_SYSTEM => 'Z-System',
|
||||
FSTYPE_CPM => 'CP/M',
|
||||
FSTYPE_TOPS20 => 'TOPS-20',
|
||||
FSTYPE_NTFS => 'NTFS',
|
||||
FSTYPE_QDOS => 'SMS/QDOS',
|
||||
FSTYPE_ACORN => 'Acorn RISC OS',
|
||||
FSTYPE_VFAT => 'Win32 VFAT',
|
||||
FSTYPE_MVS => 'MVS',
|
||||
FSTYPE_BEOS => 'BeOS',
|
||||
FSTYPE_TANDEM => 'Tandem NSK',
|
||||
FSTYPE_THEOS => 'Theos',
|
||||
FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)',
|
||||
FSTYPE_ATHEOS => 'AtheOS'
|
||||
FSTYPE_FAT => 'FAT'.freeze,
|
||||
FSTYPE_AMIGA => 'Amiga'.freeze,
|
||||
FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze,
|
||||
FSTYPE_UNIX => 'Unix'.freeze,
|
||||
FSTYPE_VM_CMS => 'VM/CMS'.freeze,
|
||||
FSTYPE_ATARI => 'Atari ST'.freeze,
|
||||
FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze,
|
||||
FSTYPE_MAC => 'Macintosh'.freeze,
|
||||
FSTYPE_Z_SYSTEM => 'Z-System'.freeze,
|
||||
FSTYPE_CPM => 'CP/M'.freeze,
|
||||
FSTYPE_TOPS20 => 'TOPS-20'.freeze,
|
||||
FSTYPE_NTFS => 'NTFS'.freeze,
|
||||
FSTYPE_QDOS => 'SMS/QDOS'.freeze,
|
||||
FSTYPE_ACORN => 'Acorn RISC OS'.freeze,
|
||||
FSTYPE_VFAT => 'Win32 VFAT'.freeze,
|
||||
FSTYPE_MVS => 'MVS'.freeze,
|
||||
FSTYPE_BEOS => 'BeOS'.freeze,
|
||||
FSTYPE_TANDEM => 'Tandem NSK'.freeze,
|
||||
FSTYPE_THEOS => 'Theos'.freeze,
|
||||
FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze,
|
||||
FSTYPE_ATHEOS => 'AtheOS'.freeze
|
||||
}.freeze
|
||||
|
||||
COMPRESSION_METHOD_STORE = 0
|
||||
|
@ -118,6 +112,4 @@ module Zip
|
|||
COMPRESSION_METHOD_PPMD => 'PPMd version I, Rev 1',
|
||||
COMPRESSION_METHOD_AES => 'AES encryption'
|
||||
}.freeze
|
||||
|
||||
# :startdoc:
|
||||
end
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class DecryptedIo #:nodoc:all
|
||||
CHUNK_SIZE = 32_768
|
||||
|
@ -18,7 +16,7 @@ module Zip
|
|||
buffer << produce_input
|
||||
end
|
||||
|
||||
outbuf.replace(buffer.slice!(0...(length || buffer.bytesize)))
|
||||
outbuf.replace(buffer.slice!(0...(length || output_buffer.bytesize)))
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class Encrypter #:nodoc:all
|
||||
end
|
||||
|
||||
class Decrypter # :nodoc:all
|
||||
class Decrypter
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
module NullEncryption # :nodoc:
|
||||
module NullEncryption
|
||||
def header_bytesize
|
||||
0
|
||||
end
|
||||
|
@ -11,7 +9,7 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
class NullEncrypter < Encrypter # :nodoc:
|
||||
class NullEncrypter < Encrypter
|
||||
include NullEncryption
|
||||
|
||||
def header(_mtime)
|
||||
|
@ -22,14 +20,14 @@ module Zip
|
|||
data
|
||||
end
|
||||
|
||||
def data_descriptor(_crc32, _compressed_size, _uncompressed_size)
|
||||
def data_descriptor(_crc32, _compressed_size, _uncomprssed_size)
|
||||
''
|
||||
end
|
||||
|
||||
def reset!; end
|
||||
end
|
||||
|
||||
class NullDecrypter < Decrypter # :nodoc:
|
||||
class NullDecrypter < Decrypter
|
||||
include NullEncryption
|
||||
|
||||
def decrypt(data)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
module TraditionalEncryption # :nodoc:
|
||||
module TraditionalEncryption
|
||||
def initialize(password)
|
||||
@password = password
|
||||
reset_keys!
|
||||
|
@ -28,7 +26,7 @@ module Zip
|
|||
|
||||
def update_keys(num)
|
||||
@key0 = ~Zlib.crc32(num, ~@key0)
|
||||
@key1 = (((@key1 + (@key0 & 0xff)) * 134_775_813) + 1) & 0xffffffff
|
||||
@key1 = ((@key1 + (@key0 & 0xff)) * 134_775_813 + 1) & 0xffffffff
|
||||
@key2 = ~Zlib.crc32((@key1 >> 24).chr, ~@key2)
|
||||
end
|
||||
|
||||
|
@ -38,7 +36,7 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
class TraditionalEncrypter < Encrypter # :nodoc:
|
||||
class TraditionalEncrypter < Encrypter
|
||||
include TraditionalEncryption
|
||||
|
||||
def header(mtime)
|
||||
|
@ -55,8 +53,8 @@ module Zip
|
|||
data.unpack('C*').map { |x| encode x }.pack('C*')
|
||||
end
|
||||
|
||||
def data_descriptor(crc32, compressed_size, uncompressed_size)
|
||||
[0x08074b50, crc32, compressed_size, uncompressed_size].pack('VVVV')
|
||||
def data_descriptor(crc32, compressed_size, uncomprssed_size)
|
||||
[0x08074b50, crc32, compressed_size, uncomprssed_size].pack('VVVV')
|
||||
end
|
||||
|
||||
def reset!
|
||||
|
@ -72,7 +70,7 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
class TraditionalDecrypter < Decrypter # :nodoc:
|
||||
class TraditionalDecrypter < Decrypter
|
||||
include TraditionalEncryption
|
||||
|
||||
def decrypt(data)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class Decompressor #:nodoc:all
|
||||
CHUNK_SIZE = 32_768
|
||||
|
@ -16,7 +14,8 @@ module Zip
|
|||
decompressor_classes[compression_method]
|
||||
end
|
||||
|
||||
attr_reader :decompressed_size, :input_stream
|
||||
attr_reader :input_stream
|
||||
attr_reader :decompressed_size
|
||||
|
||||
def initialize(input_stream, decompressed_size = nil)
|
||||
super()
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class Deflater < Compressor #:nodoc:all
|
||||
def initialize(output_stream, level = Zip.default_compression, encrypter = NullEncrypter.new)
|
||||
|
@ -15,16 +13,16 @@ module Zip
|
|||
val = data.to_s
|
||||
@crc = Zlib.crc32(val, @crc)
|
||||
@size += val.bytesize
|
||||
buffer = @zlib_deflater.deflate(data, Zlib::SYNC_FLUSH)
|
||||
return @output_stream if buffer.empty?
|
||||
|
||||
buffer = @zlib_deflater.deflate(data)
|
||||
if buffer.empty?
|
||||
@output_stream
|
||||
else
|
||||
@output_stream << @encrypter.encrypt(buffer)
|
||||
end
|
||||
end
|
||||
|
||||
def finish
|
||||
buffer = @zlib_deflater.finish
|
||||
@output_stream << @encrypter.encrypt(buffer) unless buffer.empty?
|
||||
@zlib_deflater.close
|
||||
@output_stream << @encrypter.encrypt(@zlib_deflater.finish) until @zlib_deflater.finished?
|
||||
end
|
||||
|
||||
attr_reader :size, :crc
|
||||
|
|
|
@ -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
|
|
@ -1,7 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rubygems'
|
||||
|
||||
module Zip
|
||||
class DOSTime < Time #:nodoc:all
|
||||
# MS-DOS File Date and Time format as used in Interrupt 21H Function 57H:
|
||||
|
@ -16,14 +12,6 @@ module Zip
|
|||
# bits 5-8 month (1-12)
|
||||
# bits 9-15 year (four digit year minus 1980)
|
||||
|
||||
attr_writer :absolute_time # :nodoc:
|
||||
|
||||
def absolute_time?
|
||||
# If absolute time is not set, we can assume it is an absolute time
|
||||
# because times do have timezone information by default.
|
||||
@absolute_time.nil? ? true : @absolute_time
|
||||
end
|
||||
|
||||
def to_binary_dos_time
|
||||
(sec / 2) +
|
||||
(min << 5) +
|
||||
|
@ -37,7 +25,8 @@ module Zip
|
|||
end
|
||||
|
||||
def dos_equals(other)
|
||||
warn 'Zip::DOSTime#dos_equals is deprecated. Use `==` instead.'
|
||||
Zip.warn_about_v3_api('DOSTime#dos_equals')
|
||||
|
||||
self == other
|
||||
end
|
||||
|
||||
|
@ -61,35 +50,7 @@ module Zip
|
|||
month = (0b111100000 & bin_dos_date) >> 5
|
||||
year = ((0b1111111000000000 & bin_dos_date) >> 9) + 1980
|
||||
|
||||
time = local(year, month, day, hour, minute, second)
|
||||
time.absolute_time = false
|
||||
time
|
||||
end
|
||||
|
||||
if defined? JRUBY_VERSION && Gem::Version.new(JRUBY_VERSION) < '9.2.18.0'
|
||||
module JRubyCMP # :nodoc:
|
||||
def ==(other)
|
||||
(self <=> other).zero?
|
||||
end
|
||||
|
||||
def <(other)
|
||||
(self <=> other).negative?
|
||||
end
|
||||
|
||||
def <=(other)
|
||||
(self <=> other) <= 0
|
||||
end
|
||||
|
||||
def >(other)
|
||||
(self <=> other).positive?
|
||||
end
|
||||
|
||||
def >=(other)
|
||||
(self <=> other) >= 0
|
||||
end
|
||||
end
|
||||
|
||||
include JRubyCMP
|
||||
local(year, month, day, hour, minute, second)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
570
lib/zip/entry.rb
570
lib/zip/entry.rb
|
@ -1,44 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'pathname'
|
||||
|
||||
require_relative 'dirtyable'
|
||||
|
||||
module Zip
|
||||
# Zip::Entry represents an entry in a Zip archive.
|
||||
class Entry
|
||||
include Dirtyable
|
||||
|
||||
# Constant used to specify that the entry is stored (i.e., not compressed).
|
||||
STORED = ::Zip::COMPRESSION_METHOD_STORE
|
||||
|
||||
# Constant used to specify that the entry is deflated (i.e., compressed).
|
||||
DEFLATED = ::Zip::COMPRESSION_METHOD_DEFLATE
|
||||
|
||||
STORED = 0
|
||||
DEFLATED = 8
|
||||
# Language encoding flag (EFS) bit
|
||||
EFS = 0b100000000000 # :nodoc:
|
||||
EFS = 0b100000000000
|
||||
|
||||
# Compression level flags (used as part of the gp flags).
|
||||
COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110 # :nodoc:
|
||||
COMPRESSION_LEVEL_FAST_GPFLAG = 0b100 # :nodoc:
|
||||
COMPRESSION_LEVEL_MAX_GPFLAG = 0b010 # :nodoc:
|
||||
attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
|
||||
:name, :size, :local_header_offset, :zipfile, :fstype, :external_file_attributes,
|
||||
:internal_file_attributes,
|
||||
:gp_flags, :header_signature, :follow_symlinks,
|
||||
:restore_times, :restore_permissions, :restore_ownership,
|
||||
:unix_uid, :unix_gid, :unix_perms,
|
||||
:dirty
|
||||
attr_reader :ftype, :filepath # :nodoc:
|
||||
|
||||
attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
|
||||
:restore_ownership, :restore_permissions, :restore_times,
|
||||
:unix_gid, :unix_perms, :unix_uid
|
||||
|
||||
attr_accessor :crc, :external_file_attributes, :fstype, :gp_flags,
|
||||
:internal_file_attributes, :local_header_offset # :nodoc:
|
||||
|
||||
attr_reader :extra, :compression_level, :filepath # :nodoc:
|
||||
|
||||
attr_writer :size # :nodoc:
|
||||
|
||||
mark_dirty :comment=, :compressed_size=, :external_file_attributes=,
|
||||
:fstype=, :gp_flags=, :name=, :size=,
|
||||
:unix_gid=, :unix_perms=, :unix_uid=
|
||||
|
||||
def set_default_vars_values # :nodoc:
|
||||
def set_default_vars_values
|
||||
@local_header_offset = 0
|
||||
@local_header_size = nil # not known until local entry is created or read
|
||||
@internal_file_attributes = 1
|
||||
|
@ -57,222 +34,175 @@ module Zip
|
|||
end
|
||||
@follow_symlinks = false
|
||||
|
||||
@restore_times = DEFAULT_RESTORE_OPTIONS[:restore_times]
|
||||
@restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
|
||||
@restore_ownership = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
|
||||
@restore_times = false
|
||||
@restore_permissions = false
|
||||
@restore_ownership = false
|
||||
# BUG: need an extra field to support uid/gid's
|
||||
@unix_uid = nil
|
||||
@unix_gid = nil
|
||||
@unix_perms = nil
|
||||
# @posix_acl = nil
|
||||
# @ntfs_acl = nil
|
||||
@dirty = false
|
||||
end
|
||||
|
||||
def check_name(name) # :nodoc:
|
||||
raise EntryNameError, name if name.start_with?('/')
|
||||
raise EntryNameError if name.length > 65_535
|
||||
def check_name(name)
|
||||
return unless name.start_with?('/')
|
||||
|
||||
raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
|
||||
end
|
||||
|
||||
# Create a new Zip::Entry.
|
||||
def initialize(
|
||||
zipfile = '', name = '',
|
||||
comment: '', size: nil, compressed_size: 0, crc: 0,
|
||||
compression_method: DEFLATED,
|
||||
compression_level: ::Zip.default_compression,
|
||||
time: ::Zip::DOSTime.now, extra: ::Zip::ExtraField.new
|
||||
)
|
||||
super()
|
||||
@name = name
|
||||
check_name(@name)
|
||||
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
def initialize(zipfile = nil, name = nil, *args)
|
||||
name ||= ''
|
||||
check_name(name)
|
||||
|
||||
set_default_vars_values
|
||||
@fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
|
||||
|
||||
@zipfile = zipfile
|
||||
@comment = comment || ''
|
||||
@compression_method = compression_method || DEFLATED
|
||||
@compression_level = compression_level || ::Zip.default_compression
|
||||
@compressed_size = compressed_size || 0
|
||||
@crc = crc || 0
|
||||
@size = size
|
||||
@time = case time
|
||||
when ::Zip::DOSTime
|
||||
time
|
||||
when Time
|
||||
::Zip::DOSTime.from_time(time)
|
||||
@zipfile = zipfile || ''
|
||||
@name = name
|
||||
|
||||
if (args_hash = args.first).kind_of?(::Hash)
|
||||
@comment = args_hash[:comment] || ''
|
||||
@extra = args_hash[:extra] || ''
|
||||
@compressed_size = args_hash[:compressed_size] || 0
|
||||
@crc = args_hash[:crc] || 0
|
||||
@compression_method = args_hash[:compression_method] || ::Zip::Entry::DEFLATED
|
||||
@size = args_hash[:size] || 0
|
||||
@time = args_hash[:time] || ::Zip::DOSTime.now
|
||||
else
|
||||
::Zip::DOSTime.now
|
||||
end
|
||||
@extra =
|
||||
extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
|
||||
Zip.warn_about_v3_api('Zip::Entry.new') unless args.empty?
|
||||
|
||||
set_compression_level_flags
|
||||
@comment = args[0] || ''
|
||||
@extra = args[1] || ''
|
||||
@compressed_size = args[2] || 0
|
||||
@crc = args[3] || 0
|
||||
@compression_method = args[4] || ::Zip::Entry::DEFLATED
|
||||
@size = args[5] || 0
|
||||
@time = args[6] || ::Zip::DOSTime.now
|
||||
end
|
||||
|
||||
# Is this entry encrypted?
|
||||
@ftype = name_is_directory? ? :directory : :file
|
||||
@extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.kind_of?(::Zip::ExtraField)
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
|
||||
def encrypted?
|
||||
gp_flags & 1 == 1
|
||||
end
|
||||
|
||||
def incomplete? # :nodoc:
|
||||
(gp_flags & 8 == 8) && (crc == 0 || size == 0 || compressed_size == 0)
|
||||
def incomplete?
|
||||
gp_flags & 8 == 8
|
||||
end
|
||||
|
||||
# The uncompressed size of the entry.
|
||||
def size
|
||||
@size || 0
|
||||
end
|
||||
|
||||
# Get a timestamp component of this entry.
|
||||
#
|
||||
# Returns modification time by default.
|
||||
def time(component: :mtime)
|
||||
time =
|
||||
def time
|
||||
if @extra['UniversalTime']
|
||||
@extra['UniversalTime'].send(component)
|
||||
@extra['UniversalTime'].mtime
|
||||
elsif @extra['NTFS']
|
||||
@extra['NTFS'].send(component)
|
||||
end
|
||||
|
||||
@extra['NTFS'].mtime
|
||||
else
|
||||
# Standard time field in central directory has local time
|
||||
# under archive creator. Then, we can't get timezone.
|
||||
time || (@time if component == :mtime)
|
||||
@time
|
||||
end
|
||||
end
|
||||
|
||||
alias mtime time
|
||||
|
||||
# Get the last access time of this entry, if available.
|
||||
def atime
|
||||
time(component: :atime)
|
||||
end
|
||||
|
||||
# Get the creation time of this entry, if available.
|
||||
def ctime
|
||||
time(component: :ctime)
|
||||
end
|
||||
|
||||
# Set a timestamp component of this entry.
|
||||
#
|
||||
# Sets modification time by default.
|
||||
def time=(value, component: :mtime)
|
||||
@dirty = true
|
||||
def time=(value)
|
||||
unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
|
||||
@extra.create('UniversalTime')
|
||||
end
|
||||
|
||||
value = DOSTime.from_time(value)
|
||||
comp = "#{component}=" unless component.to_s.end_with?('=')
|
||||
(@extra['UniversalTime'] || @extra['NTFS']).send(comp, value)
|
||||
@time = value if component == :mtime
|
||||
(@extra['UniversalTime'] || @extra['NTFS']).mtime = value
|
||||
@time = value
|
||||
end
|
||||
|
||||
alias mtime= time=
|
||||
def file_type_is?(type)
|
||||
raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
|
||||
|
||||
# Set the last access time of this entry.
|
||||
def atime=(value)
|
||||
send(:time=, value, component: :atime)
|
||||
end
|
||||
|
||||
# Set the creation time of this entry.
|
||||
def ctime=(value)
|
||||
send(:time=, value, component: :ctime)
|
||||
end
|
||||
|
||||
# Does this entry return time fields with accurate timezone information?
|
||||
def absolute_time?
|
||||
@extra.member?('UniversalTime') || @extra.member?('NTFS')
|
||||
end
|
||||
|
||||
# Return the compression method for this entry.
|
||||
#
|
||||
# Returns STORED if the entry is a directory or if the compression
|
||||
# level is 0.
|
||||
def compression_method
|
||||
return STORED if ftype == :directory || @compression_level == 0
|
||||
|
||||
@compression_method
|
||||
end
|
||||
|
||||
# Set the compression method for this entry.
|
||||
def compression_method=(method)
|
||||
@dirty = true
|
||||
@compression_method = (ftype == :directory ? STORED : method)
|
||||
end
|
||||
|
||||
# Does this entry use the ZIP64 extensions?
|
||||
def zip64?
|
||||
!@extra['Zip64'].nil?
|
||||
end
|
||||
|
||||
def file_type_is?(type) # :nodoc:
|
||||
ftype == type
|
||||
end
|
||||
|
||||
def ftype # :nodoc:
|
||||
@ftype ||= name_is_directory? ? :directory : :file
|
||||
@ftype == type
|
||||
end
|
||||
|
||||
# Dynamic checkers
|
||||
%w[directory file symlink].each do |k|
|
||||
define_method :"#{k}?" do
|
||||
define_method "#{k}?" do
|
||||
file_type_is?(k.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
def name_is_directory? # :nodoc:
|
||||
def name_is_directory? #:nodoc:all
|
||||
@name.end_with?('/')
|
||||
end
|
||||
|
||||
# Is the name a relative path, free of `..` patterns that could lead to
|
||||
# path traversal attacks? This does NOT handle symlinks; if the path
|
||||
# contains symlinks, this check is NOT enough to guarantee safety.
|
||||
def name_safe? # :nodoc:
|
||||
def name_safe?
|
||||
cleanpath = Pathname.new(@name).cleanpath
|
||||
return false unless cleanpath.relative?
|
||||
|
||||
root = ::File::SEPARATOR
|
||||
naive = ::File.join(root, cleanpath.to_s)
|
||||
# Allow for Windows drive mappings at the root.
|
||||
::File.absolute_path(cleanpath.to_s, root).match?(/([A-Z]:)?#{naive}/i)
|
||||
naive_expanded_path = ::File.join(root, cleanpath.to_s)
|
||||
::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
|
||||
end
|
||||
|
||||
def local_entry_offset # :nodoc:
|
||||
def local_entry_offset #:nodoc:all
|
||||
local_header_offset + @local_header_size
|
||||
end
|
||||
|
||||
def name_size # :nodoc:
|
||||
def name_size
|
||||
@name ? @name.bytesize : 0
|
||||
end
|
||||
|
||||
def extra_size # :nodoc:
|
||||
def extra_size
|
||||
@extra ? @extra.local_size : 0
|
||||
end
|
||||
|
||||
def comment_size # :nodoc:
|
||||
def comment_size
|
||||
@comment ? @comment.bytesize : 0
|
||||
end
|
||||
|
||||
def calculate_local_header_size # :nodoc:
|
||||
def calculate_local_header_size #:nodoc:all
|
||||
LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
|
||||
end
|
||||
|
||||
# check before rewriting an entry (after file sizes are known)
|
||||
# that we didn't change the header size (and thus clobber file data or something)
|
||||
def verify_local_header_size! # :nodoc:
|
||||
def verify_local_header_size!
|
||||
return if @local_header_size.nil?
|
||||
|
||||
new_size = calculate_local_header_size
|
||||
return unless @local_header_size != new_size
|
||||
|
||||
raise Error,
|
||||
"Local header size changed (#{@local_header_size} -> #{new_size})"
|
||||
raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
|
||||
end
|
||||
|
||||
def cdir_header_size # :nodoc:
|
||||
def cdir_header_size #:nodoc:all
|
||||
CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
|
||||
(@extra ? @extra.c_dir_size : 0) + comment_size
|
||||
end
|
||||
|
||||
def next_header_offset # :nodoc:
|
||||
local_entry_offset + compressed_size
|
||||
def next_header_offset #:nodoc:all
|
||||
local_entry_offset + compressed_size + data_descriptor_size
|
||||
end
|
||||
|
||||
# Extracts entry to file dest_path (defaults to @name).
|
||||
# NB: The caller is responsible for making sure dest_path is safe, if it
|
||||
# is passed.
|
||||
def extract(dest_path = nil, &block)
|
||||
Zip.warn_about_v3_api('Zip::Entry#extract')
|
||||
|
||||
if dest_path.nil? && !name_safe?
|
||||
warn "WARNING: skipped '#{@name}' as unsafe."
|
||||
return self
|
||||
end
|
||||
|
||||
dest_path ||= @name
|
||||
block ||= proc { ::Zip.on_exists_proc }
|
||||
|
||||
raise "unknown file type #{inspect}" unless directory? || file? || symlink?
|
||||
|
||||
__send__("create_#{@ftype}", dest_path, &block)
|
||||
self
|
||||
end
|
||||
|
||||
# Extracts this entry to a file at `entry_path`, with
|
||||
|
@ -280,7 +210,7 @@ module Zip
|
|||
#
|
||||
# NB: The caller is responsible for making sure `destination_directory` is
|
||||
# safe, if it is passed.
|
||||
def extract(entry_path = @name, destination_directory: '.', &block)
|
||||
def extract_v3(entry_path = @name, destination_directory: '.', &block)
|
||||
dest_dir = ::File.absolute_path(destination_directory || '.')
|
||||
extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
|
||||
|
||||
|
@ -297,12 +227,24 @@ module Zip
|
|||
self
|
||||
end
|
||||
|
||||
def to_s # :nodoc:
|
||||
def to_s
|
||||
@name
|
||||
end
|
||||
|
||||
class << self
|
||||
def read_c_dir_entry(io) # :nodoc:
|
||||
def read_zip_short(io) # :nodoc:
|
||||
io.read(2).unpack1('v')
|
||||
end
|
||||
|
||||
def read_zip_long(io) # :nodoc:
|
||||
io.read(4).unpack1('V')
|
||||
end
|
||||
|
||||
def read_zip_64_long(io) # :nodoc:
|
||||
io.read(8).unpack1('Q<')
|
||||
end
|
||||
|
||||
def read_c_dir_entry(io) #:nodoc:all
|
||||
path = if io.respond_to?(:path)
|
||||
io.path
|
||||
else
|
||||
|
@ -315,18 +257,16 @@ module Zip
|
|||
nil
|
||||
end
|
||||
|
||||
def read_local_entry(io) # :nodoc:
|
||||
def read_local_entry(io)
|
||||
entry = new(io)
|
||||
entry.read_local_entry(io)
|
||||
entry
|
||||
rescue SplitArchiveError
|
||||
raise
|
||||
rescue Error
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def unpack_local_entry(buf) # :nodoc:
|
||||
def unpack_local_entry(buf)
|
||||
@header_signature,
|
||||
@version,
|
||||
@fstype,
|
||||
|
@ -341,66 +281,62 @@ module Zip
|
|||
@extra_length = buf.unpack('VCCvvvvVVVvv')
|
||||
end
|
||||
|
||||
def read_local_entry(io) # :nodoc:
|
||||
@dirty = false # No changes at this point.
|
||||
current_offset = io.tell
|
||||
def read_local_entry(io) #:nodoc:all
|
||||
@local_header_offset = io.tell
|
||||
|
||||
read_local_header_fields(io)
|
||||
static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
|
||||
|
||||
if @header_signature == SPLIT_FILE_SIGNATURE
|
||||
raise SplitArchiveError if current_offset.zero?
|
||||
|
||||
# Rewind, skipping the data descriptor, then try to read the local header again.
|
||||
current_offset += 16
|
||||
io.seek(current_offset)
|
||||
read_local_header_fields(io)
|
||||
unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
|
||||
raise Error, 'Premature end of file. Not enough data for zip entry local header'
|
||||
end
|
||||
|
||||
unless @header_signature == LOCAL_ENTRY_SIGNATURE
|
||||
raise Error, "Zip local header magic not found at location '#{current_offset}'"
|
||||
end
|
||||
unpack_local_entry(static_sized_fields_buf)
|
||||
|
||||
@local_header_offset = current_offset
|
||||
unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
|
||||
raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
|
||||
end
|
||||
|
||||
set_time(@last_mod_date, @last_mod_time)
|
||||
|
||||
@name = io.read(@name_length)
|
||||
extra = io.read(@extra_length)
|
||||
|
||||
@name.tr!('\\', '/')
|
||||
if ::Zip.force_entry_names_encoding
|
||||
@name.force_encoding(::Zip.force_entry_names_encoding)
|
||||
end
|
||||
@name.tr!('\\', '/') # Normalise filepath separators after encoding set.
|
||||
|
||||
# We need to do this here because `initialize` has so many side-effects.
|
||||
# :-(
|
||||
@ftype = name_is_directory? ? :directory : :file
|
||||
|
||||
extra = io.read(@extra_length)
|
||||
if extra && extra.bytesize != @extra_length
|
||||
raise ::Zip::Error, 'Truncated local zip entry header'
|
||||
end
|
||||
|
||||
read_extra_field(extra, local: true)
|
||||
if @extra.kind_of?(::Zip::ExtraField)
|
||||
@extra.merge(extra) if extra
|
||||
else
|
||||
@extra = ::Zip::ExtraField.new(extra)
|
||||
end
|
||||
|
||||
parse_zip64_extra(true)
|
||||
@local_header_size = calculate_local_header_size
|
||||
end
|
||||
|
||||
def pack_local_entry # :nodoc:
|
||||
def pack_local_entry
|
||||
zip64 = @extra['Zip64']
|
||||
[::Zip::LOCAL_ENTRY_SIGNATURE,
|
||||
@version_needed_to_extract, # version needed to extract
|
||||
@gp_flags, # @gp_flags
|
||||
compression_method,
|
||||
@compression_method,
|
||||
@time.to_binary_dos_time, # @last_mod_time
|
||||
@time.to_binary_dos_date, # @last_mod_date
|
||||
@crc,
|
||||
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
||||
zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
|
||||
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
||||
name_size,
|
||||
@extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
|
||||
end
|
||||
|
||||
def write_local_entry(io, rewrite: false) # :nodoc:
|
||||
prep_local_zip64_extra
|
||||
def write_local_entry(io, rewrite = false) #:nodoc:all
|
||||
prep_zip64_extra(true)
|
||||
verify_local_header_size! if rewrite
|
||||
@local_header_offset = io.tell
|
||||
|
||||
|
@ -411,7 +347,7 @@ module Zip
|
|||
@local_header_size = io.tell - @local_header_offset
|
||||
end
|
||||
|
||||
def unpack_c_dir_entry(buf) # :nodoc:
|
||||
def unpack_c_dir_entry(buf)
|
||||
@header_signature,
|
||||
@version, # version of encoding software
|
||||
@fstype, # filesystem type
|
||||
|
@ -435,7 +371,7 @@ module Zip
|
|||
@comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
|
||||
end
|
||||
|
||||
def set_ftype_from_c_dir_entry # :nodoc:
|
||||
def set_ftype_from_c_dir_entry
|
||||
@ftype = case @fstype
|
||||
when ::Zip::FSTYPE_UNIX
|
||||
@unix_perms = (@external_file_attributes >> 16) & 0o7777
|
||||
|
@ -447,9 +383,8 @@ module Zip
|
|||
when ::Zip::FILE_TYPE_SYMLINK
|
||||
:symlink
|
||||
else
|
||||
# Best case guess for whether it is a file or not.
|
||||
# Otherwise this would be set to unknown and that
|
||||
# entry would never be able to be extracted.
|
||||
# best case guess for whether it is a file or not
|
||||
# Otherwise this would be set to unknown and that entry would never be able to extracted
|
||||
if name_is_directory?
|
||||
:directory
|
||||
else
|
||||
|
@ -465,47 +400,43 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
def check_c_dir_entry_static_header_length(buf) # :nodoc:
|
||||
return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
|
||||
def check_c_dir_entry_static_header_length(buf)
|
||||
return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
|
||||
|
||||
raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
|
||||
end
|
||||
|
||||
def check_c_dir_entry_signature # :nodoc:
|
||||
return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
|
||||
def check_c_dir_entry_signature
|
||||
return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
|
||||
|
||||
raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
|
||||
end
|
||||
|
||||
def check_c_dir_entry_comment_size # :nodoc:
|
||||
def check_c_dir_entry_comment_size
|
||||
return if @comment && @comment.bytesize == @comment_length
|
||||
|
||||
raise ::Zip::Error, 'Truncated cdir zip entry header'
|
||||
end
|
||||
|
||||
def read_extra_field(buf, local: false) # :nodoc:
|
||||
def read_c_dir_extra_field(io)
|
||||
if @extra.kind_of?(::Zip::ExtraField)
|
||||
@extra.merge(buf, local: local) if buf
|
||||
@extra.merge(io.read(@extra_length))
|
||||
else
|
||||
@extra = ::Zip::ExtraField.new(buf, local: local)
|
||||
@extra = ::Zip::ExtraField.new(io.read(@extra_length))
|
||||
end
|
||||
end
|
||||
|
||||
def read_c_dir_entry(io) # :nodoc:
|
||||
@dirty = false # No changes at this point.
|
||||
def read_c_dir_entry(io) #:nodoc:all
|
||||
static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
|
||||
check_c_dir_entry_static_header_length(static_sized_fields_buf)
|
||||
unpack_c_dir_entry(static_sized_fields_buf)
|
||||
check_c_dir_entry_signature
|
||||
set_time(@last_mod_date, @last_mod_time)
|
||||
|
||||
@name = io.read(@name_length)
|
||||
if ::Zip.force_entry_names_encoding
|
||||
@name.force_encoding(::Zip.force_entry_names_encoding)
|
||||
end
|
||||
@name.tr!('\\', '/') # Normalise filepath separators after encoding set.
|
||||
|
||||
read_extra_field(io.read(@extra_length))
|
||||
read_c_dir_extra_field(io)
|
||||
@comment = io.read(@comment_length)
|
||||
check_c_dir_entry_comment_size
|
||||
set_ftype_from_c_dir_entry
|
||||
|
@ -521,27 +452,27 @@ module Zip
|
|||
end
|
||||
|
||||
def get_extra_attributes_from_path(path) # :nodoc:
|
||||
stat = file_stat(path)
|
||||
@time = DOSTime.from_time(stat.mtime)
|
||||
return if ::Zip::RUNNING_ON_WINDOWS
|
||||
return if Zip::RUNNING_ON_WINDOWS
|
||||
|
||||
stat = file_stat(path)
|
||||
@unix_uid = stat.uid
|
||||
@unix_gid = stat.gid
|
||||
@unix_perms = stat.mode & 0o7777
|
||||
@time = ::Zip::DOSTime.from_time(stat.mtime)
|
||||
end
|
||||
|
||||
# rubocop:disable Style/GuardClause
|
||||
def set_unix_attributes_on_path(dest_path) # :nodoc:
|
||||
# Ignore setuid/setgid bits by default. Honour if @restore_ownership.
|
||||
unix_perms_mask = (@restore_ownership ? 0o7777 : 0o1777)
|
||||
if @restore_permissions && @unix_perms
|
||||
::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path)
|
||||
def set_unix_attributes_on_path(dest_path)
|
||||
# ignore setuid/setgid bits by default. honor if @restore_ownership
|
||||
unix_perms_mask = 0o1777
|
||||
unix_perms_mask = 0o7777 if @restore_ownership
|
||||
::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
|
||||
::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
|
||||
|
||||
# Restore the timestamp on a file. This will either have come from the
|
||||
# original source file that was copied into the archive, or from the
|
||||
# creation date of the archive if there was no original source file.
|
||||
::FileUtils.touch(dest_path, mtime: time) if @restore_times
|
||||
end
|
||||
if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
|
||||
::FileUtils.chown(@unix_uid, @unix_gid, dest_path)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/GuardClause
|
||||
|
||||
def set_extra_attributes_on_path(dest_path) # :nodoc:
|
||||
return unless file? || directory?
|
||||
|
@ -550,14 +481,9 @@ module Zip
|
|||
when ::Zip::FSTYPE_UNIX
|
||||
set_unix_attributes_on_path(dest_path)
|
||||
end
|
||||
|
||||
# Restore the timestamp on a file. This will either have come from the
|
||||
# original source file that was copied into the archive, or from the
|
||||
# creation date of the archive if there was no original source file.
|
||||
::FileUtils.touch(dest_path, mtime: time) if @restore_times
|
||||
end
|
||||
|
||||
def pack_c_dir_entry # :nodoc:
|
||||
def pack_c_dir_entry
|
||||
zip64 = @extra['Zip64']
|
||||
[
|
||||
@header_signature,
|
||||
|
@ -565,12 +491,12 @@ module Zip
|
|||
@fstype, # filesystem type
|
||||
@version_needed_to_extract, # @versionNeededToExtract
|
||||
@gp_flags, # @gp_flags
|
||||
compression_method,
|
||||
@compression_method,
|
||||
@time.to_binary_dos_time, # @last_mod_time
|
||||
@time.to_binary_dos_date, # @last_mod_date
|
||||
@crc,
|
||||
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
||||
zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
|
||||
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
||||
name_size,
|
||||
@extra ? @extra.c_dir_size : 0,
|
||||
comment_size,
|
||||
|
@ -584,12 +510,11 @@ module Zip
|
|||
].pack('VCCvvvvvVVVvvvvvVV')
|
||||
end
|
||||
|
||||
def write_c_dir_entry(io) # :nodoc:
|
||||
prep_cdir_zip64_extra
|
||||
|
||||
def write_c_dir_entry(io) #:nodoc:all
|
||||
prep_zip64_extra(false)
|
||||
case @fstype
|
||||
when ::Zip::FSTYPE_UNIX
|
||||
ft = case ftype
|
||||
ft = case @ftype
|
||||
when :file
|
||||
@unix_perms ||= 0o644
|
||||
::Zip::FILE_TYPE_FILE
|
||||
|
@ -602,7 +527,7 @@ module Zip
|
|||
end
|
||||
|
||||
unless ft.nil?
|
||||
@external_file_attributes = ((ft << 12) | (@unix_perms & 0o7777)) << 16
|
||||
@external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -613,42 +538,43 @@ module Zip
|
|||
io << @comment
|
||||
end
|
||||
|
||||
def ==(other) # :nodoc:
|
||||
def ==(other)
|
||||
return false unless other.class == self.class
|
||||
|
||||
# Compares contents of local entry and exposed fields
|
||||
%w[compression_method crc compressed_size size name extra filepath time].all? do |k|
|
||||
keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
|
||||
other.__send__(k.to_sym) == __send__(k.to_sym)
|
||||
end
|
||||
keys_equal && time == other.time
|
||||
end
|
||||
|
||||
def <=>(other) # :nodoc:
|
||||
def <=>(other)
|
||||
to_s <=> other.to_s
|
||||
end
|
||||
|
||||
# Returns an IO like object for the given ZipEntry.
|
||||
# Warning: may behave weird with symlinks.
|
||||
def get_input_stream(&block)
|
||||
if ftype == :directory
|
||||
yield ::Zip::NullInputStream if block
|
||||
if @ftype == :directory
|
||||
yield ::Zip::NullInputStream if block_given?
|
||||
::Zip::NullInputStream
|
||||
elsif @filepath
|
||||
case ftype
|
||||
case @ftype
|
||||
when :file
|
||||
::File.open(@filepath, 'rb', &block)
|
||||
when :symlink
|
||||
linkpath = ::File.readlink(@filepath)
|
||||
stringio = ::StringIO.new(linkpath)
|
||||
yield(stringio) if block
|
||||
yield(stringio) if block_given?
|
||||
stringio
|
||||
else
|
||||
raise "unknown @file_type #{ftype}"
|
||||
raise "unknown @file_type #{@ftype}"
|
||||
end
|
||||
else
|
||||
zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
|
||||
zis.instance_variable_set(:@complete_entry, self)
|
||||
zis.get_next_entry
|
||||
if block
|
||||
if block_given?
|
||||
begin
|
||||
yield(zis)
|
||||
ensure
|
||||
|
@ -685,30 +611,27 @@ module Zip
|
|||
end
|
||||
|
||||
@filepath = src_path
|
||||
@size = stat.size
|
||||
get_extra_attributes_from_path(@filepath)
|
||||
end
|
||||
|
||||
def write_to_zip_output_stream(zip_output_stream) # :nodoc:
|
||||
if ftype == :directory
|
||||
zip_output_stream.put_next_entry(self)
|
||||
def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
|
||||
if @ftype == :directory
|
||||
zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
|
||||
elsif @filepath
|
||||
zip_output_stream.put_next_entry(self)
|
||||
get_input_stream do |is|
|
||||
::Zip::IOExtras.copy_stream(zip_output_stream, is)
|
||||
end
|
||||
zip_output_stream.put_next_entry(self, nil, nil, compression_method || ::Zip::Entry::DEFLATED)
|
||||
get_input_stream { |is| ::Zip::IOExtras.copy_stream(zip_output_stream, is) }
|
||||
else
|
||||
zip_output_stream.copy_raw_entry(self)
|
||||
end
|
||||
end
|
||||
|
||||
def parent_as_string # :nodoc:
|
||||
def parent_as_string
|
||||
entry_name = name.chomp('/')
|
||||
slash_index = entry_name.rindex('/')
|
||||
slash_index ? entry_name.slice(0, slash_index + 1) : nil
|
||||
end
|
||||
|
||||
def get_raw_input_stream(&block) # :nodoc:
|
||||
def get_raw_input_stream(&block)
|
||||
if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
|
||||
yield @zipfile
|
||||
else
|
||||
|
@ -716,22 +639,12 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
def clean_up # :nodoc:
|
||||
@dirty = false # Any changes are written at this point.
|
||||
def clean_up
|
||||
# By default, do nothing
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_local_header_fields(io) # :nodoc:
|
||||
static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
|
||||
|
||||
unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
|
||||
raise Error, 'Premature end of file. Not enough data for zip entry local header'
|
||||
end
|
||||
|
||||
unpack_local_entry(static_sized_fields_buf)
|
||||
end
|
||||
|
||||
def set_time(binary_dos_date, binary_dos_time)
|
||||
@time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
|
||||
rescue ArgumentError
|
||||
|
@ -740,9 +653,9 @@ module Zip
|
|||
|
||||
def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
|
||||
if ::File.exist?(dest_path) && !yield(self, dest_path)
|
||||
raise ::Zip::DestinationExistsError, dest_path
|
||||
raise ::Zip::DestinationFileExistsError,
|
||||
"Destination '#{dest_path}' already exists"
|
||||
end
|
||||
|
||||
::File.open(dest_path, 'wb') do |os|
|
||||
get_input_stream do |is|
|
||||
bytes_written = 0
|
||||
|
@ -753,10 +666,10 @@ module Zip
|
|||
bytes_written += buf.bytesize
|
||||
next unless bytes_written > size && !warned
|
||||
|
||||
error = ::Zip::EntrySizeError.new(self)
|
||||
raise error if ::Zip.validate_entry_sizes
|
||||
message = "entry '#{name}' should be #{size}B, but is larger when inflated."
|
||||
raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
|
||||
|
||||
warn "WARNING: #{error.message}"
|
||||
warn "WARNING: #{message}"
|
||||
warned = true
|
||||
end
|
||||
end
|
||||
|
@ -769,11 +682,14 @@ module Zip
|
|||
return if ::File.directory?(dest_path)
|
||||
|
||||
if ::File.exist?(dest_path)
|
||||
raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
|
||||
|
||||
if block_given? && yield(self, dest_path)
|
||||
::FileUtils.rm_f dest_path
|
||||
else
|
||||
raise ::Zip::DestinationFileExistsError,
|
||||
"Cannot create directory '#{dest_path}'. " \
|
||||
'A file already exists with that name'
|
||||
end
|
||||
end
|
||||
|
||||
::FileUtils.mkdir_p(dest_path)
|
||||
set_extra_attributes_on_path(dest_path)
|
||||
end
|
||||
|
@ -787,71 +703,53 @@ module Zip
|
|||
|
||||
# apply missing data from the zip64 extra information field, if present
|
||||
# (required when file sizes exceed 2**32, but can be used for all files)
|
||||
def parse_zip64_extra(for_local_header) # :nodoc:
|
||||
return unless zip64?
|
||||
def parse_zip64_extra(for_local_header) #:nodoc:all
|
||||
return if @extra['Zip64'].nil?
|
||||
|
||||
if for_local_header
|
||||
@size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
|
||||
else
|
||||
@size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(
|
||||
@size, @compressed_size, @local_header_offset
|
||||
)
|
||||
@size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(@size, @compressed_size, @local_header_offset)
|
||||
end
|
||||
end
|
||||
|
||||
# For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
|
||||
# indicate compression level. This seems to be mainly cosmetic but they are
|
||||
# generally set by other tools - including in docx files. It is these flags
|
||||
# that are used by commandline tools (and elsewhere) to give an indication
|
||||
# of how compressed a file is. See the PKWARE APPNOTE for more information:
|
||||
# https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
||||
#
|
||||
# It's safe to simply OR these flags here as compression_level is read only.
|
||||
def set_compression_level_flags
|
||||
return unless compression_method == DEFLATED
|
||||
|
||||
case @compression_level
|
||||
when 1
|
||||
@gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
|
||||
when 2
|
||||
@gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
|
||||
when 8, 9
|
||||
@gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
|
||||
end
|
||||
def data_descriptor_size
|
||||
(@gp_flags & 0x0008) > 0 ? 16 : 0
|
||||
end
|
||||
|
||||
# rubocop:disable Style/GuardClause
|
||||
def prep_local_zip64_extra
|
||||
# create a zip64 extra information field if we need one
|
||||
def prep_zip64_extra(for_local_header) #:nodoc:all
|
||||
return unless ::Zip.write_zip64_support
|
||||
return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
|
||||
|
||||
# Might not know size here, so need ZIP64 just in case.
|
||||
# If we already have a ZIP64 extra (placeholder) then we must fill it in.
|
||||
if zip64? || @size.nil? || @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
|
||||
need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
|
||||
need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
|
||||
if need_zip64
|
||||
@version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
|
||||
zip64 = @extra['Zip64'] || @extra.create('Zip64')
|
||||
|
||||
# Local header always includes size and compressed size.
|
||||
zip64.original_size = @size || 0
|
||||
@extra.delete('Zip64Placeholder')
|
||||
zip64 = @extra.create('Zip64')
|
||||
if for_local_header
|
||||
# local header always includes size and compressed size
|
||||
zip64.original_size = @size
|
||||
zip64.compressed_size = @compressed_size
|
||||
end
|
||||
end
|
||||
|
||||
def prep_cdir_zip64_extra
|
||||
return unless ::Zip.write_zip64_support
|
||||
|
||||
if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
|
||||
@local_header_offset >= 0xFFFFFFFF
|
||||
@version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
|
||||
zip64 = @extra['Zip64'] || @extra.create('Zip64')
|
||||
|
||||
# Central directory entry entries include whichever fields are necessary.
|
||||
zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
|
||||
else
|
||||
# central directory entry entries include whichever fields are necessary
|
||||
zip64.original_size = @size if @size >= 0xFFFFFFFF
|
||||
zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
|
||||
zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
|
||||
end
|
||||
else
|
||||
@extra.delete('Zip64')
|
||||
|
||||
# if this is a local header entry, create a placeholder
|
||||
# so we have room to write a zip64 extra field afterward
|
||||
# (we won't know if it's needed until the file data is written)
|
||||
if for_local_header
|
||||
@extra.create('Zip64Placeholder')
|
||||
else
|
||||
@extra.delete('Zip64Placeholder')
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/GuardClause
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class EntrySet #:nodoc:all
|
||||
include Enumerable
|
||||
|
||||
attr_reader :entry_set
|
||||
protected :entry_set
|
||||
attr_accessor :entry_set, :entry_order
|
||||
|
||||
def initialize(an_enumerable = [])
|
||||
super()
|
||||
|
@ -37,8 +33,10 @@ module Zip
|
|||
entry if @entry_set.delete(to_key(entry))
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
entries.each(&block)
|
||||
def each
|
||||
@entry_set = sorted_entries.dup.each do |_, value|
|
||||
yield(value)
|
||||
end
|
||||
end
|
||||
|
||||
def entries
|
||||
|
@ -61,18 +59,18 @@ module Zip
|
|||
end
|
||||
|
||||
def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH | ::File::FNM_EXTGLOB)
|
||||
entries.filter_map do |entry|
|
||||
entries.map do |entry|
|
||||
next nil unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
|
||||
|
||||
yield(entry) if block_given?
|
||||
entry
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def sorted_entries
|
||||
::Zip.sort_entries ? @entry_set.sort.to_h : @entry_set
|
||||
::Zip.sort_entries ? Hash[@entry_set.sort] : @entry_set
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,139 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
# The superclass for all rubyzip error types. Simply rescue this one if
|
||||
# you don't need to know what sort of error has been raised.
|
||||
class Error < StandardError; end
|
||||
class EntryExistsError < Error; end
|
||||
class DestinationFileExistsError < Error; end
|
||||
class CompressionMethodError < Error; end
|
||||
class EntryNameError < Error; end
|
||||
class EntrySizeError < Error; end
|
||||
class InternalError < Error; end
|
||||
class GPFBit3Error < Error; end
|
||||
class DecompressionError < Error; end
|
||||
|
||||
# Error raised if an unsupported compression method is used.
|
||||
class CompressionMethodError < Error
|
||||
# The compression method that has caused this error.
|
||||
attr_reader :compression_method
|
||||
|
||||
# Create a new CompressionMethodError with the specified incorrect
|
||||
# compression method.
|
||||
def initialize(method)
|
||||
super()
|
||||
@compression_method = method
|
||||
end
|
||||
|
||||
# The message returned by this error.
|
||||
def message
|
||||
"Unsupported compression method: #{COMPRESSION_METHODS[@compression_method]}."
|
||||
end
|
||||
end
|
||||
|
||||
# Error raised if there is a problem while decompressing an archive entry.
|
||||
class DecompressionError < Error
|
||||
# The error from the underlying Zlib library that caused this error.
|
||||
attr_reader :zlib_error
|
||||
|
||||
# Create a new DecompressionError with the specified underlying Zlib
|
||||
# error.
|
||||
def initialize(zlib_error)
|
||||
super()
|
||||
@zlib_error = zlib_error
|
||||
end
|
||||
|
||||
# The message returned by this error.
|
||||
def message
|
||||
"Zlib error ('#{@zlib_error.message}') while inflating."
|
||||
end
|
||||
end
|
||||
|
||||
# Error raised when trying to extract an archive entry over an
|
||||
# existing file.
|
||||
class DestinationExistsError < Error
|
||||
# Create a new DestinationExistsError with the clashing destination.
|
||||
def initialize(destination)
|
||||
super()
|
||||
@destination = destination
|
||||
end
|
||||
|
||||
# The message returned by this error.
|
||||
def message
|
||||
"Cannot create file or directory '#{@destination}'. " \
|
||||
'A file already exists with that name.'
|
||||
end
|
||||
end
|
||||
|
||||
# Error raised when trying to add an entry to an archive where the
|
||||
# entry name already exists.
|
||||
class EntryExistsError < Error
|
||||
# Create a new EntryExistsError with the specified source and name.
|
||||
def initialize(source, name)
|
||||
super()
|
||||
@source = source
|
||||
@name = name
|
||||
end
|
||||
|
||||
# The message returned by this error.
|
||||
def message
|
||||
"'#{@source}' failed. Entry #{@name} already exists."
|
||||
end
|
||||
end
|
||||
|
||||
# Error raised when an entry name is invalid.
|
||||
class EntryNameError < Error
|
||||
# Create a new EntryNameError with the specified name.
|
||||
def initialize(name = nil)
|
||||
super()
|
||||
@name = name
|
||||
end
|
||||
|
||||
# The message returned by this error.
|
||||
def message
|
||||
if @name.nil?
|
||||
'Illegal entry name. Names must have fewer than 65,536 characters.'
|
||||
else
|
||||
"Illegal entry name '#{@name}'. Names must not start with '/'."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Error raised if an entry is larger on extraction than it is advertised
|
||||
# to be.
|
||||
class EntrySizeError < Error
|
||||
# The entry that has caused this error.
|
||||
attr_reader :entry
|
||||
|
||||
# Create a new EntrySizeError with the specified entry.
|
||||
def initialize(entry)
|
||||
super()
|
||||
@entry = entry
|
||||
end
|
||||
|
||||
# The message returned by this error.
|
||||
def message
|
||||
"Entry '#{@entry.name}' should be #{@entry.size}B, but is larger when inflated."
|
||||
end
|
||||
end
|
||||
|
||||
# Error raised if a split archive is read. Rubyzip does not support reading
|
||||
# split archives.
|
||||
class SplitArchiveError < Error
|
||||
# The message returned by this error.
|
||||
def message
|
||||
'Rubyzip cannot extract from split archives at this time.'
|
||||
end
|
||||
end
|
||||
|
||||
# Error raised if there is not enough metadata for the entry to be streamed.
|
||||
class StreamingError < Error
|
||||
# The entry that has caused this error.
|
||||
attr_reader :entry
|
||||
|
||||
# Create a new StreamingError with the specified entry.
|
||||
def initialize(entry)
|
||||
super()
|
||||
@entry = entry
|
||||
end
|
||||
|
||||
# The message returned by this error.
|
||||
def message
|
||||
"The local header of this entry ('#{@entry.name}') does not contain " \
|
||||
'the correct metadata for `Zip::InputStream` to be able to ' \
|
||||
'uncompress it. Please use `Zip::File` instead of `Zip::InputStream`.'
|
||||
end
|
||||
end
|
||||
# Backwards compatibility with v1 (delete in v2)
|
||||
ZipError = Error
|
||||
ZipEntryExistsError = EntryExistsError
|
||||
ZipDestinationFileExistsError = DestinationFileExistsError
|
||||
ZipCompressionMethodError = CompressionMethodError
|
||||
ZipEntryNameError = EntryNameError
|
||||
ZipInternalError = InternalError
|
||||
end
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class ExtraField < Hash # :nodoc:all
|
||||
class ExtraField < Hash
|
||||
ID_MAP = {}
|
||||
|
||||
def initialize(binstr = nil, local: false)
|
||||
merge(binstr, local: local) if binstr
|
||||
def initialize(binstr = nil)
|
||||
merge(binstr) if binstr
|
||||
end
|
||||
|
||||
def extra_field_type_exist(binstr, id, len, index)
|
||||
|
@ -18,18 +16,25 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
def extra_field_type_unknown(binstr, len, index, local)
|
||||
self['Unknown'] ||= Unknown.new
|
||||
|
||||
if !len || len + 4 > binstr[index..].bytesize
|
||||
self['Unknown'].merge(binstr[index..], local: local)
|
||||
def extra_field_type_unknown(binstr, len, index)
|
||||
create_unknown_item unless self['Unknown']
|
||||
if !len || len + 4 > binstr[index..-1].bytesize
|
||||
self['Unknown'] << binstr[index..-1]
|
||||
return
|
||||
end
|
||||
|
||||
self['Unknown'].merge(binstr[index, len + 4], local: local)
|
||||
self['Unknown'] << binstr[index, len + 4]
|
||||
end
|
||||
|
||||
def merge(binstr, local: false)
|
||||
def create_unknown_item
|
||||
s = +''
|
||||
class << s
|
||||
alias_method :to_c_dir_bin, :to_s
|
||||
alias_method :to_local_bin, :to_s
|
||||
end
|
||||
self['Unknown'] = s
|
||||
end
|
||||
|
||||
def merge(binstr)
|
||||
return if binstr.empty?
|
||||
|
||||
i = 0
|
||||
|
@ -39,7 +44,8 @@ module Zip
|
|||
if id && ID_MAP.member?(id)
|
||||
extra_field_type_exist(binstr, id, len, i)
|
||||
elsif id
|
||||
break unless extra_field_type_unknown(binstr, len, i, local)
|
||||
create_unknown_item unless self['Unknown']
|
||||
break unless extra_field_type_unknown(binstr, len, i)
|
||||
end
|
||||
i += len + 4
|
||||
end
|
||||
|
@ -53,8 +59,8 @@ module Zip
|
|||
self[name] = field_class.new
|
||||
end
|
||||
|
||||
# Place Unknown last, so "extra" data that is missing the proper
|
||||
# signature/size does not prevent known fields from being read back in.
|
||||
# place Unknown last, so "extra" data that is missing the proper signature/size
|
||||
# does not prevent known fields from being read back in
|
||||
def ordered_values
|
||||
result = []
|
||||
each { |k, v| k == 'Unknown' ? result.push(v) : result.unshift(v) }
|
||||
|
@ -84,12 +90,12 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
require 'zip/extra_field/unknown'
|
||||
require 'zip/extra_field/generic'
|
||||
require 'zip/extra_field/universal_time'
|
||||
require 'zip/extra_field/old_unix'
|
||||
require 'zip/extra_field/unix'
|
||||
require 'zip/extra_field/zip64'
|
||||
require 'zip/extra_field/zip64_placeholder'
|
||||
require 'zip/extra_field/ntfs'
|
||||
|
||||
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class ExtraField::Generic # :nodoc:
|
||||
class ExtraField::Generic
|
||||
def self.register_map
|
||||
return unless const_defined?(:HEADER_ID)
|
||||
|
||||
|
@ -21,17 +19,26 @@ module Zip
|
|||
return false
|
||||
end
|
||||
|
||||
[binstr[2, 2].unpack1('v'), binstr[4..]]
|
||||
[binstr[2, 2].unpack1('v'), binstr[4..-1]]
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
return false if self.class != other.class
|
||||
|
||||
each do |k, v|
|
||||
return false if v != other[k]
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def to_local_bin
|
||||
s = pack_for_local
|
||||
(self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
|
||||
self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
|
||||
end
|
||||
|
||||
def to_c_dir_bin
|
||||
s = pack_for_c_dir
|
||||
(self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
|
||||
self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
# PKWARE NTFS Extra Field (0x000a)
|
||||
# Only Tag 0x0001 is supported
|
||||
class ExtraField::NTFS < ExtraField::Generic # :nodoc:
|
||||
class ExtraField::NTFS < ExtraField::Generic
|
||||
HEADER_ID = [0x000A].pack('v')
|
||||
register_map
|
||||
|
||||
|
@ -25,7 +23,7 @@ module Zip
|
|||
size, content = initial_parse(binstr)
|
||||
(size && content) || return
|
||||
|
||||
content = content[4..]
|
||||
content = content[4..-1]
|
||||
tags = parse_tags(content)
|
||||
|
||||
tag1 = tags[1]
|
||||
|
@ -53,7 +51,7 @@ module Zip
|
|||
# reserved 0 and tag 1
|
||||
s = [0, 1].pack('Vv')
|
||||
|
||||
tag1 = (+'').force_encoding(Encoding::BINARY)
|
||||
tag1 = ''.b
|
||||
if @mtime
|
||||
tag1 << [to_ntfs_time(@mtime)].pack('Q<')
|
||||
if @atime
|
||||
|
@ -86,7 +84,7 @@ module Zip
|
|||
end
|
||||
|
||||
def from_ntfs_time(ntfs_time)
|
||||
::Zip::DOSTime.at((ntfs_time / WINDOWS_TICK) - SEC_TO_UNIX_EPOCH)
|
||||
::Zip::DOSTime.at(ntfs_time / WINDOWS_TICK - SEC_TO_UNIX_EPOCH)
|
||||
end
|
||||
|
||||
def to_ntfs_time(time)
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
# Olf Info-ZIP Extra for UNIX uid/gid and file timestampes
|
||||
class ExtraField::OldUnix < ExtraField::Generic # :nodoc:
|
||||
class ExtraField::OldUnix < ExtraField::Generic
|
||||
HEADER_ID = 'UX'
|
||||
register_map
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
# Info-ZIP Additional timestamp field
|
||||
class ExtraField::UniversalTime < ExtraField::Generic # :nodoc:
|
||||
class ExtraField::UniversalTime < ExtraField::Generic
|
||||
HEADER_ID = 'UT'
|
||||
register_map
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
# Info-ZIP Extra for UNIX uid/gid
|
||||
class ExtraField::IUnix < ExtraField::Generic # :nodoc:
|
||||
class ExtraField::IUnix < ExtraField::Generic
|
||||
HEADER_ID = 'Ux'
|
||||
register_map
|
||||
|
||||
|
@ -22,8 +20,8 @@ module Zip
|
|||
return if !size || size == 0
|
||||
|
||||
uid, gid = content.unpack('vv')
|
||||
@uid = uid
|
||||
@gid = gid
|
||||
@uid ||= uid
|
||||
@gid ||= gid # rubocop:disable Naming/MemoizedInstanceVariableName
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
|
|
|
@ -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
|
|
@ -1,11 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
# Info-ZIP Extra for Zip64 size
|
||||
class ExtraField::Zip64 < ExtraField::Generic # :nodoc:
|
||||
attr_accessor :compressed_size, :disk_start_number,
|
||||
:original_size, :relative_header_offset
|
||||
|
||||
class ExtraField::Zip64 < ExtraField::Generic
|
||||
attr_accessor :original_size, :compressed_size, :relative_header_offset, :disk_start_number
|
||||
HEADER_ID = ['0100'].pack('H*')
|
||||
register_map
|
||||
|
||||
|
@ -40,9 +36,7 @@ module Zip
|
|||
def parse(original_size, compressed_size, relative_header_offset = nil, disk_start_number = nil)
|
||||
@original_size = extract(8, 'Q<') if original_size == 0xFFFFFFFF
|
||||
@compressed_size = extract(8, 'Q<') if compressed_size == 0xFFFFFFFF
|
||||
if relative_header_offset && relative_header_offset == 0xFFFFFFFF
|
||||
@relative_header_offset = extract(8, 'Q<')
|
||||
end
|
||||
@relative_header_offset = extract(8, 'Q<') if relative_header_offset && relative_header_offset == 0xFFFFFFFF
|
||||
@disk_start_number = extract(4, 'V') if disk_start_number && disk_start_number == 0xFFFF
|
||||
@content = nil
|
||||
[@original_size || original_size,
|
||||
|
@ -57,8 +51,7 @@ module Zip
|
|||
private :extract
|
||||
|
||||
def pack_for_local
|
||||
# Local header entries must contain original size and compressed size;
|
||||
# other fields do not apply.
|
||||
# local header entries must contain original size and compressed size; other fields do not apply
|
||||
return '' unless @original_size && @compressed_size
|
||||
|
||||
[@original_size, @compressed_size].pack('Q<Q<')
|
||||
|
@ -66,7 +59,7 @@ module Zip
|
|||
|
||||
def pack_for_c_dir
|
||||
# central directory entries contain only fields that didn't fit in the main entry part
|
||||
packed = (+'').force_encoding('BINARY')
|
||||
packed = ''.b
|
||||
packed << [@original_size].pack('Q<') if @original_size
|
||||
packed << [@compressed_size].pack('Q<') if @compressed_size
|
||||
packed << [@relative_header_offset].pack('Q<') if @relative_header_offset
|
||||
|
|
|
@ -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
|
414
lib/zip/file.rb
414
lib/zip/file.rb
|
@ -1,109 +1,132 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
|
||||
require_relative 'file_split'
|
||||
|
||||
module Zip
|
||||
# Zip::File is modeled after java.util.zip.ZipFile from the Java SDK.
|
||||
# The most important methods are those for accessing information about
|
||||
# the entries in
|
||||
# the archive and methods such as `get_input_stream` and
|
||||
# `get_output_stream` for reading from and writing entries to the
|
||||
# ZipFile is modeled after java.util.zip.ZipFile from the Java SDK.
|
||||
# The most important methods are those inherited from
|
||||
# ZipCentralDirectory for accessing information about the entries in
|
||||
# the archive and methods such as get_input_stream and
|
||||
# get_output_stream for reading from and writing entries to the
|
||||
# archive. The class includes a few convenience methods such as
|
||||
# `extract` for extracting entries to the filesystem, and `remove`,
|
||||
# `replace`, `rename` and `mkdir` for making simple modifications to
|
||||
# #extract for extracting entries to the filesystem, and #remove,
|
||||
# #replace, #rename and #mkdir for making simple modifications to
|
||||
# the archive.
|
||||
#
|
||||
# Modifications to a zip archive are not committed until `commit` or
|
||||
# `close` is called. The method `open` accepts a block following
|
||||
# the pattern from ::File.open offering a simple way to
|
||||
# Modifications to a zip archive are not committed until #commit or
|
||||
# #close is called. The method #open accepts a block following
|
||||
# the pattern from File.open offering a simple way to
|
||||
# automatically close the archive when the block returns.
|
||||
#
|
||||
# The following example opens zip archive `my.zip`
|
||||
# The following example opens zip archive <code>my.zip</code>
|
||||
# (creating it if it doesn't exist) and adds an entry
|
||||
# `first.txt` and a directory entry `a_dir`
|
||||
# <code>first.txt</code> and a directory entry <code>a_dir</code>
|
||||
# to it.
|
||||
#
|
||||
# ```
|
||||
# require 'zip'
|
||||
#
|
||||
# Zip::File.open('my.zip', create: true) do |zipfile|
|
||||
# zipfile.get_output_stream('first.txt') { |f| f.puts 'Hello from Zip::File' }
|
||||
# zipfile.mkdir('a_dir')
|
||||
# end
|
||||
# ```
|
||||
# Zip::File.open("my.zip", Zip::File::CREATE) {
|
||||
# |zipfile|
|
||||
# zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" }
|
||||
# zipfile.mkdir("a_dir")
|
||||
# }
|
||||
#
|
||||
# The next example reopens `my.zip`, writes the contents of
|
||||
# `first.txt` to standard out and deletes the entry from
|
||||
# The next example reopens <code>my.zip</code> writes the contents of
|
||||
# <code>first.txt</code> to standard out and deletes the entry from
|
||||
# the archive.
|
||||
#
|
||||
# ```
|
||||
# require 'zip'
|
||||
#
|
||||
# Zip::File.open('my.zip', create: true) do |zipfile|
|
||||
# puts zipfile.read('first.txt')
|
||||
# zipfile.remove('first.txt')
|
||||
# end
|
||||
# Zip::File.open("my.zip", Zip::File::CREATE) {
|
||||
# |zipfile|
|
||||
# puts zipfile.read("first.txt")
|
||||
# zipfile.remove("first.txt")
|
||||
# }
|
||||
#
|
||||
# Zip::FileSystem offers an alternative API that emulates ruby's
|
||||
# interface for accessing the filesystem, ie. the ::File and ::Dir classes.
|
||||
class File
|
||||
extend Forwardable
|
||||
extend FileSplit
|
||||
# ZipFileSystem offers an alternative API that emulates ruby's
|
||||
# interface for accessing the filesystem, ie. the File and Dir classes.
|
||||
|
||||
IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze # :nodoc:
|
||||
class File < CentralDirectory
|
||||
CREATE = true
|
||||
SPLIT_SIGNATURE = 0x08074b50
|
||||
ZIP64_EOCD_SIGNATURE = 0x06064b50
|
||||
MAX_SEGMENT_SIZE = 3_221_225_472
|
||||
MIN_SEGMENT_SIZE = 65_536
|
||||
DATA_BUFFER_SIZE = 8192
|
||||
IO_METHODS = [:tell, :seek, :read, :eof, :close]
|
||||
|
||||
DEFAULT_OPTIONS = {
|
||||
restore_ownership: false,
|
||||
restore_permissions: false,
|
||||
restore_times: false
|
||||
}.freeze
|
||||
|
||||
# The name of this zip archive.
|
||||
attr_reader :name
|
||||
|
||||
# default -> false.
|
||||
attr_accessor :restore_ownership
|
||||
|
||||
# default -> true.
|
||||
# default -> false, but will be set to true in a future version.
|
||||
attr_accessor :restore_permissions
|
||||
|
||||
# default -> true.
|
||||
# default -> false, but will be set to true in a future version.
|
||||
attr_accessor :restore_times
|
||||
|
||||
def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
|
||||
# Returns the zip files comment, if it has one
|
||||
attr_accessor :comment
|
||||
|
||||
# Opens a zip archive. Pass create: true to create
|
||||
# Opens a zip archive. Pass true as the second parameter to create
|
||||
# a new archive if it doesn't exist already.
|
||||
def initialize(path_or_io, create: false, buffer: false,
|
||||
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
||||
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
||||
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
||||
compression_level: ::Zip.default_compression)
|
||||
def initialize(path_or_io, dep_create = false, dep_buffer = false,
|
||||
create: false, buffer: false, **options)
|
||||
super()
|
||||
|
||||
Zip.warn_about_v3_api('File#new') if dep_create || dep_buffer
|
||||
|
||||
options = DEFAULT_OPTIONS.merge(options)
|
||||
@name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
|
||||
@create = create ? true : false # allow any truthy value to mean true
|
||||
@comment = ''
|
||||
@create = create || dep_create ? true : false # allow any truthy value to mean true
|
||||
buffer ||= dep_buffer
|
||||
|
||||
initialize_cdir(path_or_io, buffer: buffer)
|
||||
if ::File.size?(@name.to_s)
|
||||
# There is a file, which exists, that is associated with this zip.
|
||||
@create = false
|
||||
@file_permissions = ::File.stat(@name).mode
|
||||
|
||||
@restore_ownership = restore_ownership
|
||||
@restore_permissions = restore_permissions
|
||||
@restore_times = restore_times
|
||||
@compression_level = compression_level
|
||||
if buffer
|
||||
read_from_stream(path_or_io)
|
||||
else
|
||||
::File.open(@name, 'rb') do |f|
|
||||
read_from_stream(f)
|
||||
end
|
||||
end
|
||||
elsif buffer && path_or_io.size > 0
|
||||
# This zip is probably a non-empty StringIO.
|
||||
@create = false
|
||||
read_from_stream(path_or_io)
|
||||
elsif @create
|
||||
# This zip is completely new/empty and is to be created.
|
||||
@entry_set = EntrySet.new
|
||||
elsif ::File.zero?(@name)
|
||||
# A file exists, but it is empty.
|
||||
raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
|
||||
else
|
||||
# Everything is wrong.
|
||||
raise Error, "File #{@name} not found"
|
||||
end
|
||||
|
||||
@stored_entries = @entry_set.dup
|
||||
@stored_comment = @comment
|
||||
@restore_ownership = options[:restore_ownership]
|
||||
@restore_permissions = options[:restore_permissions]
|
||||
@restore_times = options[:restore_times]
|
||||
end
|
||||
|
||||
class << self
|
||||
# Similar to ::new. If a block is passed the Zip::File object is passed
|
||||
# to the block and is automatically closed afterwards, just as with
|
||||
# ruby's builtin File::open method.
|
||||
def open(file_name, create: false,
|
||||
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
||||
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
||||
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
||||
compression_level: ::Zip.default_compression)
|
||||
|
||||
zf = ::Zip::File.new(file_name, create: create,
|
||||
restore_ownership: restore_ownership,
|
||||
restore_permissions: restore_permissions,
|
||||
restore_times: restore_times,
|
||||
compression_level: compression_level)
|
||||
def open(file_name, dep_create = false, create: false, **options)
|
||||
Zip.warn_about_v3_api('Zip::File.open') if dep_create
|
||||
|
||||
zf = ::Zip::File.new(file_name, create: (dep_create || create), buffer: false, **options)
|
||||
return zf unless block_given?
|
||||
|
||||
begin
|
||||
|
@ -113,29 +136,31 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
# Same as #open. But outputs data to a buffer instead of a file
|
||||
def add_buffer
|
||||
Zip.warn_about_v3_api('Zip::File.add_buffer')
|
||||
|
||||
io = ::StringIO.new
|
||||
zf = ::Zip::File.new(io, true, true)
|
||||
yield zf
|
||||
zf.write_buffer(io)
|
||||
end
|
||||
|
||||
# Like #open, but reads zip archive contents from a String or open IO
|
||||
# stream, and outputs data to a buffer.
|
||||
# (This can be used to extract data from a
|
||||
# downloaded zip archive without first saving it to disk.)
|
||||
def open_buffer(io = ::StringIO.new, create: false,
|
||||
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
||||
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
||||
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
||||
compression_level: ::Zip.default_compression)
|
||||
|
||||
def open_buffer(io, **options)
|
||||
unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
|
||||
raise 'Zip::File.open_buffer expects a String or IO-like argument' \
|
||||
"(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
|
||||
raise "Zip::File.open_buffer expects a String or IO-like argument (responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
|
||||
end
|
||||
|
||||
io = ::StringIO.new(io) if io.kind_of?(::String)
|
||||
|
||||
zf = ::Zip::File.new(io, create: create, buffer: true,
|
||||
restore_ownership: restore_ownership,
|
||||
restore_permissions: restore_permissions,
|
||||
restore_times: restore_times,
|
||||
compression_level: compression_level)
|
||||
# https://github.com/rubyzip/rubyzip/issues/119
|
||||
io.binmode if io.respond_to?(:binmode)
|
||||
|
||||
zf = ::Zip::File.new(io, create: true, buffer: true, **options)
|
||||
return zf unless block_given?
|
||||
|
||||
yield zf
|
||||
|
@ -159,19 +184,89 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
# Count the entries in a zip archive without reading the whole set of
|
||||
# entry data into memory.
|
||||
def count_entries(path_or_io)
|
||||
cdir = ::Zip::CentralDirectory.new
|
||||
|
||||
if path_or_io.kind_of?(String)
|
||||
::File.open(path_or_io, 'rb') do |f|
|
||||
cdir.count_entries(f)
|
||||
end
|
||||
def get_segment_size_for_split(segment_size)
|
||||
if MIN_SEGMENT_SIZE > segment_size
|
||||
MIN_SEGMENT_SIZE
|
||||
elsif MAX_SEGMENT_SIZE < segment_size
|
||||
MAX_SEGMENT_SIZE
|
||||
else
|
||||
cdir.count_entries(path_or_io)
|
||||
segment_size
|
||||
end
|
||||
end
|
||||
|
||||
def get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
|
||||
unless partial_zip_file_name.nil?
|
||||
partial_zip_file_name = zip_file_name.sub(/#{::File.basename(zip_file_name)}\z/,
|
||||
partial_zip_file_name + ::File.extname(zip_file_name))
|
||||
end
|
||||
partial_zip_file_name ||= zip_file_name
|
||||
partial_zip_file_name
|
||||
end
|
||||
|
||||
def get_segment_count_for_split(zip_file_size, segment_size)
|
||||
(zip_file_size / segment_size).to_i + (zip_file_size % segment_size == 0 ? 0 : 1)
|
||||
end
|
||||
|
||||
def put_split_signature(szip_file, segment_size)
|
||||
signature_packed = [SPLIT_SIGNATURE].pack('V')
|
||||
szip_file << signature_packed
|
||||
segment_size - signature_packed.size
|
||||
end
|
||||
|
||||
#
|
||||
# TODO: Make the code more understandable
|
||||
#
|
||||
def save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
|
||||
ssegment_size = zip_file_size - zip_file.pos
|
||||
ssegment_size = segment_size if ssegment_size > segment_size
|
||||
szip_file_name = "#{partial_zip_file_name}.#{format('%03d', szip_file_index)}"
|
||||
::File.open(szip_file_name, 'wb') do |szip_file|
|
||||
if szip_file_index == 1
|
||||
ssegment_size = put_split_signature(szip_file, segment_size)
|
||||
end
|
||||
chunk_bytes = 0
|
||||
until ssegment_size == chunk_bytes || zip_file.eof?
|
||||
segment_bytes_left = ssegment_size - chunk_bytes
|
||||
buffer_size = segment_bytes_left < DATA_BUFFER_SIZE ? segment_bytes_left : DATA_BUFFER_SIZE
|
||||
chunk = zip_file.read(buffer_size)
|
||||
chunk_bytes += buffer_size
|
||||
szip_file << chunk
|
||||
# Info for track splitting
|
||||
yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Splits an archive into parts with segment size
|
||||
def split(zip_file_name,
|
||||
dep_segment_size = MAX_SEGMENT_SIZE, dep_delete_zip_file = true, dep_partial_zip_file_name = nil,
|
||||
segment_size: MAX_SEGMENT_SIZE, delete_zip_file: nil, partial_zip_file_name: nil)
|
||||
raise Error, "File #{zip_file_name} not found" unless ::File.exist?(zip_file_name)
|
||||
raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
|
||||
|
||||
if dep_segment_size != MAX_SEGMENT_SIZE || !dep_delete_zip_file || dep_partial_zip_file_name
|
||||
Zip.warn_about_v3_api('Zip::File.split')
|
||||
end
|
||||
|
||||
zip_file_size = ::File.size(zip_file_name)
|
||||
segment_size = get_segment_size_for_split(segment_size || dep_segment_size)
|
||||
return if zip_file_size <= segment_size
|
||||
|
||||
segment_count = get_segment_count_for_split(zip_file_size, segment_size)
|
||||
# Checking for correct zip structure
|
||||
::Zip::File.open(zip_file_name) {}
|
||||
partial_zip_file_name = get_partial_zip_file_name(zip_file_name, (partial_zip_file_name || dep_partial_zip_file_name))
|
||||
szip_file_index = 0
|
||||
::File.open(zip_file_name, 'rb') do |zip_file|
|
||||
until zip_file.eof?
|
||||
szip_file_index += 1
|
||||
save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
|
||||
end
|
||||
end
|
||||
delete_zip_file = delete_zip_file.nil? ? dep_delete_zip_file : delete_zip_file
|
||||
::File.delete(zip_file_name) if delete_zip_file
|
||||
szip_file_index
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an input stream to the specified entry. If a block is passed
|
||||
|
@ -186,31 +281,45 @@ module Zip
|
|||
# specified. If a block is passed the stream object is passed to the block and
|
||||
# the stream is automatically closed afterwards just as with ruby's builtin
|
||||
# File.open method.
|
||||
def get_output_stream(entry, permissions: nil, comment: nil,
|
||||
# rubocop:disable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
||||
def get_output_stream(entry,
|
||||
dep_permission_int = nil, dep_comment = nil,
|
||||
dep_extra = nil, dep_compressed_size = nil, dep_crc = nil,
|
||||
dep_compression_method = nil, dep_size = nil, dep_time = nil,
|
||||
permission_int: nil, comment: nil,
|
||||
extra: nil, compressed_size: nil, crc: nil,
|
||||
compression_method: nil, compression_level: nil,
|
||||
size: nil, time: nil, &a_proc)
|
||||
compression_method: nil, size: nil, time: nil,
|
||||
&a_proc)
|
||||
|
||||
unless dep_permission_int.nil? && dep_comment.nil? && dep_extra.nil? &&
|
||||
dep_compressed_size.nil? && dep_crc.nil? && dep_compression_method.nil? &&
|
||||
dep_size.nil? && dep_time.nil?
|
||||
Zip.warn_about_v3_api('Zip::File#get_output_stream')
|
||||
end
|
||||
|
||||
new_entry =
|
||||
if entry.kind_of?(Entry)
|
||||
entry
|
||||
else
|
||||
Entry.new(
|
||||
@name, entry.to_s, comment: comment, extra: extra,
|
||||
compressed_size: compressed_size, crc: crc, size: size,
|
||||
compression_method: compression_method,
|
||||
compression_level: compression_level, time: time
|
||||
)
|
||||
Entry.new(@name, entry.to_s,
|
||||
comment: (comment || dep_comment),
|
||||
extra: (extra || dep_extra),
|
||||
compressed_size: (compressed_size || dep_compressed_size),
|
||||
crc: (crc || dep_crc),
|
||||
compression_method: (compression_method || dep_compression_method),
|
||||
size: (size || dep_size),
|
||||
time: (time || dep_time))
|
||||
end
|
||||
if new_entry.directory?
|
||||
raise ArgumentError,
|
||||
"cannot open stream to directory entry - '#{new_entry}'"
|
||||
end
|
||||
new_entry.unix_perms = permissions
|
||||
new_entry.unix_perms = (permission_int || dep_permission_int)
|
||||
zip_streamable_entry = StreamableStream.new(new_entry)
|
||||
@cdir << zip_streamable_entry
|
||||
@entry_set << zip_streamable_entry
|
||||
zip_streamable_entry.get_output_stream(&a_proc)
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
||||
|
||||
# Returns the name of the zip archive
|
||||
def to_s
|
||||
|
@ -226,39 +335,31 @@ module Zip
|
|||
def add(entry, src_path, &continue_on_exists_proc)
|
||||
continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
|
||||
check_entry_exists(entry, continue_on_exists_proc, 'add')
|
||||
new_entry = if entry.kind_of?(::Zip::Entry)
|
||||
entry
|
||||
else
|
||||
::Zip::Entry.new(
|
||||
@name, entry.to_s,
|
||||
compression_level: @compression_level
|
||||
)
|
||||
end
|
||||
new_entry = entry.kind_of?(::Zip::Entry) ? entry : ::Zip::Entry.new(@name, entry.to_s)
|
||||
new_entry.gather_fileinfo_from_srcpath(src_path)
|
||||
@cdir << new_entry
|
||||
new_entry.dirty = true
|
||||
@entry_set << new_entry
|
||||
end
|
||||
|
||||
# Convenience method for adding the contents of a file to the archive
|
||||
# in Stored format (uncompressed)
|
||||
def add_stored(entry, src_path, &continue_on_exists_proc)
|
||||
entry = ::Zip::Entry.new(
|
||||
@name, entry.to_s, compression_method: ::Zip::Entry::STORED
|
||||
)
|
||||
entry = ::Zip::Entry.new(@name, entry.to_s, nil, nil, nil, nil, ::Zip::Entry::STORED)
|
||||
add(entry, src_path, &continue_on_exists_proc)
|
||||
end
|
||||
|
||||
# Removes the specified entry.
|
||||
def remove(entry)
|
||||
@cdir.delete(get_entry(entry))
|
||||
@entry_set.delete(get_entry(entry))
|
||||
end
|
||||
|
||||
# Renames the specified entry.
|
||||
def rename(entry, new_name, &continue_on_exists_proc)
|
||||
found_entry = get_entry(entry)
|
||||
check_entry_exists(new_name, continue_on_exists_proc, 'rename')
|
||||
@cdir.delete(found_entry)
|
||||
@entry_set.delete(found_entry)
|
||||
found_entry.name = new_name
|
||||
@cdir << found_entry
|
||||
@entry_set << found_entry
|
||||
end
|
||||
|
||||
# Replaces the specified entry with the contents of src_path (from
|
||||
|
@ -269,16 +370,25 @@ module Zip
|
|||
add(entry, src_path)
|
||||
end
|
||||
|
||||
# Extracts entry to file dest_path.
|
||||
def extract(entry, dest_path, &block)
|
||||
Zip.warn_about_v3_api('Zip::File#extract')
|
||||
|
||||
block ||= proc { ::Zip.on_exists_proc }
|
||||
found_entry = get_entry(entry)
|
||||
found_entry.extract(dest_path, &block)
|
||||
end
|
||||
|
||||
# Extracts `entry` to a file at `entry_path`, with `destination_directory`
|
||||
# as the base location in the filesystem.
|
||||
#
|
||||
# NB: The caller is responsible for making sure `destination_directory` is
|
||||
# safe, if it is passed.
|
||||
def extract(entry, entry_path = nil, destination_directory: '.', &block)
|
||||
def extract_v3(entry, entry_path = nil, destination_directory: '.', &block)
|
||||
block ||= proc { ::Zip.on_exists_proc }
|
||||
found_entry = get_entry(entry)
|
||||
entry_path ||= found_entry.name
|
||||
found_entry.extract(entry_path, destination_directory: destination_directory, &block)
|
||||
found_entry.extract_v3(entry_path, destination_directory: destination_directory, &block)
|
||||
end
|
||||
|
||||
# Commits changes that has been made since the previous commit to
|
||||
|
@ -288,15 +398,16 @@ module Zip
|
|||
|
||||
on_success_replace do |tmp_file|
|
||||
::Zip::OutputStream.open(tmp_file) do |zos|
|
||||
@cdir.each do |e|
|
||||
@entry_set.each do |e|
|
||||
e.write_to_zip_output_stream(zos)
|
||||
e.dirty = false
|
||||
e.clean_up
|
||||
end
|
||||
zos.comment = comment
|
||||
end
|
||||
true
|
||||
end
|
||||
initialize_cdir(@name)
|
||||
initialize(name)
|
||||
end
|
||||
|
||||
# Write buffer write changes to buffer and return
|
||||
|
@ -304,7 +415,7 @@ module Zip
|
|||
return io unless commit_required?
|
||||
|
||||
::Zip::OutputStream.write_buffer(io) do |zos|
|
||||
@cdir.each { |e| e.write_to_zip_output_stream(zos) }
|
||||
@entry_set.each { |e| e.write_to_zip_output_stream(zos) }
|
||||
zos.comment = comment
|
||||
end
|
||||
end
|
||||
|
@ -317,19 +428,16 @@ module Zip
|
|||
# Returns true if any changes has been made to this archive since
|
||||
# the previous commit
|
||||
def commit_required?
|
||||
return true if @create || @cdir.dirty?
|
||||
|
||||
@cdir.each do |e|
|
||||
return true if e.dirty?
|
||||
@entry_set.each do |e|
|
||||
return true if e.dirty
|
||||
end
|
||||
|
||||
false
|
||||
@comment != @stored_comment || @entry_set != @stored_entries || @create
|
||||
end
|
||||
|
||||
# Searches for entry with the specified name. Returns nil if
|
||||
# no entry is found. See also get_entry
|
||||
def find_entry(entry_name)
|
||||
selected_entry = @cdir.find_entry(entry_name)
|
||||
selected_entry = @entry_set.find_entry(entry_name)
|
||||
return if selected_entry.nil?
|
||||
|
||||
selected_entry.restore_ownership = @restore_ownership
|
||||
|
@ -338,6 +446,11 @@ module Zip
|
|||
selected_entry
|
||||
end
|
||||
|
||||
# Searches for entries given a glob
|
||||
def glob(*args, &block)
|
||||
@entry_set.glob(*args, &block)
|
||||
end
|
||||
|
||||
# Searches for an entry just as find_entry, but throws Errno::ENOENT
|
||||
# if no entry is found.
|
||||
def get_entry(entry)
|
||||
|
@ -353,50 +466,33 @@ module Zip
|
|||
|
||||
entry_name = entry_name.dup.to_s
|
||||
entry_name << '/' unless entry_name.end_with?('/')
|
||||
@cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
|
||||
@entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize_cdir(path_or_io, buffer: false)
|
||||
@cdir = ::Zip::CentralDirectory.new
|
||||
|
||||
if ::File.size?(@name.to_s)
|
||||
# There is a file, which exists, that is associated with this zip.
|
||||
@create = false
|
||||
@file_permissions = ::File.stat(@name).mode
|
||||
|
||||
if buffer
|
||||
# https://github.com/rubyzip/rubyzip/issues/119
|
||||
path_or_io.binmode if path_or_io.respond_to?(:binmode)
|
||||
@cdir.read_from_stream(path_or_io)
|
||||
else
|
||||
::File.open(@name, 'rb') do |f|
|
||||
@cdir.read_from_stream(f)
|
||||
end
|
||||
end
|
||||
elsif buffer && path_or_io.size > 0
|
||||
# This zip is probably a non-empty StringIO.
|
||||
@create = false
|
||||
@cdir.read_from_stream(path_or_io)
|
||||
elsif !@create && ::File.empty?(@name)
|
||||
# A file exists, but it is empty, and we've said we're
|
||||
# NOT creating a new zip.
|
||||
raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
|
||||
elsif !@create
|
||||
# If we get here, and we're not creating a new zip, then
|
||||
# everything is wrong.
|
||||
raise Error, "File #{@name} not found"
|
||||
def directory?(new_entry, src_path)
|
||||
path_is_directory = ::File.directory?(src_path)
|
||||
if new_entry.directory? && !path_is_directory
|
||||
raise ArgumentError,
|
||||
"entry name '#{new_entry}' indicates directory entry, but " \
|
||||
"'#{src_path}' is not a directory"
|
||||
elsif !new_entry.directory? && path_is_directory
|
||||
new_entry.name += '/'
|
||||
end
|
||||
new_entry.directory? && path_is_directory
|
||||
end
|
||||
|
||||
def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
|
||||
return unless @cdir.include?(entry_name)
|
||||
|
||||
continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
|
||||
raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
|
||||
return unless @entry_set.include?(entry_name)
|
||||
|
||||
if continue_on_exists_proc.call
|
||||
remove get_entry(entry_name)
|
||||
else
|
||||
raise ::Zip::EntryExistsError,
|
||||
proc_name + " failed. Entry #{entry_name} already exists"
|
||||
end
|
||||
end
|
||||
|
||||
def check_file(path)
|
||||
|
@ -406,6 +502,7 @@ module Zip
|
|||
def on_success_replace
|
||||
dirname, basename = ::File.split(name)
|
||||
::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
|
||||
begin
|
||||
if yield tmp_filename
|
||||
::File.rename(tmp_filename, name)
|
||||
::File.chmod(@file_permissions, name) unless @create
|
||||
|
@ -416,6 +513,7 @@ module Zip
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
||||
# rubyzip is free software; you can redistribute it and/or
|
||||
|
|
|
@ -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
|
|
@ -1,10 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'zip'
|
||||
require_relative 'filesystem/zip_file_name_mapper'
|
||||
require_relative 'filesystem/directory_iterator'
|
||||
require_relative 'filesystem/dir'
|
||||
require_relative 'filesystem/file'
|
||||
|
||||
module Zip
|
||||
# The ZipFileSystem API provides an API for accessing entries in
|
||||
|
@ -19,52 +13,627 @@ module Zip
|
|||
# <code>first.txt</code>, a directory entry named <code>mydir</code>
|
||||
# and finally another normal entry named <code>second.txt</code>
|
||||
#
|
||||
# ```
|
||||
# require 'zip/filesystem'
|
||||
#
|
||||
# Zip::File.open('my.zip', create: true) do |zipfile|
|
||||
# zipfile.file.open('first.txt', 'w') { |f| f.puts 'Hello world' }
|
||||
# zipfile.dir.mkdir('mydir')
|
||||
# zipfile.file.open('mydir/second.txt', 'w') { |f| f.puts 'Hello again' }
|
||||
# end
|
||||
# ```
|
||||
# Zip::File.open("my.zip", Zip::File::CREATE) {
|
||||
# |zipfile|
|
||||
# zipfile.file.open("first.txt", "w") { |f| f.puts "Hello world" }
|
||||
# zipfile.dir.mkdir("mydir")
|
||||
# zipfile.file.open("mydir/second.txt", "w") { |f| f.puts "Hello again" }
|
||||
# }
|
||||
#
|
||||
# Reading is as easy as writing, as the following example shows. The
|
||||
# example writes the contents of <code>first.txt</code> from zip archive
|
||||
# <code>my.zip</code> to standard out.
|
||||
#
|
||||
# ```
|
||||
# require 'zip/filesystem'
|
||||
#
|
||||
# Zip::File.open('my.zip') do |zipfile|
|
||||
# puts zipfile.file.read('first.txt')
|
||||
# end
|
||||
# ```
|
||||
# Zip::File.open("my.zip") {
|
||||
# |zipfile|
|
||||
# puts zipfile.file.read("first.txt")
|
||||
# }
|
||||
|
||||
module FileSystem
|
||||
def initialize # :nodoc:
|
||||
mapped_zip = ZipFileNameMapper.new(self)
|
||||
@zip_fs_dir = Dir.new(mapped_zip)
|
||||
@zip_fs_file = File.new(mapped_zip)
|
||||
@zip_fs_dir = ZipFsDir.new(mapped_zip)
|
||||
@zip_fs_file = ZipFsFile.new(mapped_zip)
|
||||
@zip_fs_dir.file = @zip_fs_file
|
||||
@zip_fs_file.dir = @zip_fs_dir
|
||||
end
|
||||
|
||||
# Returns a Zip::FileSystem::Dir which is much like ruby's builtin Dir
|
||||
# (class) object, except it works on the Zip::File on which this method is
|
||||
# Returns a ZipFsDir which is much like ruby's builtin Dir (class)
|
||||
# object, except it works on the Zip::File on which this method is
|
||||
# invoked
|
||||
def dir
|
||||
@zip_fs_dir
|
||||
end
|
||||
|
||||
# Returns a Zip::FileSystem::File which is much like ruby's builtin File
|
||||
# (class) object, except it works on the Zip::File on which this method is
|
||||
# Returns a ZipFsFile which is much like ruby's builtin File (class)
|
||||
# object, except it works on the Zip::File on which this method is
|
||||
# invoked
|
||||
def file
|
||||
@zip_fs_file
|
||||
end
|
||||
|
||||
# Instances of this class are normally accessed via the accessor
|
||||
# Zip::File::file. An instance of ZipFsFile behaves like ruby's
|
||||
# builtin File (class) object, except it works on Zip::File entries.
|
||||
#
|
||||
# The individual methods are not documented due to their
|
||||
# similarity with the methods in File
|
||||
class ZipFsFile
|
||||
attr_writer :dir
|
||||
# protected :dir
|
||||
|
||||
class ZipFsStat
|
||||
class << self
|
||||
def delegate_to_fs_file(*methods)
|
||||
methods.each do |method|
|
||||
class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{method} # def file?
|
||||
@zip_fs_file.#{method}(@entry_name) # @zip_fs_file.file?(@entry_name)
|
||||
end # end
|
||||
END_EVAL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class File # :nodoc:
|
||||
def initialize(zip_fs_file, entry_name)
|
||||
@zip_fs_file = zip_fs_file
|
||||
@entry_name = entry_name
|
||||
end
|
||||
|
||||
def kind_of?(type)
|
||||
super || type == ::File::Stat
|
||||
end
|
||||
|
||||
delegate_to_fs_file :file?, :directory?, :pipe?, :chardev?, :symlink?,
|
||||
:socket?, :blockdev?, :readable?, :readable_real?, :writable?, :ctime,
|
||||
:writable_real?, :executable?, :executable_real?, :sticky?, :owned?,
|
||||
:grpowned?, :setuid?, :setgid?, :zero?, :size, :size?, :mtime, :atime
|
||||
|
||||
def blocks
|
||||
nil
|
||||
end
|
||||
|
||||
def get_entry
|
||||
@zip_fs_file.__send__(:get_entry, @entry_name)
|
||||
end
|
||||
private :get_entry
|
||||
|
||||
def gid
|
||||
e = get_entry
|
||||
if e.extra.member? 'IUnix'
|
||||
e.extra['IUnix'].gid || 0
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def uid
|
||||
e = get_entry
|
||||
if e.extra.member? 'IUnix'
|
||||
e.extra['IUnix'].uid || 0
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def ino
|
||||
0
|
||||
end
|
||||
|
||||
def dev
|
||||
0
|
||||
end
|
||||
|
||||
def rdev
|
||||
0
|
||||
end
|
||||
|
||||
def rdev_major
|
||||
0
|
||||
end
|
||||
|
||||
def rdev_minor
|
||||
0
|
||||
end
|
||||
|
||||
def ftype
|
||||
if file?
|
||||
'file'
|
||||
elsif directory?
|
||||
'directory'
|
||||
else
|
||||
raise StandardError, 'Unknown file type'
|
||||
end
|
||||
end
|
||||
|
||||
def nlink
|
||||
1
|
||||
end
|
||||
|
||||
def blksize
|
||||
nil
|
||||
end
|
||||
|
||||
def mode
|
||||
e = get_entry
|
||||
if e.fstype == 3
|
||||
e.external_file_attributes >> 16
|
||||
else
|
||||
33_206 # 33206 is equivalent to -rw-rw-rw-
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(mapped_zip)
|
||||
@mapped_zip = mapped_zip
|
||||
end
|
||||
|
||||
def get_entry(filename)
|
||||
unless exists?(filename)
|
||||
raise Errno::ENOENT, "No such file or directory - #{filename}"
|
||||
end
|
||||
|
||||
@mapped_zip.find_entry(filename)
|
||||
end
|
||||
private :get_entry
|
||||
|
||||
def unix_mode_cmp(filename, mode)
|
||||
e = get_entry(filename)
|
||||
e.fstype == 3 && ((e.external_file_attributes >> 16) & mode) != 0
|
||||
rescue Errno::ENOENT
|
||||
false
|
||||
end
|
||||
private :unix_mode_cmp
|
||||
|
||||
def exists?(filename)
|
||||
expand_path(filename) == '/' || !@mapped_zip.find_entry(filename).nil?
|
||||
end
|
||||
alias exist? exists?
|
||||
|
||||
# Permissions not implemented, so if the file exists it is accessible
|
||||
alias owned? exists?
|
||||
alias grpowned? exists?
|
||||
|
||||
def readable?(filename)
|
||||
unix_mode_cmp(filename, 0o444)
|
||||
end
|
||||
alias readable_real? readable?
|
||||
|
||||
def writable?(filename)
|
||||
unix_mode_cmp(filename, 0o222)
|
||||
end
|
||||
alias writable_real? writable?
|
||||
|
||||
def executable?(filename)
|
||||
unix_mode_cmp(filename, 0o111)
|
||||
end
|
||||
alias executable_real? executable?
|
||||
|
||||
def setuid?(filename)
|
||||
unix_mode_cmp(filename, 0o4000)
|
||||
end
|
||||
|
||||
def setgid?(filename)
|
||||
unix_mode_cmp(filename, 0o2000)
|
||||
end
|
||||
|
||||
def sticky?(filename)
|
||||
unix_mode_cmp(filename, 0o1000)
|
||||
end
|
||||
|
||||
def umask(*args)
|
||||
::File.umask(*args)
|
||||
end
|
||||
|
||||
def truncate(_filename, _len)
|
||||
raise StandardError, 'truncate not supported'
|
||||
end
|
||||
|
||||
def directory?(filename)
|
||||
entry = @mapped_zip.find_entry(filename)
|
||||
expand_path(filename) == '/' || (!entry.nil? && entry.directory?)
|
||||
end
|
||||
|
||||
def open(filename, mode = 'r', permissions = 0o644, &block)
|
||||
mode = mode.delete('b') # ignore b option
|
||||
case mode
|
||||
when 'r'
|
||||
@mapped_zip.get_input_stream(filename, &block)
|
||||
when 'w'
|
||||
@mapped_zip.get_output_stream(filename, permissions, &block)
|
||||
else
|
||||
raise StandardError, "openmode '#{mode} not supported" unless mode == 'r'
|
||||
end
|
||||
end
|
||||
|
||||
def new(filename, mode = 'r')
|
||||
self.open(filename, mode)
|
||||
end
|
||||
|
||||
def size(filename)
|
||||
@mapped_zip.get_entry(filename).size
|
||||
end
|
||||
|
||||
# Returns nil for not found and nil for directories
|
||||
def size?(filename)
|
||||
entry = @mapped_zip.find_entry(filename)
|
||||
entry.nil? || entry.directory? ? nil : entry.size
|
||||
end
|
||||
|
||||
def chown(owner, group, *filenames)
|
||||
filenames.each do |filename|
|
||||
e = get_entry(filename)
|
||||
e.extra.create('IUnix') unless e.extra.member?('IUnix')
|
||||
e.extra['IUnix'].uid = owner
|
||||
e.extra['IUnix'].gid = group
|
||||
end
|
||||
filenames.size
|
||||
end
|
||||
|
||||
def chmod(mode, *filenames)
|
||||
filenames.each do |filename|
|
||||
e = get_entry(filename)
|
||||
e.fstype = 3 # force convertion filesystem type to unix
|
||||
e.unix_perms = mode
|
||||
e.external_file_attributes = mode << 16
|
||||
e.dirty = true
|
||||
end
|
||||
filenames.size
|
||||
end
|
||||
|
||||
def zero?(filename)
|
||||
sz = size(filename)
|
||||
sz.nil? || sz == 0
|
||||
rescue Errno::ENOENT
|
||||
false
|
||||
end
|
||||
|
||||
def file?(filename)
|
||||
entry = @mapped_zip.find_entry(filename)
|
||||
!entry.nil? && entry.file?
|
||||
end
|
||||
|
||||
def dirname(filename)
|
||||
::File.dirname(filename)
|
||||
end
|
||||
|
||||
def basename(filename)
|
||||
::File.basename(filename)
|
||||
end
|
||||
|
||||
def split(filename)
|
||||
::File.split(filename)
|
||||
end
|
||||
|
||||
def join(*fragments)
|
||||
::File.join(*fragments)
|
||||
end
|
||||
|
||||
def utime(modified_time, *filenames)
|
||||
filenames.each do |filename|
|
||||
get_entry(filename).time = modified_time
|
||||
end
|
||||
end
|
||||
|
||||
def mtime(filename)
|
||||
@mapped_zip.get_entry(filename).mtime
|
||||
end
|
||||
|
||||
def atime(filename)
|
||||
e = get_entry(filename)
|
||||
if e.extra.member? 'UniversalTime'
|
||||
e.extra['UniversalTime'].atime
|
||||
elsif e.extra.member? 'NTFS'
|
||||
e.extra['NTFS'].atime
|
||||
end
|
||||
end
|
||||
|
||||
def ctime(filename)
|
||||
e = get_entry(filename)
|
||||
if e.extra.member? 'UniversalTime'
|
||||
e.extra['UniversalTime'].ctime
|
||||
elsif e.extra.member? 'NTFS'
|
||||
e.extra['NTFS'].ctime
|
||||
end
|
||||
end
|
||||
|
||||
def pipe?(_filename)
|
||||
false
|
||||
end
|
||||
|
||||
def blockdev?(_filename)
|
||||
false
|
||||
end
|
||||
|
||||
def chardev?(_filename)
|
||||
false
|
||||
end
|
||||
|
||||
def symlink?(_filename)
|
||||
false
|
||||
end
|
||||
|
||||
def socket?(_filename)
|
||||
false
|
||||
end
|
||||
|
||||
def ftype(filename)
|
||||
@mapped_zip.get_entry(filename).directory? ? 'directory' : 'file'
|
||||
end
|
||||
|
||||
def readlink(_filename)
|
||||
raise NotImplementedError, 'The readlink() function is not implemented'
|
||||
end
|
||||
|
||||
def symlink(_filename, _symlink_name)
|
||||
raise NotImplementedError, 'The symlink() function is not implemented'
|
||||
end
|
||||
|
||||
def link(_filename, _symlink_name)
|
||||
raise NotImplementedError, 'The link() function is not implemented'
|
||||
end
|
||||
|
||||
def pipe
|
||||
raise NotImplementedError, 'The pipe() function is not implemented'
|
||||
end
|
||||
|
||||
def stat(filename)
|
||||
raise Errno::ENOENT, filename unless exists?(filename)
|
||||
|
||||
ZipFsStat.new(self, filename)
|
||||
end
|
||||
|
||||
alias lstat stat
|
||||
|
||||
def readlines(filename)
|
||||
self.open(filename, &:readlines)
|
||||
end
|
||||
|
||||
def read(filename)
|
||||
@mapped_zip.read(filename)
|
||||
end
|
||||
|
||||
def popen(*args, &a_proc)
|
||||
::File.popen(*args, &a_proc)
|
||||
end
|
||||
|
||||
def foreach(filename, sep = $INPUT_RECORD_SEPARATOR, &a_proc)
|
||||
self.open(filename) { |is| is.each_line(sep, &a_proc) }
|
||||
end
|
||||
|
||||
def delete(*args)
|
||||
args.each do |filename|
|
||||
if directory?(filename)
|
||||
raise Errno::EISDIR, "Is a directory - \"#{filename}\""
|
||||
end
|
||||
|
||||
@mapped_zip.remove(filename)
|
||||
end
|
||||
end
|
||||
|
||||
def rename(file_to_rename, new_name)
|
||||
@mapped_zip.rename(file_to_rename, new_name) { true }
|
||||
end
|
||||
|
||||
alias unlink delete
|
||||
|
||||
def expand_path(path)
|
||||
@mapped_zip.expand_path(path)
|
||||
end
|
||||
end
|
||||
|
||||
# Instances of this class are normally accessed via the accessor
|
||||
# ZipFile::dir. An instance of ZipFsDir behaves like ruby's
|
||||
# builtin Dir (class) object, except it works on ZipFile entries.
|
||||
#
|
||||
# The individual methods are not documented due to their
|
||||
# similarity with the methods in Dir
|
||||
class ZipFsDir
|
||||
def initialize(mapped_zip)
|
||||
@mapped_zip = mapped_zip
|
||||
end
|
||||
|
||||
attr_writer :file
|
||||
|
||||
def new(directory_name)
|
||||
ZipFsDirIterator.new(entries(directory_name))
|
||||
end
|
||||
|
||||
def open(directory_name)
|
||||
dir_iter = new(directory_name)
|
||||
if block_given?
|
||||
begin
|
||||
yield(dir_iter)
|
||||
return nil
|
||||
ensure
|
||||
dir_iter.close
|
||||
end
|
||||
end
|
||||
dir_iter
|
||||
end
|
||||
|
||||
def pwd
|
||||
@mapped_zip.pwd
|
||||
end
|
||||
alias getwd pwd
|
||||
|
||||
def chdir(directory_name)
|
||||
unless @file.stat(directory_name).directory?
|
||||
raise Errno::EINVAL, "Invalid argument - #{directory_name}"
|
||||
end
|
||||
|
||||
@mapped_zip.pwd = @file.expand_path(directory_name)
|
||||
end
|
||||
|
||||
def entries(directory_name)
|
||||
entries = []
|
||||
foreach(directory_name) { |e| entries << e }
|
||||
entries
|
||||
end
|
||||
|
||||
def glob(*args, &block)
|
||||
@mapped_zip.glob(*args, &block)
|
||||
end
|
||||
|
||||
def foreach(directory_name)
|
||||
unless @file.stat(directory_name).directory?
|
||||
raise Errno::ENOTDIR, directory_name
|
||||
end
|
||||
|
||||
path = @file.expand_path(directory_name)
|
||||
path << '/' unless path.end_with?('/')
|
||||
path = Regexp.escape(path)
|
||||
subdir_entry_regex = Regexp.new("^#{path}([^/]+)$")
|
||||
@mapped_zip.each do |filename|
|
||||
match = subdir_entry_regex.match(filename)
|
||||
yield(match[1]) unless match.nil?
|
||||
end
|
||||
end
|
||||
|
||||
def delete(entry_name)
|
||||
unless @file.stat(entry_name).directory?
|
||||
raise Errno::EINVAL, "Invalid argument - #{entry_name}"
|
||||
end
|
||||
|
||||
@mapped_zip.remove(entry_name)
|
||||
end
|
||||
alias rmdir delete
|
||||
alias unlink delete
|
||||
|
||||
def mkdir(entry_name, permissions = 0o755)
|
||||
@mapped_zip.mkdir(entry_name, permissions)
|
||||
end
|
||||
|
||||
def chroot(*_args)
|
||||
raise NotImplementedError, 'The chroot() function is not implemented'
|
||||
end
|
||||
end
|
||||
|
||||
class ZipFsDirIterator # :nodoc:all
|
||||
include Enumerable
|
||||
|
||||
def initialize(filenames)
|
||||
@filenames = filenames
|
||||
@index = 0
|
||||
end
|
||||
|
||||
def close
|
||||
@filenames = nil
|
||||
end
|
||||
|
||||
def each(&a_proc)
|
||||
raise IOError, 'closed directory' if @filenames.nil?
|
||||
|
||||
@filenames.each(&a_proc)
|
||||
end
|
||||
|
||||
def read
|
||||
raise IOError, 'closed directory' if @filenames.nil?
|
||||
|
||||
@filenames[(@index += 1) - 1]
|
||||
end
|
||||
|
||||
def rewind
|
||||
raise IOError, 'closed directory' if @filenames.nil?
|
||||
|
||||
@index = 0
|
||||
end
|
||||
|
||||
def seek(position)
|
||||
raise IOError, 'closed directory' if @filenames.nil?
|
||||
|
||||
@index = position
|
||||
end
|
||||
|
||||
def tell
|
||||
raise IOError, 'closed directory' if @filenames.nil?
|
||||
|
||||
@index
|
||||
end
|
||||
end
|
||||
|
||||
# All access to Zip::File from ZipFsFile and ZipFsDir goes through a
|
||||
# ZipFileNameMapper, which has one responsibility: ensure
|
||||
class ZipFileNameMapper # :nodoc:all
|
||||
include Enumerable
|
||||
|
||||
def initialize(zip_file)
|
||||
@zip_file = zip_file
|
||||
@pwd = '/'
|
||||
end
|
||||
|
||||
attr_accessor :pwd
|
||||
|
||||
def find_entry(filename)
|
||||
@zip_file.find_entry(expand_to_entry(filename))
|
||||
end
|
||||
|
||||
def get_entry(filename)
|
||||
@zip_file.get_entry(expand_to_entry(filename))
|
||||
end
|
||||
|
||||
def get_input_stream(filename, &a_proc)
|
||||
@zip_file.get_input_stream(expand_to_entry(filename), &a_proc)
|
||||
end
|
||||
|
||||
def get_output_stream(filename, permissions = nil, &a_proc)
|
||||
@zip_file.get_output_stream(
|
||||
expand_to_entry(filename), permissions, &a_proc
|
||||
)
|
||||
end
|
||||
|
||||
def glob(pattern, *flags, &block)
|
||||
@zip_file.glob(expand_to_entry(pattern), *flags, &block)
|
||||
end
|
||||
|
||||
def read(filename)
|
||||
@zip_file.read(expand_to_entry(filename))
|
||||
end
|
||||
|
||||
def remove(filename)
|
||||
@zip_file.remove(expand_to_entry(filename))
|
||||
end
|
||||
|
||||
def rename(filename, new_name, &continue_on_exists_proc)
|
||||
@zip_file.rename(
|
||||
expand_to_entry(filename),
|
||||
expand_to_entry(new_name),
|
||||
&continue_on_exists_proc
|
||||
)
|
||||
end
|
||||
|
||||
def mkdir(filename, permissions = 0o755)
|
||||
@zip_file.mkdir(expand_to_entry(filename), permissions)
|
||||
end
|
||||
|
||||
# Turns entries into strings and adds leading /
|
||||
# and removes trailing slash on directories
|
||||
def each
|
||||
@zip_file.each do |e|
|
||||
yield('/' + e.to_s.chomp('/'))
|
||||
end
|
||||
end
|
||||
|
||||
def expand_path(path)
|
||||
expanded = path.start_with?('/') ? path.dup : ::File.join(@pwd, path)
|
||||
expanded.gsub!(/\/\.(\/|$)/, '')
|
||||
expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, '')
|
||||
expanded.empty? ? '/' : expanded
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expand_to_entry(path)
|
||||
expand_path(path)[1..-1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class File
|
||||
include FileSystem
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,15 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class Inflater < Decompressor #:nodoc:all
|
||||
def initialize(*args)
|
||||
super
|
||||
|
||||
@buffer = +''
|
||||
@buffer = ''.b
|
||||
@zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
||||
end
|
||||
|
||||
def read(length = nil, outbuf = +'')
|
||||
def read(length = nil, outbuf = ''.b)
|
||||
return (length.nil? || length.zero? ? '' : nil) if eof
|
||||
|
||||
while length.nil? || (@buffer.bytesize < length)
|
||||
|
@ -39,8 +37,8 @@ module Zip
|
|||
retried += 1
|
||||
retry
|
||||
end
|
||||
rescue Zlib::Error => e
|
||||
raise ::Zip::DecompressionError, e
|
||||
rescue Zlib::Error
|
||||
raise(::Zip::DecompressionError, 'zlib error while inflating')
|
||||
end
|
||||
|
||||
def input_finished?
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
##
|
||||
module Zip
|
||||
# InputStream is the basic class for reading zip entries in a
|
||||
# zip file. It is possible to create a InputStream object directly,
|
||||
|
@ -40,8 +37,9 @@ module Zip
|
|||
#
|
||||
# java.util.zip.ZipInputStream is the original inspiration for this
|
||||
# class.
|
||||
|
||||
class InputStream
|
||||
CHUNK_SIZE = 32_768 # :nodoc:
|
||||
CHUNK_SIZE = 32_768
|
||||
|
||||
include ::Zip::IOExtras::AbstractInputStream
|
||||
|
||||
|
@ -51,35 +49,34 @@ module Zip
|
|||
#
|
||||
# @param context [String||IO||StringIO] file path or IO/StringIO object
|
||||
# @param offset [Integer] offset in the IO/StringIO
|
||||
def initialize(context, offset: 0, decrypter: nil)
|
||||
def initialize(context, dep_offset = 0, dep_decrypter = nil, offset: 0, decrypter: nil)
|
||||
super()
|
||||
@archive_io = get_io(context, offset)
|
||||
@decompressor = ::Zip::NullDecompressor
|
||||
@decrypter = decrypter || ::Zip::NullDecrypter.new
|
||||
@current_entry = nil
|
||||
@complete_entry = nil
|
||||
|
||||
if !dep_offset.zero? || !dep_decrypter.nil?
|
||||
Zip.warn_about_v3_api('Zip::InputStream.new')
|
||||
end
|
||||
|
||||
offset = dep_offset if offset.zero?
|
||||
@archive_io = get_io(context, offset)
|
||||
@decompressor = ::Zip::NullDecompressor
|
||||
@decrypter = decrypter || dep_decrypter || ::Zip::NullDecrypter.new
|
||||
@current_entry = nil
|
||||
end
|
||||
|
||||
# Close this InputStream. All further IO will raise an IOError.
|
||||
def close
|
||||
@archive_io.close
|
||||
end
|
||||
|
||||
# Returns an Entry object and positions the stream at the beginning of
|
||||
# the entry data. It is necessary to call this method on a newly created
|
||||
# InputStream before reading from the first entry in the archive.
|
||||
# Returns nil when there are no more entries.
|
||||
# Returns a Entry object. It is necessary to call this
|
||||
# method on a newly created InputStream before reading from
|
||||
# the first entry in the archive. Returns nil when there are
|
||||
# no more entries.
|
||||
def get_next_entry
|
||||
unless @current_entry.nil?
|
||||
raise StreamingError, @current_entry if @current_entry.incomplete?
|
||||
|
||||
@archive_io.seek(@current_entry.next_header_offset, IO::SEEK_SET)
|
||||
end
|
||||
|
||||
@archive_io.seek(@current_entry.next_header_offset, IO::SEEK_SET) if @current_entry
|
||||
open_entry
|
||||
end
|
||||
|
||||
# Rewinds the stream to the beginning of the current entry.
|
||||
# Rewinds the stream to the beginning of the current entry
|
||||
def rewind
|
||||
return if @current_entry.nil?
|
||||
|
||||
|
@ -94,19 +91,18 @@ module Zip
|
|||
@decompressor.read(length, outbuf)
|
||||
end
|
||||
|
||||
# Returns the size of the current entry, or `nil` if there isn't one.
|
||||
def size
|
||||
return if @current_entry.nil?
|
||||
|
||||
@current_entry.size
|
||||
end
|
||||
|
||||
class << self
|
||||
# Same as #initialize but if a block is passed the opened
|
||||
# stream is passed to the block and closed when the block
|
||||
# returns.
|
||||
def open(filename_or_io, offset: 0, decrypter: nil)
|
||||
zio = new(filename_or_io, offset: offset, decrypter: decrypter)
|
||||
def open(filename_or_io, dep_offset = 0, dep_decrypter = nil, offset: 0, decrypter: nil)
|
||||
if !dep_offset.zero? || !dep_decrypter.nil?
|
||||
Zip.warn_about_v3_api('Zip::InputStream.new')
|
||||
end
|
||||
|
||||
offset = dep_offset if offset.zero?
|
||||
|
||||
zio = new(filename_or_io, offset: offset, decrypter: decrypter || dep_decrypter)
|
||||
return zio unless block_given?
|
||||
|
||||
begin
|
||||
|
@ -115,11 +111,17 @@ module Zip
|
|||
zio.close if zio
|
||||
end
|
||||
end
|
||||
|
||||
def open_buffer(filename_or_io, offset = 0)
|
||||
Zip.warn_about_v3_api('Zip::InputStream.open_buffer')
|
||||
|
||||
::Zip::InputStream.open(filename_or_io, offset)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def get_io(io_or_file, offset = 0) # :nodoc:
|
||||
def get_io(io_or_file, offset = 0)
|
||||
if io_or_file.respond_to?(:seek)
|
||||
io = io_or_file.dup
|
||||
io.seek(offset, ::IO::SEEK_SET)
|
||||
|
@ -131,58 +133,56 @@ module Zip
|
|||
end
|
||||
end
|
||||
|
||||
def open_entry # :nodoc:
|
||||
def open_entry
|
||||
@current_entry = ::Zip::Entry.read_local_entry(@archive_io)
|
||||
return if @current_entry.nil?
|
||||
|
||||
if @current_entry.encrypted? && @decrypter.kind_of?(NullDecrypter)
|
||||
raise Error,
|
||||
'A password is required to decode this zip file'
|
||||
if @current_entry && @current_entry.encrypted? && @decrypter.kind_of?(NullEncrypter)
|
||||
raise Error, 'password required to decode zip file'
|
||||
end
|
||||
|
||||
if @current_entry.incomplete? && @current_entry.compressed_size == 0 && !@complete_entry
|
||||
raise StreamingError, @current_entry
|
||||
if @current_entry && @current_entry.incomplete? && @current_entry.crc == 0 \
|
||||
&& @current_entry.compressed_size == 0 \
|
||||
&& @current_entry.size == 0 && !@complete_entry
|
||||
raise GPFBit3Error,
|
||||
'General purpose flag Bit 3 is set so not possible to get proper info from local header.' \
|
||||
'Please use ::Zip::File instead of ::Zip::InputStream'
|
||||
end
|
||||
|
||||
@decrypted_io = get_decrypted_io
|
||||
@decompressor = get_decompressor
|
||||
flush
|
||||
@current_entry
|
||||
end
|
||||
|
||||
def get_decrypted_io # :nodoc:
|
||||
def get_decrypted_io
|
||||
header = @archive_io.read(@decrypter.header_bytesize)
|
||||
@decrypter.reset!(header)
|
||||
|
||||
::Zip::DecryptedIo.new(@archive_io, @decrypter)
|
||||
end
|
||||
|
||||
def get_decompressor # :nodoc:
|
||||
def get_decompressor
|
||||
return ::Zip::NullDecompressor if @current_entry.nil?
|
||||
|
||||
decompressed_size =
|
||||
if @current_entry.incomplete? && @current_entry.crc == 0 &&
|
||||
@current_entry.size == 0 && @complete_entry
|
||||
if @current_entry.incomplete? && @current_entry.crc == 0 && @current_entry.size == 0 && @complete_entry
|
||||
@complete_entry.size
|
||||
else
|
||||
@current_entry.size
|
||||
end
|
||||
|
||||
decompressor_class = ::Zip::Decompressor.find_by_compression_method(
|
||||
@current_entry.compression_method
|
||||
)
|
||||
decompressor_class = ::Zip::Decompressor.find_by_compression_method(@current_entry.compression_method)
|
||||
if decompressor_class.nil?
|
||||
raise ::Zip::CompressionMethodError, @current_entry.compression_method
|
||||
raise ::Zip::CompressionMethodError,
|
||||
"Unsupported compression method #{@current_entry.compression_method}"
|
||||
end
|
||||
|
||||
decompressor_class.new(@decrypted_io, decompressed_size)
|
||||
end
|
||||
|
||||
def produce_input # :nodoc:
|
||||
def produce_input
|
||||
@decompressor.read(CHUNK_SIZE)
|
||||
end
|
||||
|
||||
def input_finished? # :nodoc:
|
||||
def input_finished?
|
||||
@decompressor.eof
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
module IOExtras #:nodoc:
|
||||
CHUNK_SIZE = 131_072
|
||||
|
||||
RANGE_ALL = 0..-1
|
||||
|
||||
class << self
|
||||
def copy_stream(ostream, istream)
|
||||
ostream.write(istream.read(CHUNK_SIZE, +'')) until istream.eof?
|
||||
ostream.write(istream.read(CHUNK_SIZE, ''.b)) until istream.eof?
|
||||
end
|
||||
|
||||
def copy_stream_n(ostream, istream, nbytes)
|
||||
toread = nbytes
|
||||
while toread > 0 && !istream.eof?
|
||||
tr = [toread, CHUNK_SIZE].min
|
||||
ostream.write(istream.read(tr, +''))
|
||||
tr = toread > CHUNK_SIZE ? CHUNK_SIZE : toread
|
||||
ostream.write(istream.read(tr, ''.b))
|
||||
toread -= tr
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Implements kind_of? in order to pretend to be an IO object
|
||||
module FakeIO # :nodoc:
|
||||
module FakeIO
|
||||
def kind_of?(object)
|
||||
object == IO || super
|
||||
end
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
module IOExtras # :nodoc:
|
||||
module IOExtras
|
||||
# Implements many of the convenience methods of IO
|
||||
# such as gets, getc, readline and readlines
|
||||
# depends on: input_finished?, produce_input and read
|
||||
module AbstractInputStream # :nodoc:
|
||||
module AbstractInputStream
|
||||
include Enumerable
|
||||
include FakeIO
|
||||
|
||||
|
@ -13,22 +11,22 @@ module Zip
|
|||
super
|
||||
@lineno = 0
|
||||
@pos = 0
|
||||
@output_buffer = +''
|
||||
@output_buffer = ''.b
|
||||
end
|
||||
|
||||
attr_accessor :lineno
|
||||
attr_reader :pos
|
||||
|
||||
def read(number_of_bytes = nil, buf = +'')
|
||||
def read(number_of_bytes = nil, buf = ''.b)
|
||||
tbuf = if @output_buffer.bytesize > 0
|
||||
if number_of_bytes && number_of_bytes <= @output_buffer.bytesize
|
||||
if number_of_bytes <= @output_buffer.bytesize
|
||||
@output_buffer.slice!(0, number_of_bytes)
|
||||
else
|
||||
number_of_bytes -= @output_buffer.bytesize if number_of_bytes
|
||||
rbuf = sysread(number_of_bytes, buf)
|
||||
out = @output_buffer
|
||||
out << rbuf if rbuf
|
||||
@output_buffer = ''
|
||||
@output_buffer = ''.b
|
||||
out
|
||||
end
|
||||
else
|
||||
|
@ -36,7 +34,7 @@ module Zip
|
|||
end
|
||||
|
||||
if tbuf.nil? || tbuf.empty?
|
||||
return nil if number_of_bytes&.positive?
|
||||
return nil if number_of_bytes
|
||||
|
||||
return ''
|
||||
end
|
||||
|
@ -76,18 +74,15 @@ module Zip
|
|||
a_sep_string = "#{$INPUT_RECORD_SEPARATOR}#{$INPUT_RECORD_SEPARATOR}" if a_sep_string.empty?
|
||||
|
||||
buffer_index = 0
|
||||
over_limit = number_of_bytes && @output_buffer.bytesize >= number_of_bytes
|
||||
over_limit = (number_of_bytes && @output_buffer.bytesize >= number_of_bytes)
|
||||
while (match_index = @output_buffer.index(a_sep_string, buffer_index)).nil? && !over_limit
|
||||
buffer_index = [buffer_index, @output_buffer.bytesize - a_sep_string.bytesize].max
|
||||
return @output_buffer.empty? ? nil : flush if input_finished?
|
||||
|
||||
@output_buffer << produce_input
|
||||
over_limit = number_of_bytes && @output_buffer.bytesize >= number_of_bytes
|
||||
over_limit = (number_of_bytes && @output_buffer.bytesize >= number_of_bytes)
|
||||
end
|
||||
sep_index = [
|
||||
match_index + a_sep_string.bytesize,
|
||||
number_of_bytes || @output_buffer.bytesize
|
||||
].min
|
||||
sep_index = [match_index + a_sep_string.bytesize, number_of_bytes || @output_buffer.bytesize].min
|
||||
@pos += sep_index
|
||||
@output_buffer.slice!(0...sep_index)
|
||||
end
|
||||
|
@ -98,7 +93,7 @@ module Zip
|
|||
|
||||
def flush
|
||||
ret_val = @output_buffer
|
||||
@output_buffer = +''
|
||||
@output_buffer = ''.b
|
||||
ret_val
|
||||
end
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
module IOExtras # :nodoc:
|
||||
module IOExtras
|
||||
# Implements many of the output convenience methods of IO.
|
||||
# relies on <<
|
||||
module AbstractOutputStream # :nodoc:
|
||||
module AbstractOutputStream
|
||||
include FakeIO
|
||||
|
||||
def write(data)
|
||||
|
@ -13,7 +11,7 @@ module Zip
|
|||
end
|
||||
|
||||
def print(*params)
|
||||
self << params.join << $OUTPUT_RECORD_SEPARATOR.to_s
|
||||
self << params.join($OUTPUT_FIELD_SEPARATOR) << $OUTPUT_RECORD_SEPARATOR.to_s
|
||||
end
|
||||
|
||||
def printf(a_format_string, *params)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class NullCompressor < Compressor #:nodoc:all
|
||||
include Singleton
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
module NullDecompressor #:nodoc:all
|
||||
module_function
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
module NullInputStream #:nodoc:all
|
||||
include ::Zip::NullDecompressor
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
|
||||
##
|
||||
module Zip
|
||||
# ZipOutputStream is the basic class for writing zip files. It is
|
||||
# possible to create a ZipOutputStream object directly, passing
|
||||
|
@ -21,49 +16,57 @@ module Zip
|
|||
#
|
||||
# java.util.zip.ZipOutputStream is the original inspiration for this
|
||||
# class.
|
||||
|
||||
class OutputStream
|
||||
extend Forwardable
|
||||
include ::Zip::IOExtras::AbstractOutputStream
|
||||
|
||||
def_delegators :@cdir, :comment, :comment=
|
||||
attr_accessor :comment
|
||||
|
||||
# Opens the indicated zip file. If a file with that name already
|
||||
# exists it will be overwritten.
|
||||
def initialize(file_name, stream: false, encrypter: nil)
|
||||
def initialize(file_name, dep_stream = false, dep_encrypter = nil, stream: false, encrypter: nil)
|
||||
super()
|
||||
|
||||
Zip.warn_about_v3_api('Zip::OutputStream.new') if dep_stream || !dep_encrypter.nil?
|
||||
|
||||
@file_name = file_name
|
||||
@output_stream = if stream
|
||||
iostream = Zip::RUNNING_ON_WINDOWS ? @file_name : @file_name.dup
|
||||
@output_stream = if stream || dep_stream
|
||||
iostream = @file_name.dup
|
||||
iostream.reopen(@file_name)
|
||||
iostream.rewind
|
||||
iostream
|
||||
else
|
||||
::File.new(@file_name, 'wb')
|
||||
end
|
||||
@cdir = ::Zip::CentralDirectory.new
|
||||
@entry_set = ::Zip::EntrySet.new
|
||||
@compressor = ::Zip::NullCompressor.instance
|
||||
@encrypter = encrypter || ::Zip::NullEncrypter.new
|
||||
@encrypter = encrypter || dep_encrypter || ::Zip::NullEncrypter.new
|
||||
@closed = false
|
||||
@current_entry = nil
|
||||
@comment = nil
|
||||
end
|
||||
|
||||
class << self
|
||||
# Same as #initialize but if a block is passed the opened
|
||||
# stream is passed to the block and closed when the block
|
||||
# returns.
|
||||
def open(file_name, encrypter: nil)
|
||||
class << self
|
||||
def open(file_name, dep_encrypter = nil, encrypter: nil)
|
||||
return new(file_name) unless block_given?
|
||||
|
||||
zos = new(file_name, stream: false, encrypter: encrypter)
|
||||
Zip.warn_about_v3_api('Zip::OutputStream.open') unless dep_encrypter.nil?
|
||||
|
||||
zos = new(file_name, stream: false, encrypter: (encrypter || dep_encrypter))
|
||||
yield zos
|
||||
ensure
|
||||
zos.close if zos
|
||||
end
|
||||
|
||||
# Same as #open but writes to a filestream instead
|
||||
def write_buffer(io = ::StringIO.new, encrypter: nil)
|
||||
def write_buffer(io = ::StringIO.new, dep_encrypter = nil, encrypter: nil)
|
||||
Zip.warn_about_v3_api('Zip::OutputStream.write_buffer') unless dep_encrypter.nil?
|
||||
|
||||
io.binmode if io.respond_to?(:binmode)
|
||||
zos = new(io, stream: true, encrypter: encrypter)
|
||||
zos = new(io, stream: true, encrypter: (encrypter || dep_encrypter))
|
||||
yield zos
|
||||
zos.close_buffer
|
||||
end
|
||||
|
@ -75,7 +78,7 @@ module Zip
|
|||
|
||||
finalize_current_entry
|
||||
update_local_headers
|
||||
@cdir.write_to_stream(@output_stream)
|
||||
write_central_directory
|
||||
@output_stream.close
|
||||
@closed = true
|
||||
end
|
||||
|
@ -86,41 +89,37 @@ module Zip
|
|||
|
||||
finalize_current_entry
|
||||
update_local_headers
|
||||
@cdir.write_to_stream(@output_stream)
|
||||
write_central_directory
|
||||
@closed = true
|
||||
@output_stream.flush
|
||||
@output_stream
|
||||
end
|
||||
|
||||
# Closes the current entry and opens a new for writing.
|
||||
# +entry+ can be a ZipEntry object or a string.
|
||||
def put_next_entry(
|
||||
entry_name, comment = '', extra = ExtraField.new,
|
||||
compression_method = Entry::DEFLATED, level = Zip.default_compression
|
||||
)
|
||||
def put_next_entry(entry_name, comment = nil, extra = nil, compression_method = Entry::DEFLATED, level = Zip.default_compression)
|
||||
raise Error, 'zip stream is closed' if @closed
|
||||
|
||||
new_entry =
|
||||
if entry_name.kind_of?(Entry) || entry_name.kind_of?(StreamableStream)
|
||||
new_entry = if entry_name.kind_of?(Entry)
|
||||
entry_name
|
||||
else
|
||||
Entry.new(
|
||||
@file_name, entry_name.to_s, comment: comment, extra: extra,
|
||||
compression_method: compression_method, compression_level: level
|
||||
)
|
||||
Entry.new(@file_name, entry_name.to_s)
|
||||
end
|
||||
|
||||
init_next_entry(new_entry)
|
||||
new_entry.comment = comment unless comment.nil?
|
||||
unless extra.nil?
|
||||
new_entry.extra = extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
|
||||
end
|
||||
new_entry.compression_method = compression_method unless compression_method.nil?
|
||||
init_next_entry(new_entry, level)
|
||||
@current_entry = new_entry
|
||||
end
|
||||
|
||||
def copy_raw_entry(entry) # :nodoc:
|
||||
def copy_raw_entry(entry)
|
||||
entry = entry.dup
|
||||
raise Error, 'zip stream is closed' if @closed
|
||||
raise Error, 'entry is not a ZipEntry' unless entry.kind_of?(Entry)
|
||||
|
||||
finalize_current_entry
|
||||
@cdir << entry
|
||||
@entry_set << entry
|
||||
src_pos = entry.local_header_offset
|
||||
entry.write_local_entry(@output_stream)
|
||||
@compressor = NullCompressor.instance
|
||||
|
@ -139,53 +138,55 @@ module Zip
|
|||
return unless @current_entry
|
||||
|
||||
finish
|
||||
@current_entry.compressed_size = @output_stream.tell -
|
||||
@current_entry.local_header_offset -
|
||||
@current_entry.compressed_size = @output_stream.tell - \
|
||||
@current_entry.local_header_offset - \
|
||||
@current_entry.calculate_local_header_size
|
||||
@current_entry.size = @compressor.size
|
||||
@current_entry.crc = @compressor.crc
|
||||
@output_stream << @encrypter.data_descriptor(
|
||||
@current_entry.crc,
|
||||
@current_entry.compressed_size,
|
||||
@current_entry.size
|
||||
)
|
||||
@output_stream << @encrypter.data_descriptor(@current_entry.crc, @current_entry.compressed_size, @current_entry.size)
|
||||
@current_entry.gp_flags |= @encrypter.gp_flags
|
||||
@current_entry = nil
|
||||
@compressor = ::Zip::NullCompressor.instance
|
||||
end
|
||||
|
||||
def init_next_entry(entry)
|
||||
def init_next_entry(entry, level = Zip.default_compression)
|
||||
finalize_current_entry
|
||||
@cdir << entry
|
||||
@entry_set << entry
|
||||
entry.write_local_entry(@output_stream)
|
||||
@encrypter.reset!
|
||||
@output_stream << @encrypter.header(entry.mtime)
|
||||
@compressor = get_compressor(entry)
|
||||
@compressor = get_compressor(entry, level)
|
||||
end
|
||||
|
||||
def get_compressor(entry)
|
||||
def get_compressor(entry, level)
|
||||
case entry.compression_method
|
||||
when Entry::DEFLATED
|
||||
::Zip::Deflater.new(@output_stream, entry.compression_level, @encrypter)
|
||||
::Zip::Deflater.new(@output_stream, level, @encrypter)
|
||||
when Entry::STORED
|
||||
::Zip::PassThruCompressor.new(@output_stream)
|
||||
else
|
||||
raise ::Zip::CompressionMethodError, entry.compression_method
|
||||
raise ::Zip::CompressionMethodError,
|
||||
"Invalid compression method: '#{entry.compression_method}'"
|
||||
end
|
||||
end
|
||||
|
||||
def update_local_headers
|
||||
pos = @output_stream.pos
|
||||
@cdir.each do |entry|
|
||||
@entry_set.each do |entry|
|
||||
@output_stream.pos = entry.local_header_offset
|
||||
entry.write_local_entry(@output_stream, rewrite: true)
|
||||
entry.write_local_entry(@output_stream, true)
|
||||
end
|
||||
@output_stream.pos = pos
|
||||
end
|
||||
|
||||
def write_central_directory
|
||||
cdir = CentralDirectory.new(@entry_set, @comment)
|
||||
cdir.write_to_stream(@output_stream)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def finish # :nodoc:
|
||||
def finish
|
||||
@compressor.finish
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class PassThruCompressor < Compressor #:nodoc:all
|
||||
def initialize(output_stream)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class PassThruDecompressor < Decompressor #:nodoc:all
|
||||
def initialize(*args)
|
||||
|
@ -7,7 +5,7 @@ module Zip
|
|||
@read_so_far = 0
|
||||
end
|
||||
|
||||
def read(length = nil, outbuf = +'')
|
||||
def read(length = nil, outbuf = ''.b)
|
||||
return (length.nil? || length.zero? ? '' : nil) if eof
|
||||
|
||||
if length.nil? || (@read_so_far + length) > decompressed_size
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class StreamableDirectory < Entry # :nodoc:
|
||||
class StreamableDirectory < Entry
|
||||
def initialize(zipfile, entry, src_path = nil, permission = nil)
|
||||
super(zipfile, entry)
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
class StreamableStream < DelegateClass(Entry) # :nodoc:all
|
||||
def initialize(entry)
|
||||
|
@ -44,7 +42,6 @@ module Zip
|
|||
end
|
||||
|
||||
def clean_up
|
||||
super
|
||||
@temp_file.unlink
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Zip
|
||||
VERSION = '3.0.0.rc2' # :nodoc:
|
||||
VERSION = '2.4.1'
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/zip/version'
|
||||
lib = File.expand_path('lib', __dir__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'zip/version'
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'rubyzip'
|
||||
s.version = Zip::VERSION
|
||||
s.version = ::Zip::VERSION
|
||||
s.authors = ['Robert Haines', 'John Lees-Miller', 'Alexander Simonov']
|
||||
s.email = [
|
||||
'hainesr@gmail.com', 'jdleesmiller@gmail.com', 'alex@simonov.me'
|
||||
|
@ -12,11 +12,9 @@ Gem::Specification.new do |s|
|
|||
s.homepage = 'http://github.com/rubyzip/rubyzip'
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.summary = 'rubyzip is a ruby module for reading and writing zip files'
|
||||
s.files = Dir.glob('{samples,lib}/**/*.rb') +
|
||||
%w[LICENSE.md README.md Changelog.md Rakefile rubyzip.gemspec]
|
||||
s.files = Dir.glob('{samples,lib}/**/*.rb') + %w[README.md TODO Rakefile]
|
||||
s.require_paths = ['lib']
|
||||
s.license = 'BSD-2-Clause'
|
||||
|
||||
s.license = 'BSD 2-Clause'
|
||||
s.metadata = {
|
||||
'bug_tracker_uri' => 'https://github.com/rubyzip/rubyzip/issues',
|
||||
'changelog_uri' => "https://github.com/rubyzip/rubyzip/blob/v#{s.version}/Changelog.md",
|
||||
|
@ -25,15 +23,35 @@ Gem::Specification.new do |s|
|
|||
'wiki_uri' => 'https://github.com/rubyzip/rubyzip/wiki',
|
||||
'rubygems_mfa_required' => 'true'
|
||||
}
|
||||
s.required_ruby_version = '>= 2.4'
|
||||
s.add_development_dependency 'minitest', '~> 5.4'
|
||||
s.add_development_dependency 'pry', '~> 0.10'
|
||||
s.add_development_dependency 'rake', '~> 12.3', '>= 12.3.3'
|
||||
s.add_development_dependency 'rubocop', '~> 0.79'
|
||||
|
||||
s.required_ruby_version = '>= 3.0'
|
||||
s.post_install_message = <<~ENDBANNER
|
||||
RubyZip 3.0 is coming!
|
||||
**********************
|
||||
|
||||
s.add_development_dependency 'minitest', '~> 5.25'
|
||||
s.add_development_dependency 'rake', '~> 13.2'
|
||||
s.add_development_dependency 'rdoc', '~> 6.11'
|
||||
s.add_development_dependency 'rubocop', '~> 1.61.0'
|
||||
s.add_development_dependency 'rubocop-performance', '~> 1.20.0'
|
||||
s.add_development_dependency 'rubocop-rake', '~> 0.6.0'
|
||||
s.add_development_dependency 'simplecov', '~> 0.22.0'
|
||||
s.add_development_dependency 'simplecov-lcov', '~> 0.8'
|
||||
The public API of some Rubyzip classes has been modernized to use named
|
||||
parameters for optional arguments. Please check your usage of the
|
||||
following classes:
|
||||
* `Zip::File`
|
||||
* `Zip::Entry`
|
||||
* `Zip::InputStream`
|
||||
* `Zip::OutputStream`
|
||||
* `Zip::DOSTime`
|
||||
|
||||
Run your test suite with the `RUBYZIP_V3_API_WARN` environment
|
||||
variable set to see warnings about usage of the old API. This will
|
||||
help you to identify any changes that you need to make to your code.
|
||||
See https://github.com/rubyzip/rubyzip/wiki/Updating-to-version-3.x for
|
||||
more information.
|
||||
|
||||
Please ensure that your Gemfiles and .gemspecs are suitably restrictive
|
||||
to avoid an unexpected breakage when 3.0 is released (e.g. ~> 2.3.0).
|
||||
See https://github.com/rubyzip/rubyzip for details. The Changelog also
|
||||
lists other enhancements and bugfixes that have been implemented since
|
||||
version 2.3.0.
|
||||
ENDBANNER
|
||||
end
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
example.zip
|
||||
exampleout.zip
|
||||
filesystem.zip
|
||||
zipdialogui.rb
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH << '../lib'
|
||||
system('zip example.zip example.rb gtk_ruby_zip.rb')
|
||||
|
@ -21,8 +20,7 @@ end
|
|||
|
||||
zf = Zip::File.new('example.zip')
|
||||
zf.each_with_index do |entry, index|
|
||||
puts "entry #{index} is #{entry.name}, size = #{entry.size}, " \
|
||||
"compressed size = #{entry.compressed_size}"
|
||||
puts "entry #{index} is #{entry.name}, size = #{entry.size}, compressed size = #{entry.compressed_size}"
|
||||
# use zf.get_input_stream(entry) to get a ZipInputStream for the entry
|
||||
# entry can be the ZipEntry object or any object which has a to_s method that
|
||||
# returns the name of the entry.
|
||||
|
@ -72,11 +70,8 @@ part_zips_count = Zip::File.split('large_zip_file.zip', 2_097_152, false)
|
|||
puts "Zip file splitted in #{part_zips_count} parts"
|
||||
|
||||
# Track splitting an archive
|
||||
Zip::File.split(
|
||||
'large_zip_file.zip', 1_048_576, true, 'part_zip_file'
|
||||
) do |part_count, part_index, chunk_bytes, segment_bytes|
|
||||
puts "#{part_index} of #{part_count} part splitting: " \
|
||||
"#{(chunk_bytes.to_f / segment_bytes * 100).to_i}%"
|
||||
Zip::File.split('large_zip_file.zip', 1_048_576, true, 'part_zip_file') do |part_count, part_index, chunk_bytes, segment_bytes|
|
||||
puts "#{part_index} of #{part_count} part splitting: #{(chunk_bytes.to_f / segment_bytes * 100).to_i}%"
|
||||
end
|
||||
|
||||
# For other examples, look at zip.rb and ziptest.rb
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH << '../lib'
|
||||
|
||||
|
@ -7,9 +6,9 @@ require 'zip/filesystem'
|
|||
|
||||
EXAMPLE_ZIP = 'filesystem.zip'
|
||||
|
||||
FileUtils.rm_f(EXAMPLE_ZIP)
|
||||
File.delete(EXAMPLE_ZIP) if File.exist?(EXAMPLE_ZIP)
|
||||
|
||||
Zip::File.open(EXAMPLE_ZIP, create: true) do |zf|
|
||||
Zip::File.open(EXAMPLE_ZIP, Zip::File::CREATE) do |zf|
|
||||
zf.file.open('file1.txt', 'w') { |os| os.write 'first file1.txt' }
|
||||
zf.dir.mkdir('dir1')
|
||||
zf.dir.chdir('dir1')
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'zip'
|
||||
|
||||
# This is a simple example which uses rubyzip to
|
||||
|
@ -23,7 +21,7 @@ class ZipFileGenerator
|
|||
def write
|
||||
entries = Dir.entries(@input_dir) - %w[. ..]
|
||||
|
||||
::Zip::File.open(@output_file, create: true) do |zipfile|
|
||||
::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
|
||||
write_entries entries, '', zipfile
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH << '../lib'
|
||||
|
||||
|
@ -43,8 +42,7 @@ class MainApp < Gtk::Window
|
|||
end
|
||||
|
||||
class ButtonPanel < Gtk::HButtonBox
|
||||
attr_reader :extract_button, :open_button
|
||||
|
||||
attr_reader :open_button, :extract_button
|
||||
def initialize
|
||||
super
|
||||
set_layout(Gtk::BUTTONBOX_START)
|
||||
|
@ -74,7 +72,7 @@ class MainApp < Gtk::Window
|
|||
@zipfile.each do |entry|
|
||||
@clist.append([entry.name,
|
||||
entry.size.to_s,
|
||||
"#{100.0 * entry.compressedSize / entry.size}%"])
|
||||
(100.0 * entry.compressedSize / entry.size).to_s + '%'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
$VERBOSE = true
|
||||
|
||||
|
@ -7,7 +6,7 @@ $LOAD_PATH << '../lib'
|
|||
|
||||
require 'Qt'
|
||||
system('rbuic -o zipdialogui.rb zipdialogui.ui')
|
||||
require 'zipdialogui'
|
||||
require 'zipdialogui.rb'
|
||||
require 'zip'
|
||||
|
||||
a = Qt::Application.new(ARGV)
|
||||
|
@ -66,14 +65,14 @@ class ZipDialog < ZipDialogUI
|
|||
end
|
||||
puts "selected_items.size = #{selected_items.size}"
|
||||
puts "unselected_items.size = #{unselected_items.size}"
|
||||
items = selected_items.empty? ? unselected_items : selected_items
|
||||
items = !selected_items.empty? ? selected_items : unselected_items
|
||||
puts "items.size = #{items.size}"
|
||||
|
||||
d = Qt::FileDialog.get_existing_directory(nil, self)
|
||||
if d
|
||||
zipfile { |zf| items.each { |e| zf.extract(e, File.join(d, e)) } }
|
||||
else
|
||||
if !d
|
||||
puts 'No directory chosen'
|
||||
else
|
||||
zipfile { |zf| items.each { |e| zf.extract(e, File.join(d, e)) } }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH << '../lib'
|
||||
|
||||
require 'zip'
|
||||
|
||||
Zip::OutputStream.open('simple.zip') do |zos|
|
||||
::Zip::OutputStream.open('simple.zip') do |zos|
|
||||
zos.put_next_entry 'entry.txt'
|
||||
zos.puts 'Hello world'
|
||||
end
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
$VERBOSE = true
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class BasicZipFileTest < MiniTest::Test
|
||||
|
@ -10,35 +8,44 @@ class BasicZipFileTest < MiniTest::Test
|
|||
end
|
||||
|
||||
def test_entries
|
||||
expected_entry_names = TestZipFile::TEST_ZIP2.entry_names
|
||||
actual_entry_names = @zip_file.entries.entries.map(&:name)
|
||||
assert_equal(expected_entry_names.sort, actual_entry_names.sort)
|
||||
assert_equal(TestZipFile::TEST_ZIP2.entry_names.sort,
|
||||
@zip_file.entries.entries.sort.map(&:name))
|
||||
end
|
||||
|
||||
def test_each
|
||||
expected_entry_names = TestZipFile::TEST_ZIP2.entry_names
|
||||
actual_entry_names = []
|
||||
@zip_file.each { |entry| actual_entry_names << entry.name }
|
||||
assert_equal(expected_entry_names.sort, actual_entry_names.sort)
|
||||
count = 0
|
||||
visited = {}
|
||||
@zip_file.each do |entry|
|
||||
assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name))
|
||||
assert(!visited.include?(entry.name))
|
||||
visited[entry.name] = nil
|
||||
count = count.succ
|
||||
end
|
||||
assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count)
|
||||
end
|
||||
|
||||
def test_foreach
|
||||
expected_entry_names = TestZipFile::TEST_ZIP2.entry_names
|
||||
actual_entry_names = []
|
||||
::Zip::File.foreach(TestZipFile::TEST_ZIP2.zip_name) { |entry| actual_entry_names << entry.name }
|
||||
assert_equal(expected_entry_names.sort, actual_entry_names.sort)
|
||||
count = 0
|
||||
visited = {}
|
||||
::Zip::File.foreach(TestZipFile::TEST_ZIP2.zip_name) do |entry|
|
||||
assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name))
|
||||
assert(!visited.include?(entry.name))
|
||||
visited[entry.name] = nil
|
||||
count = count.succ
|
||||
end
|
||||
assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count)
|
||||
end
|
||||
|
||||
def test_get_input_stream
|
||||
expected_entry_names = TestZipFile::TEST_ZIP2.entry_names
|
||||
actual_entry_names = []
|
||||
|
||||
count = 0
|
||||
visited = {}
|
||||
@zip_file.each do |entry|
|
||||
actual_entry_names << entry.name
|
||||
assert_entry(entry.name, @zip_file.get_input_stream(entry), entry.name)
|
||||
assert(!visited.include?(entry.name))
|
||||
visited[entry.name] = nil
|
||||
count = count.succ
|
||||
end
|
||||
|
||||
assert_equal(expected_entry_names.sort, actual_entry_names.sort)
|
||||
assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count)
|
||||
end
|
||||
|
||||
def test_get_input_stream_block
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class Bzip2SupportTest < MiniTest::Test
|
||||
|
@ -7,12 +5,7 @@ class Bzip2SupportTest < MiniTest::Test
|
|||
|
||||
def test_read
|
||||
Zip::InputStream.open(BZIP2_ZIP_TEST_FILE) do |zis|
|
||||
error = assert_raises(Zip::CompressionMethodError) do
|
||||
zis.get_next_entry
|
||||
end
|
||||
|
||||
assert_equal(12, error.compression_method)
|
||||
assert_match(/BZIP2/, error.message)
|
||||
assert_raises(Zip::CompressionMethodError) { zis.get_next_entry }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,25 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ZipCaseSensitivityTest < MiniTest::Test
|
||||
include CommonZipFileFixture
|
||||
|
||||
SRC_FILES = [
|
||||
['test/data/file1.txt', 'testfile.rb'],
|
||||
['test/data/file2.txt', 'testFILE.rb']
|
||||
].freeze
|
||||
SRC_FILES = [['test/data/file1.txt', 'testfile.rb'],
|
||||
['test/data/file2.txt', 'testFILE.rb']]
|
||||
|
||||
def teardown
|
||||
::Zip.reset!
|
||||
::Zip.case_insensitive_match = false
|
||||
end
|
||||
|
||||
# Ensure that everything functions normally when +case_insensitive_match = false+
|
||||
def test_add_case_sensitive
|
||||
::Zip.case_insensitive_match = false
|
||||
|
||||
SRC_FILES.each { |(fn, _en)| assert(::File.exist?(fn)) }
|
||||
zf = ::Zip::File.new(EMPTY_FILENAME, create: true)
|
||||
SRC_FILES.each { |fn, _en| assert(::File.exist?(fn)) }
|
||||
zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
|
||||
|
||||
SRC_FILES.each { |fn, en| zf.add(en, fn) }
|
||||
zf.close
|
||||
|
@ -37,21 +33,20 @@ class ZipCaseSensitivityTest < MiniTest::Test
|
|||
def test_add_case_insensitive
|
||||
::Zip.case_insensitive_match = true
|
||||
|
||||
SRC_FILES.each { |(fn, _en)| assert(::File.exist?(fn)) }
|
||||
zf = ::Zip::File.new(EMPTY_FILENAME, create: true)
|
||||
SRC_FILES.each { |fn, _en| assert(::File.exist?(fn)) }
|
||||
zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
|
||||
|
||||
error = assert_raises Zip::EntryExistsError do
|
||||
assert_raises Zip::EntryExistsError do
|
||||
SRC_FILES.each { |fn, en| zf.add(en, fn) }
|
||||
end
|
||||
assert_match(/'add'/, error.message)
|
||||
end
|
||||
|
||||
# Ensure that names are treated case insensitively when reading files and +case_insensitive_match = true+
|
||||
def test_add_case_sensitive_read_case_insensitive
|
||||
::Zip.case_insensitive_match = false
|
||||
|
||||
SRC_FILES.each { |(fn, _en)| assert(::File.exist?(fn)) }
|
||||
zf = ::Zip::File.new(EMPTY_FILENAME, create: true)
|
||||
SRC_FILES.each { |fn, _en| assert(::File.exist?(fn)) }
|
||||
zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
|
||||
|
||||
SRC_FILES.each { |fn, en| zf.add(en, fn) }
|
||||
zf.close
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ZipCentralDirectoryEntryTest < MiniTest::Test
|
||||
|
@ -59,43 +57,13 @@ class ZipCentralDirectoryEntryTest < MiniTest::Test
|
|||
end
|
||||
end
|
||||
|
||||
def test_read_entry_from_truncated_zip_file_raises_error
|
||||
File.open('test/data/testDirectory.bin') do |f|
|
||||
# cdir entry header is at least 46 bytes, so just read a bit.
|
||||
fragment = f.read(12)
|
||||
assert_raises(::Zip::Error) do
|
||||
def test_read_entry_from_truncated_zip_file
|
||||
fragment = ''
|
||||
File.open('test/data/testDirectory.bin') { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes
|
||||
fragment.extend(IOizeString)
|
||||
entry = ::Zip::Entry.new
|
||||
entry.read_c_dir_entry(StringIO.new(fragment))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_read_entry_from_truncated_zip_file_returns_nil
|
||||
File.open('test/data/testDirectory.bin') do |f|
|
||||
# cdir entry header is at least 46 bytes, so just read a bit.
|
||||
fragment = f.read(12)
|
||||
assert_nil(::Zip::Entry.read_c_dir_entry(StringIO.new(fragment)))
|
||||
end
|
||||
end
|
||||
|
||||
def test_read_corrupted_entry_raises_error
|
||||
fragment = File.binread('test/data/testDirectory.bin')
|
||||
fragment.slice!(12)
|
||||
io = StringIO.new(fragment)
|
||||
assert_raises(::Zip::Error) do
|
||||
entry = ::Zip::Entry.new
|
||||
entry.read_c_dir_entry(io)
|
||||
# First entry will be read but break later entries.
|
||||
entry.read_c_dir_entry(io)
|
||||
end
|
||||
end
|
||||
|
||||
def test_read_corrupted_entry_returns_nil
|
||||
fragment = File.binread('test/data/testDirectory.bin')
|
||||
fragment.slice!(12)
|
||||
io = StringIO.new(fragment)
|
||||
refute_nil(::Zip::Entry.read_c_dir_entry(io))
|
||||
# First entry will be read but break later entries.
|
||||
assert_nil(::Zip::Entry.read_c_dir_entry(io))
|
||||
entry.read_c_dir_entry(fragment)
|
||||
raise 'ZipError expected'
|
||||
rescue ::Zip::Error
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ZipCentralDirectoryTest < MiniTest::Test
|
||||
|
@ -9,11 +7,12 @@ class ZipCentralDirectoryTest < MiniTest::Test
|
|||
|
||||
def test_read_from_stream
|
||||
::File.open(TestZipFile::TEST_ZIP2.zip_name, 'rb') do |zip_file|
|
||||
cdir = ::Zip::CentralDirectory.new
|
||||
cdir.read_from_stream(zip_file)
|
||||
cdir = ::Zip::CentralDirectory.read_from_stream(zip_file)
|
||||
|
||||
assert_equal(TestZipFile::TEST_ZIP2.entry_names.size, cdir.size)
|
||||
assert_equal(cdir.entries.map(&:name).sort, TestZipFile::TEST_ZIP2.entry_names.sort)
|
||||
assert(cdir.entries.sort.compare_enumerables(TestZipFile::TEST_ZIP2.entry_names.sort) do |cdir_entry, test_entry_name|
|
||||
cdir_entry.name == test_entry_name
|
||||
end)
|
||||
assert_equal(TestZipFile::TEST_ZIP2.comment, cdir.comment)
|
||||
end
|
||||
end
|
||||
|
@ -27,52 +26,21 @@ class ZipCentralDirectoryTest < MiniTest::Test
|
|||
rescue ::Zip::Error
|
||||
end
|
||||
|
||||
def test_read_eocd_with_wrong_cdir_offset_from_file
|
||||
::File.open('test/data/testDirectory.bin', 'rb') do |f|
|
||||
assert_raises(::Zip::Error) do
|
||||
cdir = ::Zip::CentralDirectory.new
|
||||
cdir.read_from_stream(f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_read_eocd_with_wrong_cdir_offset_from_buffer
|
||||
::File.open('test/data/testDirectory.bin', 'rb') do |f|
|
||||
assert_raises(::Zip::Error) do
|
||||
cdir = ::Zip::CentralDirectory.new
|
||||
cdir.read_from_stream(StringIO.new(f.read))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_count_entries
|
||||
[
|
||||
['test/data/osx-archive.zip', 4],
|
||||
['test/data/zip64-sample.zip', 2],
|
||||
['test/data/max_length_file_comment.zip', 1],
|
||||
['test/data/100000-files.zip', 100_000]
|
||||
].each do |filename, num_entries|
|
||||
cdir = ::Zip::CentralDirectory.new
|
||||
|
||||
::File.open(filename, 'rb') do |f|
|
||||
assert_equal(num_entries, cdir.count_entries(f))
|
||||
|
||||
f.seek(0)
|
||||
s = StringIO.new(f.read)
|
||||
assert_equal(num_entries, cdir.count_entries(s))
|
||||
end
|
||||
end
|
||||
def test_read_from_truncated_zip_file
|
||||
fragment = ''
|
||||
File.open('test/data/testDirectory.bin', 'rb') { |f| fragment = f.read }
|
||||
fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete
|
||||
fragment.extend(IOizeString)
|
||||
entry = ::Zip::CentralDirectory.new
|
||||
entry.read_from_stream(fragment)
|
||||
raise 'ZipError expected'
|
||||
rescue ::Zip::Error
|
||||
end
|
||||
|
||||
def test_write_to_stream
|
||||
entries = [
|
||||
::Zip::Entry.new(
|
||||
'file.zip', 'flimse',
|
||||
comment: 'myComment', extra: 'somethingExtra'
|
||||
),
|
||||
entries = [::Zip::Entry.new('file.zip', 'flimse', 'myComment', 'somethingExtra'),
|
||||
::Zip::Entry.new('file.zip', 'secondEntryName'),
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt', comment: 'Has a comment')
|
||||
]
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt', 'Has a comment too')]
|
||||
|
||||
cdir = ::Zip::CentralDirectory.new(entries, 'my zip comment')
|
||||
File.open('test/data/generated/cdirtest.bin', 'wb') do |f|
|
||||
|
@ -87,60 +55,50 @@ class ZipCentralDirectoryTest < MiniTest::Test
|
|||
assert_equal(cdir.entries.sort, cdir_readback.entries.sort)
|
||||
end
|
||||
|
||||
def test_write64_to_stream_65536_entries
|
||||
skip unless ENV['FULL_ZIP64_TEST']
|
||||
|
||||
entries = []
|
||||
0x10000.times do |i|
|
||||
entries << Zip::Entry.new('file.zip', "#{i}.txt")
|
||||
def test_write64_to_stream
|
||||
::Zip.write_zip64_support = true
|
||||
entries = [::Zip::Entry.new('file.zip', 'file1-little', 'comment1', '', 200, 101, ::Zip::Entry::STORED, 200),
|
||||
::Zip::Entry.new('file.zip', 'file2-big', 'comment2', '', 18_000_000_000, 102, ::Zip::Entry::DEFLATED, 20_000_000_000),
|
||||
::Zip::Entry.new('file.zip', 'file3-alsobig', 'comment3', '', 15_000_000_000, 103, ::Zip::Entry::DEFLATED, 21_000_000_000),
|
||||
::Zip::Entry.new('file.zip', 'file4-little', 'comment4', '', 100, 104, ::Zip::Entry::DEFLATED, 121)]
|
||||
[0, 250, 18_000_000_300, 33_000_000_350].each_with_index do |offset, index|
|
||||
entries[index].local_header_offset = offset
|
||||
end
|
||||
|
||||
cdir = Zip::CentralDirectory.new(entries)
|
||||
cdir = ::Zip::CentralDirectory.new(entries, 'zip comment')
|
||||
File.open('test/data/generated/cdir64test.bin', 'wb') do |f|
|
||||
cdir.write_to_stream(f)
|
||||
end
|
||||
|
||||
cdir_readback = Zip::CentralDirectory.new
|
||||
cdir_readback = ::Zip::CentralDirectory.new
|
||||
File.open('test/data/generated/cdir64test.bin', 'rb') do |f|
|
||||
cdir_readback.read_from_stream(f)
|
||||
end
|
||||
|
||||
assert_equal(0x10000, cdir_readback.size)
|
||||
assert_equal(Zip::VERSION_NEEDED_TO_EXTRACT_ZIP64, cdir_readback.instance_variable_get(:@version_needed_for_extract))
|
||||
assert_equal(cdir.entries.sort, cdir_readback.entries.sort)
|
||||
assert_equal(::Zip::VERSION_NEEDED_TO_EXTRACT_ZIP64, cdir_readback.instance_variable_get(:@version_needed_for_extract))
|
||||
end
|
||||
|
||||
def test_equality
|
||||
cdir1 = ::Zip::CentralDirectory.new(
|
||||
[
|
||||
::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
|
||||
cdir1 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
|
||||
'somethingExtra'),
|
||||
::Zip::Entry.new('file.zip', 'secondEntryName'),
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt')
|
||||
],
|
||||
'my zip comment'
|
||||
)
|
||||
cdir2 = ::Zip::CentralDirectory.new(
|
||||
[
|
||||
::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt')],
|
||||
'my zip comment')
|
||||
cdir2 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
|
||||
'somethingExtra'),
|
||||
::Zip::Entry.new('file.zip', 'secondEntryName'),
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt')
|
||||
],
|
||||
'my zip comment'
|
||||
)
|
||||
cdir3 = ::Zip::CentralDirectory.new(
|
||||
[
|
||||
::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt')],
|
||||
'my zip comment')
|
||||
cdir3 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
|
||||
'somethingExtra'),
|
||||
::Zip::Entry.new('file.zip', 'secondEntryName'),
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt')
|
||||
],
|
||||
'comment?'
|
||||
)
|
||||
cdir4 = ::Zip::CentralDirectory.new(
|
||||
[
|
||||
::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt')
|
||||
],
|
||||
'comment?'
|
||||
)
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt')],
|
||||
'comment?')
|
||||
cdir4 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
|
||||
'somethingExtra'),
|
||||
::Zip::Entry.new('file.zip', 'lastEntry.txt')],
|
||||
'comment?')
|
||||
assert_equal(cdir1, cdir1)
|
||||
assert_equal(cdir1, cdir2)
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ConstantsTest < MiniTest::Test
|
||||
def test_version_number_format
|
||||
# Should at least start with MAJoR.MINOR.PATCH, but
|
||||
# allow for additional version information after that.
|
||||
assert_match(/\A\d+\.\d+\.\d+/, Zip::VERSION)
|
||||
end
|
||||
|
||||
def test_compression_methods
|
||||
assert_equal(0, Zip::COMPRESSION_METHOD_STORE)
|
||||
assert_equal(1, Zip::COMPRESSION_METHOD_SHRINK)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class NullEncrypterTest < MiniTest::Test
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class TraditionalEncrypterTest < MiniTest::Test
|
||||
|
|
|
@ -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.
Binary file not shown.
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
class NotZippedRuby
|
||||
def return_true
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
class DecompressorTest < MiniTest::Test
|
||||
TEST_COMPRESSION_METHOD = 255
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class DeflaterTest < MiniTest::Test
|
||||
|
@ -10,10 +8,6 @@ class DeflaterTest < MiniTest::Test
|
|||
DEFAULT_COMP_FILE = 'test/data/generated/compressiontest_default_compression.bin'
|
||||
NO_COMP_FILE = 'test/data/generated/compressiontest_no_compression.bin'
|
||||
|
||||
def teardown
|
||||
Zip.reset!
|
||||
end
|
||||
|
||||
def test_output_operator
|
||||
txt = load_file('test/data/file2.txt')
|
||||
deflate(txt, DEFLATER_TEST_FILE)
|
||||
|
@ -49,7 +43,7 @@ class DeflaterTest < MiniTest::Test
|
|||
private
|
||||
|
||||
def load_file(filename)
|
||||
File.binread(filename)
|
||||
File.open(filename, 'rb', &:read)
|
||||
end
|
||||
|
||||
def deflate(data, filename)
|
||||
|
|
|
@ -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
|
|
@ -1,68 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class EncryptionTest < MiniTest::Test
|
||||
ENCRYPT_ZIP_TEST_FILE = 'test/data/zipWithEncryption.zip'
|
||||
INPUT_FILE1 = 'test/data/file1.txt'
|
||||
INPUT_FILE2 = 'test/data/file2.txt'
|
||||
|
||||
def setup
|
||||
@default_compression = Zip.default_compression
|
||||
Zip.default_compression = ::Zlib::DEFAULT_COMPRESSION
|
||||
end
|
||||
|
||||
def teardown
|
||||
Zip.reset!
|
||||
Zip.default_compression = @default_compression
|
||||
end
|
||||
|
||||
def test_encrypt
|
||||
content = File.read(INPUT_FILE1)
|
||||
test_filename = 'top_secret_file.txt'
|
||||
test_file = ::File.open(ENCRYPT_ZIP_TEST_FILE, 'rb').read
|
||||
|
||||
password = 'swordfish'
|
||||
|
||||
encrypted_zip = Zip::OutputStream.write_buffer(
|
||||
::StringIO.new,
|
||||
encrypter: Zip::TraditionalEncrypter.new(password)
|
||||
) do |out|
|
||||
out.put_next_entry(test_filename)
|
||||
out.write content
|
||||
end
|
||||
|
||||
Zip::InputStream.open(
|
||||
encrypted_zip, decrypter: Zip::TraditionalDecrypter.new(password)
|
||||
) do |zis|
|
||||
entry = zis.get_next_entry
|
||||
assert_equal test_filename, entry.name
|
||||
assert_equal 1_327, entry.size
|
||||
assert_equal content, zis.read
|
||||
end
|
||||
|
||||
error = assert_raises(Zip::DecompressionError) do
|
||||
Zip::InputStream.open(
|
||||
encrypted_zip,
|
||||
decrypter: Zip::TraditionalDecrypter.new("#{password}wrong")
|
||||
) do |zis|
|
||||
zis.get_next_entry
|
||||
assert_equal content, zis.read
|
||||
@rand = [250, 143, 107, 13, 143, 22, 155, 75, 228, 150, 12]
|
||||
@output = ::Zip::DOSTime.stub(:now, ::Zip::DOSTime.new(2014, 12, 17, 15, 56, 24)) do
|
||||
Random.stub(:rand, ->(_range) { @rand.shift }) do
|
||||
Zip::OutputStream.write_buffer(::StringIO.new, Zip::TraditionalEncrypter.new('password')) do |zos|
|
||||
zos.put_next_entry('file1.txt')
|
||||
zos.write ::File.open(INPUT_FILE1).read
|
||||
end.string
|
||||
end
|
||||
end
|
||||
assert_match(/Zlib error \('.+'\) while inflating\./, error.message)
|
||||
|
||||
@output.unpack('C*').each_with_index do |c, i|
|
||||
assert_equal test_file[i].ord, c
|
||||
end
|
||||
end
|
||||
|
||||
def test_decrypt
|
||||
Zip::InputStream.open(
|
||||
ENCRYPT_ZIP_TEST_FILE,
|
||||
decrypter: Zip::TraditionalDecrypter.new('password')
|
||||
) do |zis|
|
||||
Zip::InputStream.open(ENCRYPT_ZIP_TEST_FILE, 0, Zip::TraditionalDecrypter.new('password')) do |zis|
|
||||
entry = zis.get_next_entry
|
||||
assert_equal 'file1.txt', entry.name
|
||||
assert_equal 1_327, entry.size
|
||||
assert_equal ::File.read(INPUT_FILE1), zis.read
|
||||
entry = zis.get_next_entry
|
||||
assert_equal 'file2.txt', entry.name
|
||||
assert_equal 41_234, entry.size
|
||||
assert_equal ::File.read(INPUT_FILE2), zis.read
|
||||
assert_equal 1327, entry.size
|
||||
assert_equal ::File.open(INPUT_FILE1, 'r').read, zis.read
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ZipEntrySetTest < MiniTest::Test
|
||||
ZIP_ENTRIES = [
|
||||
::Zip::Entry.new('zipfile.zip', 'name1', comment: 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name3', comment: 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name2', comment: 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name4', comment: 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name5', comment: 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name6', comment: 'comment1')
|
||||
].freeze
|
||||
::Zip::Entry.new('zipfile.zip', 'name1', 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name3', 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name2', 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name4', 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name5', 'comment1'),
|
||||
::Zip::Entry.new('zipfile.zip', 'name6', 'comment1')
|
||||
]
|
||||
|
||||
def setup
|
||||
@zip_entry_set = ::Zip::EntrySet.new(ZIP_ENTRIES)
|
||||
|
@ -22,17 +20,13 @@ class ZipEntrySetTest < MiniTest::Test
|
|||
|
||||
def test_include
|
||||
assert(@zip_entry_set.include?(ZIP_ENTRIES.first))
|
||||
assert(
|
||||
!@zip_entry_set.include?(
|
||||
::Zip::Entry.new('different.zip', 'different', comment: 'aComment')
|
||||
)
|
||||
)
|
||||
assert(!@zip_entry_set.include?(::Zip::Entry.new('different.zip', 'different', 'aComment')))
|
||||
end
|
||||
|
||||
def test_size
|
||||
assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size)
|
||||
assert_equal(ZIP_ENTRIES.size, @zip_entry_set.length)
|
||||
@zip_entry_set << ::Zip::Entry.new('a', 'b', comment: 'c')
|
||||
@zip_entry_set << ::Zip::Entry.new('a', 'b', 'c')
|
||||
assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.length)
|
||||
end
|
||||
|
||||
|
@ -60,19 +54,11 @@ class ZipEntrySetTest < MiniTest::Test
|
|||
def test_each
|
||||
# Used each instead each_with_index due the bug in jRuby
|
||||
count = 0
|
||||
new_size = 200
|
||||
@zip_entry_set.each do |entry|
|
||||
assert(ZIP_ENTRIES.include?(entry))
|
||||
entry.clean_up # Start from a "saved" state.
|
||||
entry.size = new_size # Check that entries can be changed in this block.
|
||||
count += 1
|
||||
end
|
||||
|
||||
assert_equal(ZIP_ENTRIES.size, count)
|
||||
@zip_entry_set.each do |entry|
|
||||
assert_equal(new_size, entry.size)
|
||||
assert(entry.dirty?) # Size was changed.
|
||||
end
|
||||
end
|
||||
|
||||
def test_entries
|
||||
|
@ -80,9 +66,7 @@ class ZipEntrySetTest < MiniTest::Test
|
|||
end
|
||||
|
||||
def test_find_entry
|
||||
entries = [
|
||||
::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', comment: 'comment1')
|
||||
]
|
||||
entries = [::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', 'comment1')]
|
||||
|
||||
::Zip.case_insensitive_match = true
|
||||
zip_entry_set = ::Zip::EntrySet.new(entries)
|
||||
|
@ -109,20 +93,10 @@ class ZipEntrySetTest < MiniTest::Test
|
|||
arr << entry
|
||||
end
|
||||
assert_equal(ZIP_ENTRIES.sort, arr)
|
||||
|
||||
# Ensure `each` above hasn't permanently altered the ordering.
|
||||
::Zip.sort_entries = false
|
||||
arr = []
|
||||
@zip_entry_set.each do |entry|
|
||||
arr << entry
|
||||
end
|
||||
assert_equal(ZIP_ENTRIES, arr)
|
||||
end
|
||||
|
||||
def test_compound
|
||||
new_entry = ::Zip::Entry.new(
|
||||
'zf.zip', 'new entry', comment: "new entry's comment"
|
||||
)
|
||||
new_entry = ::Zip::Entry.new('zf.zip', 'new entry', "new entry's comment")
|
||||
assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size)
|
||||
@zip_entry_set << new_entry
|
||||
assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.size)
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ZipEntryTest < MiniTest::Test
|
||||
include ZipEntryData
|
||||
|
||||
def teardown
|
||||
::Zip.reset!
|
||||
end
|
||||
|
||||
def test_constructor_and_getters
|
||||
entry = ::Zip::Entry.new(
|
||||
TEST_ZIPFILE, TEST_NAME,
|
||||
comment: TEST_COMMENT, extra: TEST_EXTRA,
|
||||
compressed_size: TEST_COMPRESSED_SIZE,
|
||||
crc: TEST_CRC, size: TEST_SIZE, time: TEST_TIME,
|
||||
compression_method: TEST_COMPRESSIONMETHOD,
|
||||
compression_level: TEST_COMPRESSIONLEVEL
|
||||
)
|
||||
entry = ::Zip::Entry.new(TEST_ZIPFILE,
|
||||
TEST_NAME,
|
||||
TEST_COMMENT,
|
||||
TEST_EXTRA,
|
||||
TEST_COMPRESSED_SIZE,
|
||||
TEST_CRC,
|
||||
TEST_COMPRESSIONMETHOD,
|
||||
TEST_SIZE,
|
||||
TEST_TIME)
|
||||
|
||||
assert_equal(TEST_COMMENT, entry.comment)
|
||||
assert_equal(TEST_COMPRESSED_SIZE, entry.compressed_size)
|
||||
|
@ -26,10 +21,7 @@ class ZipEntryTest < MiniTest::Test
|
|||
assert_equal(TEST_COMPRESSIONMETHOD, entry.compression_method)
|
||||
assert_equal(TEST_NAME, entry.name)
|
||||
assert_equal(TEST_SIZE, entry.size)
|
||||
|
||||
# Reverse times when testing because we need to use DOSTime#== for the
|
||||
# comparison, not Time#==.
|
||||
assert_equal(entry.time, TEST_TIME)
|
||||
assert_equal(TEST_TIME, entry.time)
|
||||
end
|
||||
|
||||
def test_is_directory_and_is_file
|
||||
|
@ -47,54 +39,30 @@ class ZipEntryTest < MiniTest::Test
|
|||
end
|
||||
|
||||
def test_equality
|
||||
entry1 = ::Zip::Entry.new(
|
||||
'file.zip', 'name',
|
||||
comment: 'isNotCompared', extra: 'something extra',
|
||||
compressed_size: 123, crc: 1234, size: 10_000
|
||||
)
|
||||
|
||||
entry2 = ::Zip::Entry.new(
|
||||
'file.zip', 'name',
|
||||
comment: 'isNotComparedXXX', extra: 'something extra',
|
||||
compressed_size: 123, crc: 1234, size: 10_000
|
||||
)
|
||||
|
||||
entry3 = ::Zip::Entry.new(
|
||||
'file.zip', 'name2',
|
||||
comment: 'isNotComparedXXX', extra: 'something extra',
|
||||
compressed_size: 123, crc: 1234, size: 10_000
|
||||
)
|
||||
|
||||
entry4 = ::Zip::Entry.new(
|
||||
'file.zip', 'name2',
|
||||
comment: 'isNotComparedXXX', extra: 'something extraXX',
|
||||
compressed_size: 123, crc: 1234, size: 10_000
|
||||
)
|
||||
|
||||
entry5 = ::Zip::Entry.new(
|
||||
'file.zip', 'name2',
|
||||
comment: 'isNotComparedXXX', extra: 'something extraXX',
|
||||
compressed_size: 12, crc: 1234, size: 10_000
|
||||
)
|
||||
|
||||
entry6 = ::Zip::Entry.new(
|
||||
'file.zip', 'name2',
|
||||
comment: 'isNotComparedXXX', extra: 'something extraXX',
|
||||
compressed_size: 12, crc: 123, size: 10_000
|
||||
)
|
||||
|
||||
entry7 = ::Zip::Entry.new(
|
||||
'file.zip', 'name2', comment: 'isNotComparedXXX',
|
||||
extra: 'something extraXX', compressed_size: 12, crc: 123, size: 10_000,
|
||||
compression_method: ::Zip::Entry::STORED
|
||||
)
|
||||
|
||||
entry8 = ::Zip::Entry.new(
|
||||
'file.zip', 'name2',
|
||||
comment: 'isNotComparedXXX', extra: 'something extraXX',
|
||||
compressed_size: 12, crc: 123, size: 100_000,
|
||||
compression_method: ::Zip::Entry::STORED
|
||||
)
|
||||
entry1 = ::Zip::Entry.new('file.zip', 'name', 'isNotCompared',
|
||||
'something extra', 123, 1234,
|
||||
::Zip::Entry::DEFLATED, 10_000)
|
||||
entry2 = ::Zip::Entry.new('file.zip', 'name', 'isNotComparedXXX',
|
||||
'something extra', 123, 1234,
|
||||
::Zip::Entry::DEFLATED, 10_000)
|
||||
entry3 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
|
||||
'something extra', 123, 1234,
|
||||
::Zip::Entry::DEFLATED, 10_000)
|
||||
entry4 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
|
||||
'something extraXX', 123, 1234,
|
||||
::Zip::Entry::DEFLATED, 10_000)
|
||||
entry5 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
|
||||
'something extraXX', 12, 1234,
|
||||
::Zip::Entry::DEFLATED, 10_000)
|
||||
entry6 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
|
||||
'something extraXX', 12, 123,
|
||||
::Zip::Entry::DEFLATED, 10_000)
|
||||
entry7 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
|
||||
'something extraXX', 12, 123,
|
||||
::Zip::Entry::STORED, 10_000)
|
||||
entry8 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
|
||||
'something extraXX', 12, 123,
|
||||
::Zip::Entry::STORED, 100_000)
|
||||
|
||||
assert_equal(entry1, entry1)
|
||||
assert_equal(entry1, entry2)
|
||||
|
@ -150,53 +118,39 @@ class ZipEntryTest < MiniTest::Test
|
|||
end
|
||||
|
||||
def test_entry_name_cannot_start_with_slash
|
||||
error = assert_raises(::Zip::EntryNameError) do
|
||||
::Zip::Entry.new('zf.zip', '/hej/der')
|
||||
end
|
||||
assert_match(/'\/hej\/der'/, error.message)
|
||||
end
|
||||
|
||||
def test_entry_name_cannot_be_too_long
|
||||
name = 'a' * 65_535
|
||||
::Zip::Entry.new('', name) # Should not raise anything.
|
||||
|
||||
error = assert_raises(::Zip::EntryNameError) do
|
||||
::Zip::Entry.new('', "a#{name}")
|
||||
end
|
||||
assert_match(/65,536/, error.message)
|
||||
assert_raises(::Zip::EntryNameError) { ::Zip::Entry.new('zf.zip', '/hej/der') }
|
||||
end
|
||||
|
||||
def test_store_file_without_compression
|
||||
Dir.mktmpdir do |tmp|
|
||||
tmp_zip = File.join(tmp, 'no_compress.zip')
|
||||
File.delete('/tmp/no_compress.zip') if File.exist?('/tmp/no_compress.zip')
|
||||
files = Dir[File.join('test/data/globTest', '**', '**')]
|
||||
|
||||
Zip.setup do |z|
|
||||
z.write_zip64_support = false
|
||||
end
|
||||
|
||||
zipfile = Zip::File.open(tmp_zip, create: true)
|
||||
|
||||
mimetype_entry = Zip::Entry.new(
|
||||
zipfile, # @zipfile
|
||||
zipfile = Zip::File.open('/tmp/no_compress.zip', Zip::File::CREATE)
|
||||
mimetype_entry = Zip::Entry.new(zipfile, # @zipfile
|
||||
'mimetype', # @name
|
||||
compression_method: Zip::Entry::STORED
|
||||
)
|
||||
'', # @comment
|
||||
'', # @extra
|
||||
0, # @compressed_size
|
||||
0, # @crc
|
||||
Zip::Entry::STORED) # @comppressed_method
|
||||
|
||||
zipfile.add(mimetype_entry, 'test/data/mimetype')
|
||||
|
||||
files = Dir[File.join('test/data/globTest', '**', '**')]
|
||||
files.each do |file|
|
||||
zipfile.add(file.sub('test/data/globTest/', ''), file)
|
||||
end
|
||||
|
||||
zipfile.close
|
||||
|
||||
f = File.open(tmp_zip, 'rb')
|
||||
f = File.open('/tmp/no_compress.zip', 'rb')
|
||||
first_100_bytes = f.read(100)
|
||||
f.close
|
||||
|
||||
assert_match(/mimetypeapplication\/epub\+zip/, first_100_bytes)
|
||||
end
|
||||
end
|
||||
|
||||
def test_encrypted?
|
||||
entry = Zip::Entry.new
|
||||
|
@ -215,137 +169,4 @@ class ZipEntryTest < MiniTest::Test
|
|||
entry.gp_flags = 0
|
||||
assert_equal(false, entry.incomplete?)
|
||||
end
|
||||
|
||||
def test_compression_level_flags
|
||||
[
|
||||
[Zip.default_compression, 0],
|
||||
[0, 0],
|
||||
[1, 6],
|
||||
[2, 4],
|
||||
[3, 0],
|
||||
[7, 0],
|
||||
[8, 2],
|
||||
[9, 2]
|
||||
].each do |level, flags|
|
||||
# Check flags are set correctly when DEFLATED is (implicitly) specified.
|
||||
e_def = Zip::Entry.new(
|
||||
'', '',
|
||||
compression_level: level
|
||||
)
|
||||
assert_equal(flags, e_def.gp_flags & 0b110)
|
||||
|
||||
# Check that flags are not set when STORED is specified.
|
||||
e_sto = Zip::Entry.new(
|
||||
'', '',
|
||||
compression_method: Zip::Entry::STORED,
|
||||
compression_level: level
|
||||
)
|
||||
assert_equal(0, e_sto.gp_flags & 0b110)
|
||||
end
|
||||
|
||||
# Check that a directory entry's flags are not set, even if DEFLATED
|
||||
# is specified.
|
||||
e_dir = Zip::Entry.new(
|
||||
'', 'd/', compression_method: Zip::Entry::DEFLATED, compression_level: 1
|
||||
)
|
||||
assert_equal(0, e_dir.gp_flags & 0b110)
|
||||
end
|
||||
|
||||
def test_compression_method_reader
|
||||
[
|
||||
[Zip.default_compression, Zip::Entry::DEFLATED],
|
||||
[0, Zip::Entry::STORED],
|
||||
[1, Zip::Entry::DEFLATED],
|
||||
[9, Zip::Entry::DEFLATED]
|
||||
].each do |level, method|
|
||||
# Check that the correct method is returned when DEFLATED is specified.
|
||||
entry = Zip::Entry.new(compression_level: level)
|
||||
assert_equal(method, entry.compression_method)
|
||||
end
|
||||
|
||||
# Check that the correct method is returned when STORED is specified.
|
||||
entry = Zip::Entry.new(
|
||||
compression_method: Zip::Entry::STORED, compression_level: 1
|
||||
)
|
||||
assert_equal(Zip::Entry::STORED, entry.compression_method)
|
||||
|
||||
# Check that directories are always STORED, whatever level is specified.
|
||||
entry = Zip::Entry.new(
|
||||
'', 'd/', compression_method: Zip::Entry::DEFLATED, compression_level: 1
|
||||
)
|
||||
assert_equal(Zip::Entry::STORED, entry.compression_method)
|
||||
end
|
||||
|
||||
def test_set_time_as_dos_time
|
||||
entry = ::Zip::Entry.new
|
||||
assert(entry.time.kind_of?(::Zip::DOSTime))
|
||||
entry.time = Time.now
|
||||
assert(entry.time.kind_of?(::Zip::DOSTime))
|
||||
entry.time = ::Zip::DOSTime.now
|
||||
assert(entry.time.kind_of?(::Zip::DOSTime))
|
||||
end
|
||||
|
||||
def test_atime
|
||||
entry = ::Zip::Entry.new
|
||||
time = Time.new(1999, 12, 31, 23, 59, 59)
|
||||
|
||||
entry.atime = time
|
||||
assert(entry.dirty?)
|
||||
assert_equal(::Zip::DOSTime.from_time(time), entry.atime)
|
||||
refute_equal(entry.time, entry.atime)
|
||||
assert(entry.atime.kind_of?(::Zip::DOSTime))
|
||||
assert_nil(entry.ctime)
|
||||
end
|
||||
|
||||
def test_ctime
|
||||
entry = ::Zip::Entry.new
|
||||
time = Time.new(1999, 12, 31, 23, 59, 59)
|
||||
|
||||
entry.ctime = time
|
||||
assert(entry.dirty?)
|
||||
assert_equal(::Zip::DOSTime.from_time(time), entry.ctime)
|
||||
refute_equal(entry.time, entry.ctime)
|
||||
assert(entry.ctime.kind_of?(::Zip::DOSTime))
|
||||
assert_nil(entry.atime)
|
||||
end
|
||||
|
||||
def test_mtime
|
||||
entry = ::Zip::Entry.new
|
||||
time = Time.new(1999, 12, 31, 23, 59, 59)
|
||||
|
||||
entry.mtime = time
|
||||
assert(entry.dirty?)
|
||||
assert_equal(::Zip::DOSTime.from_time(time), entry.mtime)
|
||||
assert_equal(entry.time, entry.mtime)
|
||||
assert(entry.mtime.kind_of?(::Zip::DOSTime))
|
||||
assert_nil(entry.atime)
|
||||
assert_nil(entry.ctime)
|
||||
end
|
||||
|
||||
def test_time
|
||||
entry = ::Zip::Entry.new
|
||||
time = Time.new(1999, 12, 31, 23, 59, 59)
|
||||
|
||||
entry.time = time
|
||||
assert(entry.dirty?)
|
||||
assert_equal(::Zip::DOSTime.from_time(time), entry.time)
|
||||
assert_equal(entry.mtime, entry.time)
|
||||
assert(entry.time.kind_of?(::Zip::DOSTime))
|
||||
assert_nil(entry.atime)
|
||||
assert_nil(entry.ctime)
|
||||
end
|
||||
|
||||
def test_ensure_entry_time_set_to_file_mtime
|
||||
entry = ::Zip::Entry.new
|
||||
entry.gather_fileinfo_from_srcpath('test/data/mimetype')
|
||||
assert_equal(entry.time, File.stat('test/data/mimetype').mtime)
|
||||
end
|
||||
|
||||
def test_absolute_time
|
||||
entry = ::Zip::Entry.new
|
||||
refute(entry.absolute_time?)
|
||||
|
||||
entry.time = Time.now
|
||||
assert(entry.absolute_time?)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -1,56 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ZipExtraFieldTest < MiniTest::Test
|
||||
def test_new
|
||||
extra_pure = ::Zip::ExtraField.new('')
|
||||
extra_withstr = ::Zip::ExtraField.new('foo')
|
||||
extra_withstr_local = ::Zip::ExtraField.new('foo', local: true)
|
||||
|
||||
assert_instance_of(::Zip::ExtraField, extra_pure)
|
||||
assert_instance_of(::Zip::ExtraField, extra_withstr)
|
||||
assert_instance_of(::Zip::ExtraField, extra_withstr_local)
|
||||
|
||||
assert_equal('foo', extra_withstr['Unknown'].to_c_dir_bin)
|
||||
assert_equal('foo', extra_withstr_local['Unknown'].to_local_bin)
|
||||
end
|
||||
|
||||
def test_unknownfield
|
||||
extra = ::Zip::ExtraField.new('foo')
|
||||
assert_equal('foo', extra['Unknown'].to_c_dir_bin)
|
||||
|
||||
assert_equal(extra['Unknown'], 'foo')
|
||||
extra.merge('a')
|
||||
assert_equal('fooa', extra['Unknown'].to_c_dir_bin)
|
||||
|
||||
assert_equal(extra['Unknown'], 'fooa')
|
||||
extra.merge('barbaz')
|
||||
assert_equal('fooabarbaz', extra['Unknown'].to_c_dir_bin)
|
||||
|
||||
extra.merge('bar', local: true)
|
||||
assert_equal('bar', extra['Unknown'].to_local_bin)
|
||||
assert_equal('fooabarbaz', extra['Unknown'].to_c_dir_bin)
|
||||
end
|
||||
|
||||
def test_bad_header_id
|
||||
str = "ut\x5\0\x3\250$\r@"
|
||||
ut = nil
|
||||
assert_output('', /WARNING/) do
|
||||
ut = ::Zip::ExtraField::UniversalTime.new(str)
|
||||
end
|
||||
assert_instance_of(::Zip::ExtraField::UniversalTime, ut)
|
||||
assert_nil(ut.mtime)
|
||||
assert_equal(extra.to_s, 'fooabarbaz')
|
||||
end
|
||||
|
||||
def test_ntfs
|
||||
str = +"\x0A\x00 \x00\x00\x00\x00\x00\x01\x00\x18\x00\xC0\x81\x17\xE8B\xCE\xCF\x01\xC0\x81\x17\xE8B\xCE\xCF\x01\xC0\x81\x17\xE8B\xCE\xCF\x01"
|
||||
str = "\x0A\x00 \x00\x00\x00\x00\x00\x01\x00\x18\x00\xC0\x81\x17\xE8B\xCE\xCF\x01\xC0\x81\x17\xE8B\xCE\xCF\x01\xC0\x81\x17\xE8B\xCE\xCF\x01"
|
||||
extra = ::Zip::ExtraField.new(str)
|
||||
assert(extra.member?('NTFS'))
|
||||
t = ::Zip::DOSTime.at(1_410_496_497.405178)
|
||||
assert_equal(t, extra['NTFS'].mtime)
|
||||
assert_equal(t, extra['NTFS'].atime)
|
||||
assert_equal(t, extra['NTFS'].ctime)
|
||||
|
||||
assert_equal(str.force_encoding('BINARY'), extra.to_local_bin)
|
||||
end
|
||||
|
||||
def test_merge
|
||||
|
@ -78,9 +52,9 @@ class ZipExtraFieldTest < MiniTest::Test
|
|||
extra = ::Zip::ExtraField.new(str)
|
||||
assert_instance_of(String, extra.to_s)
|
||||
|
||||
extra_len = extra.to_s.length
|
||||
extra.merge('foo', local: true)
|
||||
assert_equal(extra_len + 3, extra.to_s.length)
|
||||
s = extra.to_s
|
||||
extra.merge('foo')
|
||||
assert_equal(s.length + 3, extra.to_s.length)
|
||||
end
|
||||
|
||||
def test_equality
|
||||
|
@ -99,26 +73,4 @@ class ZipExtraFieldTest < MiniTest::Test
|
|||
extra1.create('IUnix')
|
||||
assert_equal(extra1, extra3)
|
||||
end
|
||||
|
||||
def test_read_local_extra_field
|
||||
::Zip::File.open('test/data/local_extra_field.zip') do |zf|
|
||||
['file1.txt', 'file2.txt'].each do |file|
|
||||
entry = zf.get_entry(file)
|
||||
|
||||
assert_instance_of(::Zip::ExtraField, entry.extra)
|
||||
assert_equal(1_000, entry.extra['IUnix'].uid)
|
||||
assert_equal(1_000, entry.extra['IUnix'].gid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_load_unknown_extra_field
|
||||
::Zip::File.open('test/data/osx-archive.zip') do |zf|
|
||||
zf.each do |entry|
|
||||
# Check that there is only one occurance of the 'ux' extra field.
|
||||
assert_equal(0, entry.extra['Unknown'].to_c_dir_bin.rindex('ux'))
|
||||
assert_equal(0, entry.extra['Unknown'].to_local_bin.rindex('ux'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ZipExtraFieldUTTest < MiniTest::Test
|
||||
|
@ -11,7 +9,7 @@ class ZipExtraFieldUTTest < MiniTest::Test
|
|||
["UT\x09\x00\x05PS>APS>A", 0b101, true, false, false],
|
||||
["UT\x09\x00\x06PS>APS>A", 0b110, false, false, true],
|
||||
["UT\x13\x00\x07PS>APS>APS>A", 0b111, false, false, false]
|
||||
].freeze
|
||||
]
|
||||
|
||||
def test_parse
|
||||
PARSE_TESTS.each do |bin, flags, a, c, m|
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ZipFileExtractDirectoryTest < MiniTest::Test
|
||||
|
@ -22,7 +20,7 @@ class ZipFileExtractDirectoryTest < MiniTest::Test
|
|||
super
|
||||
|
||||
Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME
|
||||
FileUtils.rm_f(TEST_OUT_NAME)
|
||||
File.delete(TEST_OUT_NAME) if File.exist? TEST_OUT_NAME
|
||||
end
|
||||
|
||||
def test_extract_directory
|
||||
|
@ -38,9 +36,7 @@ class ZipFileExtractDirectoryTest < MiniTest::Test
|
|||
|
||||
def test_extract_directory_exists_as_file
|
||||
File.open(TEST_OUT_NAME, 'w') { |f| f.puts 'something' }
|
||||
assert_raises(::Zip::DestinationExistsError) do
|
||||
extract_test_dir
|
||||
end
|
||||
assert_raises(::Zip::DestinationFileExistsError) { extract_test_dir }
|
||||
end
|
||||
|
||||
def test_extract_directory_exists_as_file_overwrite
|
||||
|
@ -48,7 +44,7 @@ class ZipFileExtractDirectoryTest < MiniTest::Test
|
|||
called = false
|
||||
extract_test_dir do |entry, dest_path|
|
||||
called = true
|
||||
assert_equal(File.absolute_path(TEST_OUT_NAME), dest_path)
|
||||
assert_equal(TEST_OUT_NAME, dest_path)
|
||||
assert(entry.directory?)
|
||||
true
|
||||
end
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ZipFileExtractTest < MiniTest::Test
|
||||
include CommonZipFileFixture
|
||||
EXTRACTED_FILENAME = 'test/data/generated/extEntry'
|
||||
EXTRACTED_FILENAME_ABS = ::File.absolute_path(EXTRACTED_FILENAME)
|
||||
ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entry_names.reverse
|
||||
|
||||
def setup
|
||||
|
@ -38,14 +35,13 @@ class ZipFileExtractTest < MiniTest::Test
|
|||
|
||||
def test_extract_exists
|
||||
text = 'written text'
|
||||
::File.write(EXTRACTED_FILENAME, text)
|
||||
::File.open(EXTRACTED_FILENAME, 'w') { |f| f.write(text) }
|
||||
|
||||
assert_raises(::Zip::DestinationExistsError) do
|
||||
assert_raises(::Zip::DestinationFileExistsError) do
|
||||
::Zip::File.open(TEST_ZIP.zip_name) do |zf|
|
||||
zf.extract(zf.entries.first, EXTRACTED_FILENAME)
|
||||
end
|
||||
end
|
||||
|
||||
File.open(EXTRACTED_FILENAME, 'r') do |f|
|
||||
assert_equal(text, f.read)
|
||||
end
|
||||
|
@ -53,13 +49,13 @@ class ZipFileExtractTest < MiniTest::Test
|
|||
|
||||
def test_extract_exists_overwrite
|
||||
text = 'written text'
|
||||
::File.write(EXTRACTED_FILENAME, text)
|
||||
::File.open(EXTRACTED_FILENAME, 'w') { |f| f.write(text) }
|
||||
|
||||
called_correctly = false
|
||||
::Zip::File.open(TEST_ZIP.zip_name) do |zf|
|
||||
zf.extract(zf.entries.first, EXTRACTED_FILENAME) do |entry, extract_loc|
|
||||
called_correctly = zf.entries.first == entry &&
|
||||
extract_loc == EXTRACTED_FILENAME_ABS
|
||||
extract_loc == EXTRACTED_FILENAME
|
||||
true
|
||||
end
|
||||
end
|
||||
|
@ -77,7 +73,7 @@ class ZipFileExtractTest < MiniTest::Test
|
|||
zf.close if zf
|
||||
end
|
||||
|
||||
def test_extract_another_non_entry
|
||||
def test_extract_non_entry_2
|
||||
out_file = 'outfile'
|
||||
assert_raises(Errno::ENOENT) do
|
||||
zf = ::Zip::File.new(TEST_ZIP.zip_name)
|
||||
|
@ -90,8 +86,6 @@ class ZipFileExtractTest < MiniTest::Test
|
|||
end
|
||||
|
||||
def test_extract_incorrect_size
|
||||
Zip.write_zip64_support = false
|
||||
|
||||
# The uncompressed size fields in the zip file cannot be trusted. This makes
|
||||
# it harder for callers to validate the sizes of the files they are
|
||||
# extracting, which can lead to denial of service. See also
|
||||
|
@ -103,7 +97,7 @@ class ZipFileExtractTest < MiniTest::Test
|
|||
true_size = 500_000
|
||||
fake_size = 1
|
||||
|
||||
::Zip::File.open(real_zip, create: true) do |zf|
|
||||
::Zip::File.open(real_zip, ::Zip::File::CREATE) do |zf|
|
||||
zf.get_output_stream(file_name) do |os|
|
||||
os.write 'a' * true_size
|
||||
end
|
||||
|
@ -116,14 +110,16 @@ class ZipFileExtractTest < MiniTest::Test
|
|||
assert_equal true_size, a_entry.size
|
||||
end
|
||||
|
||||
true_size_bytes = [compressed_size, true_size, file_name.size].pack('VVv')
|
||||
fake_size_bytes = [compressed_size, fake_size, file_name.size].pack('VVv')
|
||||
true_size_bytes = [compressed_size, true_size, file_name.size].pack('LLS')
|
||||
fake_size_bytes = [compressed_size, fake_size, file_name.size].pack('LLS')
|
||||
|
||||
data = File.binread(real_zip)
|
||||
assert data.include?(true_size_bytes)
|
||||
data.gsub! true_size_bytes, fake_size_bytes
|
||||
|
||||
File.binwrite(fake_zip, data)
|
||||
File.open(fake_zip, 'wb') do |file|
|
||||
file.write data
|
||||
end
|
||||
|
||||
Dir.chdir tmp do
|
||||
::Zip::File.open(fake_zip) do |zf|
|
||||
|
@ -131,7 +127,7 @@ class ZipFileExtractTest < MiniTest::Test
|
|||
assert_equal fake_size, a_entry.size
|
||||
|
||||
::Zip.validate_entry_sizes = false
|
||||
assert_output('', /.+'a'.+1B.+/) do
|
||||
assert_output('', /.+\'a\'.+1B.+/) do
|
||||
a_entry.extract
|
||||
end
|
||||
assert_equal true_size, File.size(file_name)
|
||||
|
@ -141,72 +137,9 @@ class ZipFileExtractTest < MiniTest::Test
|
|||
error = assert_raises ::Zip::EntrySizeError do
|
||||
a_entry.extract
|
||||
end
|
||||
assert_equal(
|
||||
"Entry 'a' should be 1B, but is larger when inflated.",
|
||||
assert_equal \
|
||||
"entry 'a' should be 1B, but is larger when inflated.",
|
||||
error.message
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_extract_incorrect_size_zip64
|
||||
# The uncompressed size fields in the zip file cannot be trusted. This makes
|
||||
# it harder for callers to validate the sizes of the files they are
|
||||
# extracting, which can lead to denial of service. See also
|
||||
# https://en.wikipedia.org/wiki/Zip_bomb
|
||||
#
|
||||
# This version of the test ensures that fraudulent sizes in the ZIP64
|
||||
# extensions are caught.
|
||||
Dir.mktmpdir do |tmp|
|
||||
real_zip = File.join(tmp, 'real.zip')
|
||||
fake_zip = File.join(tmp, 'fake.zip')
|
||||
file_name = 'a'
|
||||
true_size = 500_000
|
||||
fake_size = 1
|
||||
|
||||
::Zip::File.open(real_zip, create: true) do |zf|
|
||||
zf.get_output_stream(file_name) do |os|
|
||||
os.write 'a' * true_size
|
||||
end
|
||||
end
|
||||
|
||||
compressed_size = nil
|
||||
::Zip::File.open(real_zip) do |zf|
|
||||
a_entry = zf.find_entry(file_name)
|
||||
compressed_size = a_entry.compressed_size
|
||||
assert_equal true_size, a_entry.size
|
||||
end
|
||||
|
||||
true_size_bytes = [0x1, 16, true_size, compressed_size].pack('vvQ<Q<')
|
||||
fake_size_bytes = [0x1, 16, fake_size, compressed_size].pack('vvQ<Q<')
|
||||
|
||||
data = File.binread(real_zip)
|
||||
assert data.include?(true_size_bytes)
|
||||
data.gsub! true_size_bytes, fake_size_bytes
|
||||
|
||||
File.binwrite(fake_zip, data)
|
||||
|
||||
Dir.chdir tmp do
|
||||
::Zip::File.open(fake_zip) do |zf|
|
||||
a_entry = zf.find_entry(file_name)
|
||||
assert_equal fake_size, a_entry.size
|
||||
|
||||
::Zip.validate_entry_sizes = false
|
||||
assert_output('', /.+'a'.+1B.+/) do
|
||||
a_entry.extract
|
||||
end
|
||||
assert_equal true_size, File.size(file_name)
|
||||
FileUtils.rm file_name
|
||||
|
||||
::Zip.validate_entry_sizes = true
|
||||
error = assert_raises ::Zip::EntrySizeError do
|
||||
a_entry.extract
|
||||
end
|
||||
assert_equal(
|
||||
"Entry 'a' should be 1B, but is larger when inflated.",
|
||||
error.message
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue