diff --git a/.rubocop.yml b/.rubocop.yml index 75dce5fd..a408fa0d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,2 +1,6 @@ inherit_from: - - .rubocop_rubyzip.yml \ No newline at end of file + - .rubocop_rubyzip.yml +AllCops: + TargetRubyVersion: 1.9 +Style/MutableConstant: + Enabled: false # Because some existent code relies on mutable constant diff --git a/.travis.yml b/.travis.yml index 12aac095..358e2a8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,32 +1,36 @@ language: ruby -sudo: false +dist: trusty cache: bundler rvm: - - 2.0.0 - - 2.1.10 - - 2.2.6 - - 2.3.3 - - 2.4.0 + - 2.0 + - 2.1 + - 2.2 + - 2.3 + - 2.4 + - 2.5 + - 2.6 - ruby-head - - rbx-2 matrix: include: - - rvm: jruby-9.1.4.0 - jdk: oraclejdk7 - - rvm: jruby-9.1.4.0 + - rvm: jruby jdk: oraclejdk8 - - rvm: jruby-9.1.4.0 + - rvm: jruby-9.1 jdk: openjdk7 - rvm: jruby-head jdk: oraclejdk8 + - rvm: rbx-4 allow_failures: - rvm: ruby-head - - rvm: rbx-2 + - rvm: rbx-4 - rvm: jruby-head before_install: - - gem update --system - - gem install bundler - gem --version before_script: - echo `whereis zip` - echo `whereis unzip` +env: + global: + - JRUBY_OPTS="--debug" + - COVERALLS_PARALLEL=true +notifications: + webhooks: https://coveralls.io/webhook diff --git a/Changelog.md b/Changelog.md index 7318fd10..36ae1009 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,281 +1,281 @@ -1.2.1 -===== - -* Add accessor to @internal_file_attributes #304 -* Extended globbing #303 -* README updates #283, #289 -* Cleanup after tests #298, #306 -* Fix permissions on new zip files #294, #300 -* Fix examples #297 -* Support cp932 encoding #308 -* Fix Directory traversal vulnerability #315 -* Allow open_buffer to work without a given block #314 +# X.X.X (Next) -1.2.0 -===== +- -* Don't enable JRuby objectspace #252 -* Fixes an exception thrown when decoding some weird .zip files #248 -* Use duck typing with IO methods #244 -* Added error for empty (zero bit) zip file #242 -* Accept StringIO in Zip.open_buffer #238 -* Do something more expected with new file permissions #237 -* Case insensitivity option for #find_entry #222 -* Fixes in documentation and examples +# 1.3.0 (2019-09-25) -1.1.7 -===== +Security -* Fix UTF-8 support for comments -* `Zip.sort_entries` working for zip output -* Prevent tempfile path from being unlinked by garbage collection -* NTFS Extra Field (0x000a) support -* Use String#tr instead of String#gsub -* Ability to not show warning about incorrect date -* Be smarter about handling buffer file modes. -* Support for Traditional Encryption (ZipCrypto) +- Add `validate_entry_sizes` option so that callers can trust an entry's reported size when using `extract` [#403](https://github.com/rubyzip/rubyzip/pull/403) + - This option defaults to `false` for backward compatibility in this release, but you are strongly encouraged to set it to `true`. It will default to `true` in rubyzip 2.0. -1.1.6 -===== +New Feature -* Revert "Return created zip file from Zip::File.open when supplied a block" +- Add `add_stored` method to simplify adding entries without compression [#366](https://github.com/rubyzip/rubyzip/pull/366) -1.1.5 -===== +Tooling / Documentation -* Treat empty file as non-exists (@layerssss) -* Revert regression commit -* Return created zip file from Zip::File.open when supplied a block (@tpickett66) -* Zip::Entry::DEFLATED is forced on every file (@mehmetc) -* Add InputStream#ungetc (@zacstewart) -* Alias for legacy error names (@orien) +- Add more gem metadata links [#402](https://github.com/rubyzip/rubyzip/pull/402) -1.1.4 -===== +# 1.2.4 (2019-09-06) -* Don't send empty string to stream (@mrloop) -* Zip::Entry::DEFLATED was forced on every file (@mehmetc) -* Alias for legacy error names (@orien) +- Do not rewrite zip files opened with `open_buffer` that have not changed [#360](https://github.com/rubyzip/rubyzip/pull/360) -1.1.3 -===== +Tooling / Documentation -* Fix compatibility of ::OutputStream::write_buffer (@orien) -* Clean up tempfiles from output stream (@iangreenleaf) +- Update `example_recursive.rb` in README [#397](https://github.com/rubyzip/rubyzip/pull/397) +- Hold CI at `trusty` for now, automatically pick the latest ruby patch version, use rbx-4 and hold jruby at 9.1 [#399](https://github.com/rubyzip/rubyzip/pull/399) -1.1.2 -===== +# 1.2.3 -* Fix compatibility of ::Zip::File.write_buffer +- Allow tilde in zip entry names [#391](https://github.com/rubyzip/rubyzip/pull/391) (fixes regression in 1.2.2 from [#376](https://github.com/rubyzip/rubyzip/pull/376)) +- Support frozen string literals in more files [#390](https://github.com/rubyzip/rubyzip/pull/390) +- Require `pathname` explicitly [#388](https://github.com/rubyzip/rubyzip/pull/388) (fixes regression in 1.2.2 from [#376](https://github.com/rubyzip/rubyzip/pull/376)) -1.1.1 -===== +Tooling / Documentation: -* Speedup deflater (@loadhigh) -* Less Arrays and Strings allocations (@srawlins) -* Fix Zip64 writting support (@mrjamesriley) -* Fix StringIO support (@simonoff) -* Posibility to change default compression level -* Make Zip64 write support optional via configuration +- CI updates [#392](https://github.com/rubyzip/rubyzip/pull/392), [#394](https://github.com/rubyzip/rubyzip/pull/394) + - Bump supported ruby versions and add 2.6 + - JRuby failures are no longer ignored (reverts [#375](https://github.com/rubyzip/rubyzip/pull/375) / part of [#371](https://github.com/rubyzip/rubyzip/pull/371)) +- Add changelog entry that was missing for last release [#387](https://github.com/rubyzip/rubyzip/pull/387) +- Comment cleanup [#385](https://github.com/rubyzip/rubyzip/pull/385) -1.1.0 -===== +# 1.2.2 -* StringIO Support -* Zip64 Support -* Better jRuby Support -* Order of files in the archive can be sorted -* Other small fixes +NB: This release drops support for extracting symlinks, because there was no clear way to support this securely. See https://github.com/rubyzip/rubyzip/pull/376#issue-210954555 for details. -1.0.0 -===== +- Fix CVE-2018-1000544 [#376](https://github.com/rubyzip/rubyzip/pull/376) / [#371](https://github.com/rubyzip/rubyzip/pull/371) +- Fix NoMethodError: undefined method `glob' [#363](https://github.com/rubyzip/rubyzip/pull/363) +- Fix handling of stored files (i.e. files not using compression) with general purpose bit 3 set [#358](https://github.com/rubyzip/rubyzip/pull/358) +- Fix `close` on StringIO-backed zip file [#353](https://github.com/rubyzip/rubyzip/pull/353) +- Add `Zip.force_entry_names_encoding` option [#340](https://github.com/rubyzip/rubyzip/pull/340) +- Update rubocop, apply auto-fixes, and fix regressions caused by said auto-fixes [#332](https://github.com/rubyzip/rubyzip/pull/332), [#355](https://github.com/rubyzip/rubyzip/pull/355) +- Save temporary files to temporary directory (rather than current directory) [#325](https://github.com/rubyzip/rubyzip/pull/325) -* Removed support for Ruby 1.8 -* Changed the API for gem. Now it can be used without require param in Gemfile. -* Added read-only support for Zip64 files. -* Added support for setting Unicode file names. +Tooling / Documentation: -0.9.9 -===== +- Turn off all terminal output in all tests [#361](https://github.com/rubyzip/rubyzip/pull/361) +- Several CI updates [#346](https://github.com/rubyzip/rubyzip/pull/346), [#347](https://github.com/rubyzip/rubyzip/pull/347), [#350](https://github.com/rubyzip/rubyzip/pull/350), [#352](https://github.com/rubyzip/rubyzip/pull/352) +- Several README improvements [#345](https://github.com/rubyzip/rubyzip/pull/345), [#326](https://github.com/rubyzip/rubyzip/pull/326), [#321](https://github.com/rubyzip/rubyzip/pull/321) -* Added support for backslashes in zip files (generated by the default Windows zip packer for example) and comment sections with the comment length set to zero even though there is actually a comment. +# 1.2.1 -0.9.8 -===== +- Add accessor to @internal_file_attributes #304 +- Extended globbing #303 +- README updates #283, #289 +- Cleanup after tests #298, #306 +- Fix permissions on new zip files #294, #300 +- Fix examples #297 +- Support cp932 encoding #308 +- Fix Directory traversal vulnerability #315 +- Allow open_buffer to work without a given block #314 -* Fixed: "Unitialized constant NullInputStream" error +# 1.2.0 -0.9.5 -===== +- Don't enable JRuby objectspace #252 +- Fixes an exception thrown when decoding some weird .zip files #248 +- Use duck typing with IO methods #244 +- Added error for empty (zero bit) zip file #242 +- Accept StringIO in Zip.open_buffer #238 +- Do something more expected with new file permissions #237 +- Case insensitivity option for #find_entry #222 +- Fixes in documentation and examples -* Removed support for loading ruby in zip files (ziprequire.rb). +# 1.1.7 -0.9.4 -===== +- Fix UTF-8 support for comments +- `Zip.sort_entries` working for zip output +- Prevent tempfile path from being unlinked by garbage collection +- NTFS Extra Field (0x000a) support +- Use String#tr instead of String#gsub +- Ability to not show warning about incorrect date +- Be smarter about handling buffer file modes. +- Support for Traditional Encryption (ZipCrypto) -* Changed ZipOutputStream.put_next_entry signature (API CHANGE!). Now allows comment, extra field and compression method to be specified. +# 1.1.6 -0.9.3 -===== +- Revert "Return created zip file from Zip::File.open when supplied a block" -* Fixed: Added ZipEntry::name_encoding which retrieves the character -encoding of the name and comment of the entry. -* Added convenience methods ZipEntry::name_in(enc) and ZipEntry::comment_in(enc) for -getting zip entry names and comments in a specified character -encoding. +# 1.1.5 -0.9.2 -===== +- Treat empty file as non-exists (@layerssss) +- Revert regression commit +- Return created zip file from Zip::File.open when supplied a block (@tpickett66) +- Zip::Entry::DEFLATED is forced on every file (@mehmetc) +- Add InputStream#ungetc (@zacstewart) +- Alias for legacy error names (@orien) -* Fixed: Renaming an entry failed if the entry's new name was a different length than its old name. (Diego Barros) +# 1.1.4 -0.9.1 -===== +- Don't send empty string to stream (@mrloop) +- Zip::Entry::DEFLATED was forced on every file (@mehmetc) +- Alias for legacy error names (@orien) -* Added symlink support and support for unix file permissions. Reduced memory usage during decompression. -* New methods ZipFile::[follow_symlinks, restore_times, restore_permissions, restore_ownership]. -* New methods ZipEntry::unix_perms, ZipInputStream::eof?. -* Added documentation and test for new ZipFile::extract. -* Added some of the API suggestions from sf.net #1281314. -* Applied patch for sf.net bug #1446926. -* Applied patch for sf.net bug #1459902. -* Rework ZipEntry and delegate classes. +# 1.1.3 -0.5.12 -====== +- Fix compatibility of ::OutputStream::write_buffer (@orien) +- Clean up tempfiles from output stream (@iangreenleaf) -* Fixed problem with writing binary content to a ZipFile in MS Windows. +# 1.1.2 -0.5.11 -====== +- Fix compatibility of ::Zip::File.write_buffer -* Fixed name clash file method copy_stream from fileutils.rb. Fixed problem with references to constant CHUNK_SIZE. -* ZipInputStream/AbstractInputStream read is now buffered like ruby IO's read method, which means that read and gets etc can be mixed. The - unbuffered read method has been renamed to sysread. +# 1.1.1 -0.5.10 -====== +- Speedup deflater (@loadhigh) +- Less Arrays and Strings allocations (@srawlins) +- Fix Zip64 writing support (@mrjamesriley) +- Fix StringIO support (@simonoff) +- Possibility to change default compression level +- Make Zip64 write support optional via configuration -* Fixed method name resolution problem with FileUtils::copy_stream and IOExtras::copy_stream. +# 1.1.0 -0.5.9 -===== +- StringIO Support +- Zip64 Support +- Better jRuby Support +- Order of files in the archive can be sorted +- Other small fixes -* Fixed serious memory consumption issue +# 1.0.0 -0.5.8 -===== +- Removed support for Ruby 1.8 +- Changed the API for gem. Now it can be used without require param in Gemfile. +- Added read-only support for Zip64 files. +- Added support for setting Unicode file names. -* Fixed install script. +# 0.9.9 -0.5.7 -===== -* install.rb no longer assumes it is being run from the toplevel source -dir. Directory structure changed to reflect common ruby library -project structure. Migrated from RubyUnit to Test::Unit format. Now -uses Rake to build source packages and gems and run unit tests. +- Added support for backslashes in zip files (generated by the default Windows zip packer for example) and comment sections with the comment length set to zero even though there is actually a comment. -0.5.6 -===== -* Fix for FreeBSD 4.9 which returns Errno::EFBIG instead of -Errno::EINVAL for some invalid seeks. Fixed 'version needed to -extract'-field incorrect in local headers. +# 0.9.8 -0.5.5 -===== +- Fixed: "Unitialized constant NullInputStream" error -* Fix for a problem with writing zip files that concerns only ruby 1.8.1. +# 0.9.5 -0.5.4 -===== +- Removed support for loading ruby in zip files (ziprequire.rb). -* Significantly reduced memory footprint when modifying zip files. +# 0.9.4 -0.5.3 -===== -* Added optimization to avoid decompressing and recompressing individual -entries when modifying a zip archive. +- Changed ZipOutputStream.put_next_entry signature (API CHANGE!). Now allows comment, extra field and compression method to be specified. -0.5.2 -===== -* Fixed ZipFile corruption bug in ZipFile class. Added basic unix -extra-field support. +# 0.9.3 -0.5.1 -===== +- Fixed: Added ZipEntry::name_encoding which retrieves the character encoding of the name and comment of the entry. +- Added convenience methods ZipEntry::name_in(enc) and ZipEntry::comment_in(enc) for getting zip entry names and comments in a specified character encoding. -* Fixed ZipFile.get_output_stream bug. +# 0.9.2 -0.5.0 -===== +- Fixed: Renaming an entry failed if the entry's new name was a different length than its old name. (Diego Barros) -* Ruby 1.8.0 and ruby-zlib 0.6.0 compatibility -* Changed method names from camelCase to rubys underscore style. -* Installs to zip/ subdir instead of directly to site_ruby -* Added ZipFile.directory and ZipFile.file - each method return an -object that can be used like Dir and File only for the contents of the -zip file. -* Added sample application zipfind which works like Find.find, only -Zip::ZipFind.find traverses into zip archives too. -* FIX: AbstractInputStream.each_line with non-default separator +# 0.9.1 +- Added symlink support and support for unix file permissions. Reduced memory usage during decompression. +- New methods ZipFile::[follow_symlinks, restore_times, restore_permissions, restore_ownership]. +- New methods ZipEntry::unix_perms, ZipInputStream::eof?. +- Added documentation and test for new ZipFile::extract. +- Added some of the API suggestions from sf.net #1281314. +- Applied patch for sf.net bug #1446926. +- Applied patch for sf.net bug #1459902. +- Rework ZipEntry and delegate classes. -0.5.0a -====== -Source reorganized. Added ziprequire, which can be used to load ruby -modules from a zip file, in a fashion similar to jar files in -Java. Added gtk_ruby_zip, another sample application. Implemented -ZipInputStream.lineno and ZipInputStream.rewind +# 0.5.12 -Bug fixes: +- Fixed problem with writing binary content to a ZipFile in MS Windows. + +# 0.5.11 + +- Fixed name clash file method copy_stream from fileutils.rb. Fixed problem with references to constant CHUNK_SIZE. +- ZipInputStream/AbstractInputStream read is now buffered like ruby IO's read method, which means that read and gets etc can be mixed. The unbuffered read method has been renamed to sysread. + +# 0.5.10 + +- Fixed method name resolution problem with FileUtils::copy_stream and IOExtras::copy_stream. + +# 0.5.9 + +- Fixed serious memory consumption issue + +# 0.5.8 + +- Fixed install script. + +# 0.5.7 + +- install.rb no longer assumes it is being run from the toplevel source dir. Directory structure changed to reflect common ruby library project structure. Migrated from RubyUnit to Test::Unit format. Now uses Rake to build source packages and gems and run unit tests. + +# 0.5.6 + +- Fix for FreeBSD 4.9 which returns Errno::EFBIG instead of Errno::EINVAL for some invalid seeks. Fixed 'version needed to extract'-field incorrect in local headers. -* Read and write date and time information correctly for zip entries. -* Fixed read() using separate buffer, causing mix of gets/readline/read to -cause problems. +# 0.5.5 -0.4.2 -===== +- Fix for a problem with writing zip files that concerns only ruby 1.8.1. -* Performance optimizations. Test suite runs in half the time. +# 0.5.4 + +- Significantly reduced memory footprint when modifying zip files. + +# 0.5.3 + +- Added optimization to avoid decompressing and recompressing individual entries when modifying a zip archive. + +# 0.5.2 + +- Fixed ZipFile corruption bug in ZipFile class. Added basic unix extra-field support. + +# 0.5.1 + +- Fixed ZipFile.get_output_stream bug. + +# 0.5.0 + +- Ruby 1.8.0 and ruby-zlib 0.6.0 compatibility +- Changed method names from camelCase to rubys underscore style. +- Installs to zip/ subdir instead of directly to site_ruby +- Added ZipFile.directory and ZipFile.file - each method return an + object that can be used like Dir and File only for the contents of the + zip file. +- Added sample application zipfind which works like Find.find, only + Zip::ZipFind.find traverses into zip archives too. +- FIX: AbstractInputStream.each_line with non-default separator + +# 0.5.0a + +Source reorganized. Added ziprequire, which can be used to load ruby modules from a zip file, in a fashion similar to jar files in Java. Added gtk_ruby_zip, another sample application. Implemented ZipInputStream.lineno and ZipInputStream.rewind + +Bug fixes: -0.4.1 -===== +- Read and write date and time information correctly for zip entries. +- Fixed read() using separate buffer, causing mix of gets/readline/read to cause problems. -* Windows compatibility fixes. +# 0.4.2 -0.4.0 -===== +- Performance optimizations. Test suite runs in half the time. -* Zip::ZipFile is now mutable and provides a more convenient way of -modifying zip archives than Zip::ZipOutputStream. Operations for -adding, extracting, renaming, replacing and removing entries to zip -archives are now available. +# 0.4.1 -* Runs without warnings with -w switch. +- Windows compatibility fixes. -* Install script install.rb added. +# 0.4.0 -0.3.1 -===== +- Zip::ZipFile is now mutable and provides a more convenient way of modifying zip archives than Zip::ZipOutputStream. Operations for adding, extracting, renaming, replacing and removing entries to zip archives are now available. +- Runs without warnings with -w switch. +- Install script install.rb added. -* Rudimentary support for writing zip archives. +# 0.3.1 -0.2.2 -===== +- Rudimentary support for writing zip archives. -* Fixed and extended unit test suite. Updated to work with ruby/zlib -0.5. It doesn't work with earlier versions of ruby/zlib. +# 0.2.2 -0.2.0 -===== +- Fixed and extended unit test suite. Updated to work with ruby/zlib 0.5. It doesn't work with earlier versions of ruby/zlib. -* Class ZipFile added. Where ZipInputStream is used to read the -individual entries in a zip file, ZipFile reads the central directory -in the zip archive, so you can get to any entry in the zip archive -without having to skipping through all the preceeding entries. +# 0.2.0 +- Class ZipFile added. Where ZipInputStream is used to read the individual entries in a zip file, ZipFile reads the central directory in the zip archive, so you can get to any entry in the zip archive without having to skipping through all the preceeding entries. -0.1.0 -===== +# 0.1.0 -* First working version of ZipInputStream. +- First working version of ZipInputStream. diff --git a/README.md b/README.md index 8979d06f..51b275b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # rubyzip + [![Gem Version](https://badge.fury.io/rb/rubyzip.svg)](http://badge.fury.io/rb/rubyzip) [![Build Status](https://secure.travis-ci.org/rubyzip/rubyzip.svg)](http://travis-ci.org/rubyzip/rubyzip) [![Code Climate](https://codeclimate.com/github/rubyzip/rubyzip.svg)](https://codeclimate.com/github/rubyzip/rubyzip) @@ -19,9 +20,10 @@ gem 'zip-zip' # will load compatibility for old rubyzip API. ## Requirements -* Ruby 1.9.2 or greater +- Ruby 1.9.2 or greater ## Installation + Rubyzip is available on RubyGems: ``` @@ -52,14 +54,15 @@ Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile| # Two arguments: # - The name of the file as it will appear in the archive # - The original file, including the path to find it - zipfile.add(filename, folder + '/' + filename) + zipfile.add(filename, File.join(folder, filename)) end - zipfile.get_output_stream("myFile") { |os| os.write "myFile contains just this" } + zipfile.get_output_stream("myFile") { |f| f.write "myFile contains just this" } end ``` ### Zipping a directory recursively -Copy from [here](https://github.com/rubyzip/rubyzip/blob/05916bf89181e1955118fd3ea059f18acac28cc8/samples/example_recursive.rb ) + +Copy from [here](https://github.com/rubyzip/rubyzip/blob/9d891f7353e66052283562d3e252fe380bb4b199/samples/example_recursive.rb) ```ruby require 'zip' @@ -83,40 +86,37 @@ class ZipFileGenerator # Zip the input directory. def write - entries = Dir.entries(@input_dir) - %w(. ..) + entries = Dir.entries(@input_dir) - %w[. ..] - ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |io| - write_entries entries, '', io + ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile| + write_entries entries, '', zipfile end end private # A helper method to make the recursion work. - def write_entries(entries, path, io) + def write_entries(entries, path, zipfile) entries.each do |e| - zip_file_path = path == '' ? e : File.join(path, e) - disk_file_path = File.join(@input_dir, zip_file_path) - puts "Deflating #{disk_file_path}" + zipfile_path = path == '' ? e : File.join(path, e) + disk_file_path = File.join(@input_dir, zipfile_path) if File.directory? disk_file_path - recursively_deflate_directory(disk_file_path, io, zip_file_path) + recursively_deflate_directory(disk_file_path, zipfile, zipfile_path) else - put_into_archive(disk_file_path, io, zip_file_path) + put_into_archive(disk_file_path, zipfile, zipfile_path) end end end - def recursively_deflate_directory(disk_file_path, io, zip_file_path) - io.mkdir zip_file_path - subdir = Dir.entries(disk_file_path) - %w(. ..) - write_entries subdir, zip_file_path, io + def recursively_deflate_directory(disk_file_path, zipfile, zipfile_path) + zipfile.mkdir zipfile_path + subdir = Dir.entries(disk_file_path) - %w[. ..] + write_entries subdir, zipfile_path, zipfile end - def put_into_archive(disk_file_path, io, zip_file_path) - io.get_output_stream(zip_file_path) do |f| - f.write(File.open(disk_file_path, 'rb').read) - end + def put_into_archive(disk_file_path, zipfile, zipfile_path) + zipfile.add(zipfile_path, disk_file_path) end end ``` @@ -152,12 +152,15 @@ When modifying a zip archive the file permissions of the archive are preserved. ### Reading a Zip file ```ruby +MAX_SIZE = 1024**2 # 1MiB (but of course you can increase this) Zip::File.open('foo.zip') do |zip_file| # Handle entries one by one zip_file.each do |entry| - # Extract to file/directory/symlink puts "Extracting #{entry.name}" - entry.extract(dest_file) + raise 'File too large when extracted' if entry.size > MAX_SIZE + + # Extract to file or directory based on name in the archive + entry.extract # Read into memory content = entry.get_input_stream.read @@ -165,6 +168,7 @@ Zip::File.open('foo.zip') do |zip_file| # Find specific entry entry = zip_file.glob('*.csv').first + raise 'File too large when extracted' if entry.size > MAX_SIZE puts entry.get_input_stream.read end ``` @@ -175,9 +179,7 @@ end 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 entry in the zip archive it will raise an exception. @@ -221,7 +223,9 @@ File.open(new_path, "wb") {|f| f.write(buffer.string) } ## Configuration -By default, rubyzip will not overwrite files if they already exist inside of the extracted path. To change this behavior, you may specify a configuration option like so: +### Existing Files + +By default, rubyzip will not overwrite files if they already exist inside of the extracted path. To change this behavior, you may specify a configuration option like so: ```ruby Zip.on_exists_proc = true @@ -235,25 +239,83 @@ Additionally, if you want to configure rubyzip to overwrite existing files while Zip.continue_on_exists_proc = true ``` +### Non-ASCII Names + If you want to store non-english names and want to open them on Windows(pre 7) you need to set this option: ```ruby Zip.unicode_names = true ``` +Sometimes file names inside zip contain non-ASCII characters. If you can assume which encoding was used for such names and want to be able to find such entries using `find_entry` then you can force assumed encoding like so: + +```ruby +Zip.force_entry_names_encoding = 'UTF-8' +``` + +Allowed encoding names are the same as accepted by `String#force_encoding` + +### Date Validation + Some zip files might have an invalid date format, which will raise a warning. You can hide this warning with the following setting: ```ruby Zip.warn_invalid_date = false ``` +### Size Validation + +**This setting defaults to `false` in rubyzip 1.3 for backward compatibility, but it will default to `true` in rubyzip 2.0.** + +If you set +``` +Zip.validate_entry_sizes = true +``` +then `rubyzip`'s `extract` method checks that an entry's reported uncompressed size is not (significantly) smaller than its actual size. This is to help you protect your application against [zip bombs](https://en.wikipedia.org/wiki/Zip_bomb). Before `extract`ing an entry, you should check that its size is in the range you expect. For example, if your application supports processing up to 100 files at once, each up to 10MiB, your zip extraction code might look like: + +```ruby +MAX_FILE_SIZE = 10 * 1024**2 # 10MiB +MAX_FILES = 100 +Zip::File.open('foo.zip') do |zip_file| + num_files = 0 + zip_file.each do |entry| + num_files += 1 if entry.file? + raise 'Too many extracted files' if num_files > MAX_FILES + raise 'File too large when extracted' if entry.size > MAX_FILE_SIZE + entry.extract + end +end +``` + +If you need to extract zip files that report incorrect uncompressed sizes and you really trust them not too be too large, you can disable this setting with +```ruby +Zip.validate_entry_sizes = false +``` + +Note that if you use the lower level `Zip::InputStream` interface, `rubyzip` does *not* check the entry `size`s. In this case, the caller is responsible for making sure it does not read more data than expected from the input stream. + +### Default Compression + You can set the default compression level like so: ```ruby Zip.default_compression = Zlib::DEFAULT_COMPRESSION ``` + It defaults to `Zlib::DEFAULT_COMPRESSION`. Possible values are `Zlib::BEST_COMPRESSION`, `Zlib::DEFAULT_COMPRESSION` and `Zlib::NO_COMPRESSION` +### Zip64 Support + +By default, Zip64 support is disabled for writing. To enable it do this: + +```ruby +Zip.write_zip64_support = true +``` + +_NOTE_: If you will enable Zip64 writing then you will need zip extractor with Zip64 support to extract archive. + +### Block Form + You can set multiple settings at the same time by using a block: ```ruby @@ -265,14 +327,6 @@ You can set multiple settings at the same time by using a block: end ``` -By default, Zip64 support is disabled for writing. To enable it do this: - -```ruby -Zip.write_zip64_support = true -``` - -_NOTE_: If you will enable Zip64 writing then you will need zip extractor with Zip64 support to extract archive. - ## Developing To run the test you need to do this: diff --git a/lib/zip.rb b/lib/zip.rb index bb44361a..eeac96a0 100644 --- a/lib/zip.rb +++ b/lib/zip.rb @@ -34,7 +34,16 @@ module Zip extend self - attr_accessor :unicode_names, :on_exists_proc, :continue_on_exists_proc, :sort_entries, :default_compression, :write_zip64_support, :warn_invalid_date, :case_insensitive_match + attr_accessor :unicode_names, + :on_exists_proc, + :continue_on_exists_proc, + :sort_entries, + :default_compression, + :write_zip64_support, + :warn_invalid_date, + :case_insensitive_match, + :force_entry_names_encoding, + :validate_entry_sizes def reset! @_ran_once = false @@ -46,6 +55,7 @@ def reset! @write_zip64_support = false @warn_invalid_date = true @case_insensitive_match = false + @validate_entry_sizes = false end def setup diff --git a/lib/zip/central_directory.rb b/lib/zip/central_directory.rb index cb7e2da7..0b6874ef 100644 --- a/lib/zip/central_directory.rb +++ b/lib/zip/central_directory.rb @@ -96,7 +96,7 @@ def read_64_e_o_c_d(buf) #:nodoc: @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.size == 0 + raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty? end def read_e_o_c_d(buf) #:nodoc: @@ -113,7 +113,7 @@ def read_e_o_c_d(buf) #:nodoc: else buf.read(comment_length) end - raise Error, 'Zip consistency problem while reading eocd structure' unless buf.size == 0 + raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty? end def read_central_directory_entries(io) #:nodoc: @@ -130,7 +130,7 @@ def read_central_directory_entries(io) #:nodoc: def read_from_stream(io) #:nodoc: buf = start_buf(io) - if self.zip64_file?(buf) + if zip64_file?(buf) read_64_e_o_c_d(buf) else read_e_o_c_d(buf) diff --git a/lib/zip/compressor.rb b/lib/zip/compressor.rb index ce2b847f..079c1cb0 100644 --- a/lib/zip/compressor.rb +++ b/lib/zip/compressor.rb @@ -1,7 +1,6 @@ module Zip class Compressor #:nodoc:all - def finish - end + def finish; end end end diff --git a/lib/zip/constants.rb b/lib/zip/constants.rb index 94f8f501..5eb5c1da 100644 --- a/lib/zip/constants.rb +++ b/lib/zip/constants.rb @@ -11,9 +11,9 @@ module Zip VERSION_NEEDED_TO_EXTRACT = 20 VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45 - FILE_TYPE_FILE = 010 - FILE_TYPE_DIR = 004 - FILE_TYPE_SYMLINK = 012 + FILE_TYPE_FILE = 0o10 + FILE_TYPE_DIR = 0o04 + FILE_TYPE_SYMLINK = 0o12 FSTYPE_FAT = 0 FSTYPE_AMIGA = 1 diff --git a/lib/zip/crypto/null_encryption.rb b/lib/zip/crypto/null_encryption.rb index 62d47f0e..a93f707c 100644 --- a/lib/zip/crypto/null_encryption.rb +++ b/lib/zip/crypto/null_encryption.rb @@ -24,8 +24,7 @@ def data_descriptor(_crc32, _compressed_size, _uncomprssed_size) '' end - def reset! - end + def reset!; end end class NullDecrypter < Decrypter @@ -35,8 +34,7 @@ def decrypt(data) data end - def reset!(_header) - end + def reset!(_header); end end end diff --git a/lib/zip/decompressor.rb b/lib/zip/decompressor.rb index cd0fb054..047ed5e7 100644 --- a/lib/zip/decompressor.rb +++ b/lib/zip/decompressor.rb @@ -1,5 +1,5 @@ module Zip - class Decompressor #:nodoc:all + class Decompressor #:nodoc:all CHUNK_SIZE = 32_768 def initialize(input_stream) super() diff --git a/lib/zip/dos_time.rb b/lib/zip/dos_time.rb index fd9353c6..bf0cb7e0 100644 --- a/lib/zip/dos_time.rb +++ b/lib/zip/dos_time.rb @@ -19,7 +19,7 @@ def to_binary_dos_time end def to_binary_dos_date - (day) + + day + (month << 5) + ((year - 1980) << 9) end diff --git a/lib/zip/entry.rb b/lib/zip/entry.rb index 0aba0eb8..677e49ef 100644 --- a/lib/zip/entry.rb +++ b/lib/zip/entry.rb @@ -1,3 +1,4 @@ +require 'pathname' module Zip class Entry STORED = 0 @@ -99,7 +100,7 @@ def file_type_is?(type) end # Dynamic checkers - %w(directory file symlink).each do |k| + %w[directory file symlink].each do |k| define_method "#{k}?" do file_type_is?(k.to_sym) end @@ -109,6 +110,17 @@ 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? + cleanpath = Pathname.new(@name).cleanpath + return false unless cleanpath.relative? + root = ::File::SEPARATOR + naive_expanded_path = ::File.join(root, cleanpath.to_s) + ::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path + end + def local_entry_offset #:nodoc:all local_header_offset + @local_header_size end @@ -147,14 +159,17 @@ def next_header_offset #:nodoc:all end # Extracts entry to file dest_path (defaults to @name). - def extract(dest_path = @name, &block) - block ||= proc { ::Zip.on_exists_proc } - - if @name.squeeze('/') =~ /\.{2}(?:\/|\z)/ - puts "WARNING: skipped \"../\" path component(s) in #{@name}" + # NB: The caller is responsible for making sure dest_path is safe, if it + # is passed. + def extract(dest_path = nil, &block) + if dest_path.nil? && !name_safe? + puts "WARNING: skipped #{@name} as unsafe" return self end + dest_path ||= @name + block ||= proc { ::Zip.on_exists_proc } + if directory? || file? || symlink? __send__("create_#{@ftype}", dest_path, &block) else @@ -239,7 +254,10 @@ def read_local_entry(io) #:nodoc:all @name = io.read(@name_length) extra = io.read(@extra_length) - @name.gsub!('\\', '/') + @name.tr!('\\', '/') + if ::Zip.force_entry_names_encoding + @name.force_encoding(::Zip.force_entry_names_encoding) + end if extra && extra.bytesize != @extra_length raise ::Zip::Error, 'Truncated local zip entry header' @@ -258,13 +276,13 @@ def pack_local_entry zip64 = @extra['Zip64'] [::Zip::LOCAL_ENTRY_SIGNATURE, @version_needed_to_extract, # version needed to extract - @gp_flags, # @gp_flags , + @gp_flags, # @gp_flags @compression_method, - @time.to_binary_dos_time, # @last_mod_time , - @time.to_binary_dos_date, # @last_mod_date , + @time.to_binary_dos_time, # @last_mod_time + @time.to_binary_dos_date, # @last_mod_date @crc, - (zip64 && zip64.compressed_size) ? 0xFFFFFFFF : @compressed_size, - (zip64 && zip64.original_size) ? 0xFFFFFFFF : @size, + zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size, + zip64 && zip64.original_size ? 0xFFFFFFFF : @size, name_size, @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv') end @@ -308,7 +326,7 @@ def unpack_c_dir_entry(buf) def set_ftype_from_c_dir_entry @ftype = case @fstype when ::Zip::FSTYPE_UNIX - @unix_perms = (@external_file_attributes >> 16) & 07777 + @unix_perms = (@external_file_attributes >> 16) & 0o7777 case (@external_file_attributes >> 28) when ::Zip::FILE_TYPE_DIR :directory @@ -364,6 +382,9 @@ def read_c_dir_entry(io) #:nodoc:all check_c_dir_entry_signature set_time(@last_mod_date, @last_mod_time) @name = io.read(@name_length) + if ::Zip.force_entry_names_encoding + @name.force_encoding(::Zip.force_entry_names_encoding) + end read_c_dir_extra_field(io) @comment = io.read(@comment_length) check_c_dir_entry_comment_size @@ -384,14 +405,14 @@ def get_extra_attributes_from_path(path) # :nodoc: stat = file_stat(path) @unix_uid = stat.uid @unix_gid = stat.gid - @unix_perms = stat.mode & 07777 + @unix_perms = stat.mode & 0o7777 end def set_unix_permissions_on_path(dest_path) # BUG: does not update timestamps into account # ignore setuid/setgid bits by default. honor if @restore_ownership - unix_perms_mask = 01777 - unix_perms_mask = 07777 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 # File::utimes() @@ -412,21 +433,21 @@ def pack_c_dir_entry @header_signature, @version, # version of encoding software @fstype, # filesystem type - @version_needed_to_extract, # @versionNeededToExtract , - @gp_flags, # @gp_flags , + @version_needed_to_extract, # @versionNeededToExtract + @gp_flags, # @gp_flags @compression_method, - @time.to_binary_dos_time, # @last_mod_time , - @time.to_binary_dos_date, # @last_mod_date , + @time.to_binary_dos_time, # @last_mod_time + @time.to_binary_dos_date, # @last_mod_date @crc, - (zip64 && zip64.compressed_size) ? 0xFFFFFFFF : @compressed_size, - (zip64 && zip64.original_size) ? 0xFFFFFFFF : @size, + zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size, + zip64 && zip64.original_size ? 0xFFFFFFFF : @size, name_size, @extra ? @extra.c_dir_size : 0, comment_size, - (zip64 && zip64.disk_start_number) ? 0xFFFF : 0, # disk number start + zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start @internal_file_attributes, # file type (binary=0, text=1) @external_file_attributes, # native filesystem attributes - (zip64 && zip64.relative_header_offset) ? 0xFFFFFFFF : @local_header_offset, + zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset, @name, @extra, @comment @@ -439,18 +460,18 @@ def write_c_dir_entry(io) #:nodoc:all when ::Zip::FSTYPE_UNIX ft = case @ftype when :file - @unix_perms ||= 0644 + @unix_perms ||= 0o644 ::Zip::FILE_TYPE_FILE when :directory - @unix_perms ||= 0755 + @unix_perms ||= 0o755 ::Zip::FILE_TYPE_DIR when :symlink - @unix_perms ||= 0755 + @unix_perms ||= 0o755 ::Zip::FILE_TYPE_SYMLINK end unless ft.nil? - @external_file_attributes = (ft << 12 | (@unix_perms & 07777)) << 16 + @external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16 end end @@ -464,7 +485,7 @@ def write_c_dir_entry(io) #:nodoc:all def ==(other) return false unless other.class == self.class # Compares contents of local entry and exposed fields - keys_equal = %w(compression_method crc compressed_size size name extra filepath).all? do |k| + 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.dos_equals(other.time) @@ -494,7 +515,7 @@ def get_input_stream(&block) end else zis = ::Zip::InputStream.new(@zipfile, local_header_offset) - zis.instance_variable_set(:@internal, true) + zis.instance_variable_set(:@complete_entry, self) zis.get_next_entry if block_given? begin @@ -582,9 +603,21 @@ def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exi get_input_stream do |is| set_extra_attributes_on_path(dest_path) - buf = '' + bytes_written = 0 + warned = false + buf = ''.dup while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf)) os << buf + bytes_written += buf.bytesize + if bytes_written > size && !warned + message = "Entry #{name} should be #{size}B but is larger when inflated" + if ::Zip.validate_entry_sizes + raise ::Zip::EntrySizeError, message + else + puts "WARNING: #{message}" + warned = true + end + end end end end @@ -607,32 +640,9 @@ def create_directory(dest_path) # BUG: create_symlink() does not use &block def create_symlink(dest_path) - stat = nil - begin - stat = ::File.lstat(dest_path) - rescue Errno::ENOENT - end - - io = get_input_stream - linkto = io.read - - if stat - if stat.symlink? - if ::File.readlink(dest_path) == linkto - return - else - raise ::Zip::DestinationFileExistsError, - "Cannot create symlink '#{dest_path}'. " \ - 'A symlink already exists with that name' - end - else - raise ::Zip::DestinationFileExistsError, - "Cannot create symlink '#{dest_path}'. " \ - 'A file already exists with that name' - end - end - - ::File.symlink(linkto, dest_path) + # TODO: Symlinks pose security challenges. Symlink support temporarily + # removed in view of https://github.com/rubyzip/rubyzip/issues/369 . + puts "WARNING: skipped symlink #{dest_path}" end # apply missing data from the zip64 extra information field, if present diff --git a/lib/zip/entry_set.rb b/lib/zip/entry_set.rb index 21bfd381..3272b2a4 100644 --- a/lib/zip/entry_set.rb +++ b/lib/zip/entry_set.rb @@ -5,7 +5,7 @@ class EntrySet #:nodoc:all def initialize(an_enumerable = []) super() - @entry_set = {} + @entry_set = {} an_enumerable.each { |o| push(o) } end @@ -33,9 +33,9 @@ def delete(entry) entry if @entry_set.delete(to_key(entry)) end - def each(&block) + def each @entry_set = sorted_entries.dup.each do |_, value| - block.call(value) + yield(value) end end diff --git a/lib/zip/errors.rb b/lib/zip/errors.rb index b2bcccd2..364c6eee 100644 --- a/lib/zip/errors.rb +++ b/lib/zip/errors.rb @@ -4,6 +4,7 @@ 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 diff --git a/lib/zip/extra_field.rb b/lib/zip/extra_field.rb index 225abeb5..72c36764 100644 --- a/lib/zip/extra_field.rb +++ b/lib/zip/extra_field.rb @@ -8,7 +8,7 @@ def initialize(binstr = nil) def extra_field_type_exist(binstr, id, len, i) field_name = ID_MAP[id].name - if self.member?(field_name) + if member?(field_name) self[field_name].merge(binstr[i, len + 4]) else field_obj = ID_MAP[id].new(binstr[i, len + 4]) @@ -26,7 +26,7 @@ def extra_field_type_unknown(binstr, len, i) end def create_unknown_item - s = '' + s = ''.dup class << s alias_method :to_c_dir_bin, :to_s alias_method :to_local_bin, :to_s diff --git a/lib/zip/extra_field/generic.rb b/lib/zip/extra_field/generic.rb index 7b60bebb..5931b5c2 100644 --- a/lib/zip/extra_field/generic.rb +++ b/lib/zip/extra_field/generic.rb @@ -1,7 +1,7 @@ module Zip class ExtraField::Generic def self.register_map - if self.const_defined?(:HEADER_ID) + if const_defined?(:HEADER_ID) ::Zip::ExtraField::ID_MAP[const_get(:HEADER_ID)] = self end end diff --git a/lib/zip/extra_field/zip64_placeholder.rb b/lib/zip/extra_field/zip64_placeholder.rb index f2573eb4..dfaa56e8 100644 --- a/lib/zip/extra_field/zip64_placeholder.rb +++ b/lib/zip/extra_field/zip64_placeholder.rb @@ -6,8 +6,7 @@ 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 initialize(_binstr = nil); end def pack_for_local "\x00" * 16 diff --git a/lib/zip/file.rb b/lib/zip/file.rb index 8c5173ba..9c7f3cbd 100644 --- a/lib/zip/file.rb +++ b/lib/zip/file.rb @@ -64,25 +64,38 @@ class File < CentralDirectory # Opens a zip archive. Pass true as the second parameter to create # a new archive if it doesn't exist already. - def initialize(file_name, create = false, buffer = false, options = {}) + def initialize(path_or_io, create = false, buffer = false, options = {}) super() - @name = file_name + @name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io @comment = '' @create = create ? true : false # allow any truthy value to mean true - case - when !buffer && ::File.size?(file_name) + + if ::File.size?(@name.to_s) + # There is a file, which exists, that is associated with this zip. @create = false - @file_permissions = ::File.stat(file_name).mode - ::File.open(name, 'rb') do |f| - read_from_stream(f) + @file_permissions = ::File.stat(@name).mode + + if buffer + read_from_stream(path_or_io) + else + ::File.open(@name, 'rb') do |f| + read_from_stream(f) + end end - when @create + elsif buffer && path_or_io.size > 0 + # This zip is probably a non-empty StringIO. + read_from_stream(path_or_io) + elsif @create + # This zip is completely new/empty and is to be created. @entry_set = EntrySet.new - when ::File.zero?(file_name) - raise Error, "File #{file_name} has zero size. Did you mean to pass the create flag?" + 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 - raise Error, "File #{file_name} not found" + # Everything is wrong. + raise Error, "File #{@name} not found" end + @stored_entries = @entry_set.dup @stored_comment = @comment @restore_ownership = options[:restore_ownership] || false @@ -120,17 +133,16 @@ def open_buffer(io, options = {}) unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.is_a?(String) raise "Zip::File.open_buffer expects a String or IO-like argument (responds to #{IO_METHODS.join(', ')}). Found: #{io.class}" end - if io.is_a?(::String) - require 'stringio' - io = ::StringIO.new(io) - elsif io.respond_to?(:binmode) - # https://github.com/rubyzip/rubyzip/issues/119 - io.binmode - end + + io = ::StringIO.new(io) if io.is_a?(::String) + + # https://github.com/rubyzip/rubyzip/issues/119 + io.binmode if io.respond_to?(:binmode) + zf = ::Zip::File.new(io, true, true, options) - zf.read_from_stream(io) return zf unless block_given? yield zf + begin zf.write_buffer(io) rescue IOError => e @@ -151,10 +163,9 @@ def foreach(aZipFileName, &block) end def get_segment_size_for_split(segment_size) - case - when MIN_SEGMENT_SIZE > segment_size + if MIN_SEGMENT_SIZE > segment_size MIN_SEGMENT_SIZE - when MAX_SEGMENT_SIZE < segment_size + elsif MAX_SEGMENT_SIZE < segment_size MAX_SEGMENT_SIZE else segment_size @@ -162,8 +173,10 @@ def get_segment_size_for_split(segment_size) end def get_partial_zip_file_name(zip_file_name, partial_zip_file_name) - partial_zip_file_name = zip_file_name.sub(/#{::File.basename(zip_file_name)}\z/, - partial_zip_file_name + ::File.extname(zip_file_name)) unless partial_zip_file_name.nil? + 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 @@ -237,7 +250,7 @@ def get_input_stream(entry, &aProc) # specified. If a block is passed the stream object is passed to the block and # the stream is automatically closed afterwards just as with ruby's builtin # File.open method. - def get_output_stream(entry, permission_int = nil, comment = nil, extra = nil, compressed_size = nil, crc = nil, compression_method = nil, size = nil, time = nil, &aProc) + def get_output_stream(entry, permission_int = nil, comment = nil, extra = nil, compressed_size = nil, crc = nil, compression_method = nil, size = nil, time = nil, &aProc) new_entry = if entry.kind_of?(Entry) entry @@ -274,6 +287,13 @@ def add(entry, src_path, &continue_on_exists_proc) @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, nil, nil, nil, nil, ::Zip::Entry::STORED) + add(entry, src_path, &continue_on_exists_proc) + end + # Removes the specified entry. def remove(entry) @entry_set.delete(get_entry(entry)) @@ -306,7 +326,7 @@ def extract(entry, dest_path, &block) # Commits changes that has been made since the previous commit to # the zip archive. def commit - return unless commit_required? + return if name.is_a?(StringIO) || !commit_required? on_success_replace do |tmp_file| ::Zip::OutputStream.open(tmp_file) do |zos| @entry_set.each do |e| @@ -366,7 +386,7 @@ def get_entry(entry) end # Creates a directory - def mkdir(entryName, permissionInt = 0755) + def mkdir(entryName, permissionInt = 0o755) raise Errno::EEXIST, "File exists - #{entryName}" if find_entry(entryName) entryName = entryName.dup.to_s entryName << '/' unless entryName.end_with?('/') diff --git a/lib/zip/filesystem.rb b/lib/zip/filesystem.rb index c77cdf4d..81ad1a18 100644 --- a/lib/zip/filesystem.rb +++ b/lib/zip/filesystem.rb @@ -142,9 +142,9 @@ def rdev_minor def ftype if file? - return 'file' + 'file' elsif directory? - return 'directory' + 'directory' else raise StandardError, 'Unknown file type' end @@ -198,30 +198,30 @@ def exists?(fileName) alias grpowned? exists? def readable?(fileName) - unix_mode_cmp(fileName, 0444) + unix_mode_cmp(fileName, 0o444) end alias readable_real? readable? def writable?(fileName) - unix_mode_cmp(fileName, 0222) + unix_mode_cmp(fileName, 0o222) end alias writable_real? writable? def executable?(fileName) - unix_mode_cmp(fileName, 0111) + unix_mode_cmp(fileName, 0o111) end alias executable_real? executable? def setuid?(fileName) - unix_mode_cmp(fileName, 04000) + unix_mode_cmp(fileName, 0o4000) end def setgid?(fileName) - unix_mode_cmp(fileName, 02000) + unix_mode_cmp(fileName, 0o2000) end def sticky?(fileName) - unix_mode_cmp(fileName, 01000) + unix_mode_cmp(fileName, 0o1000) end def umask(*args) @@ -237,8 +237,8 @@ def directory?(fileName) expand_path(fileName) == '/' || (!entry.nil? && entry.directory?) end - def open(fileName, openMode = 'r', permissionInt = 0644, &block) - openMode.gsub!('b', '') # ignore b option + def open(fileName, openMode = 'r', permissionInt = 0o644, &block) + openMode.delete!('b') # ignore b option case openMode when 'r' @mappedZip.get_input_stream(fileName, &block) @@ -260,7 +260,7 @@ def size(fileName) # Returns nil for not found and nil for directories def size?(fileName) entry = @mappedZip.find_entry(fileName) - (entry.nil? || entry.directory?) ? nil : entry.size + entry.nil? || entry.directory? ? nil : entry.size end def chown(ownerInt, groupInt, *filenames) @@ -498,7 +498,7 @@ def delete(entryName) alias rmdir delete alias unlink delete - def mkdir(entryName, permissionInt = 0755) + def mkdir(entryName, permissionInt = 0o755) @mappedZip.mkdir(entryName, permissionInt) end @@ -573,6 +573,10 @@ def get_output_stream(fileName, permissionInt = nil, &aProc) @zipFile.get_output_stream(expand_to_entry(fileName), permissionInt, &aProc) end + def glob(pattern, *flags, &block) + @zipFile.glob(expand_to_entry(pattern), *flags, &block) + end + def read(fileName) @zipFile.read(expand_to_entry(fileName)) end @@ -586,7 +590,7 @@ def rename(fileName, newName, &continueOnExistsProc) &continueOnExistsProc) end - def mkdir(fileName, permissionInt = 0755) + def mkdir(fileName, permissionInt = 0o755) @zipFile.mkdir(expand_to_entry(fileName), permissionInt) end diff --git a/lib/zip/inflater.rb b/lib/zip/inflater.rb index 0e2b97e1..f1b26d45 100644 --- a/lib/zip/inflater.rb +++ b/lib/zip/inflater.rb @@ -3,9 +3,9 @@ class Inflater < Decompressor #:nodoc:all def initialize(input_stream, decrypter = NullDecrypter.new) super(input_stream) @zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS) - @output_buffer = '' + @output_buffer = ''.dup @has_returned_empty_string = false - @decrypter = decrypter + @decrypter = decrypter end def sysread(number_of_bytes = nil, buf = '') diff --git a/lib/zip/input_stream.rb b/lib/zip/input_stream.rb index f8e78868..95fc3c16 100644 --- a/lib/zip/input_stream.rb +++ b/lib/zip/input_stream.rb @@ -129,23 +129,26 @@ def open_entry end if @current_entry && @current_entry.gp_flags & 8 == 8 && @current_entry.crc == 0 \ && @current_entry.compressed_size == 0 \ - && @current_entry.size == 0 && !@internal + && @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 - @decompressor = get_decompressor + @decompressor = get_decompressor flush @current_entry end def get_decompressor - case - when @current_entry.nil? + if @current_entry.nil? ::Zip::NullDecompressor - when @current_entry.compression_method == ::Zip::Entry::STORED - ::Zip::PassThruDecompressor.new(@archive_io, @current_entry.size) - when @current_entry.compression_method == ::Zip::Entry::DEFLATED + elsif @current_entry.compression_method == ::Zip::Entry::STORED + if @current_entry.gp_flags & 8 == 8 && @current_entry.crc == 0 && @current_entry.size == 0 && @complete_entry + ::Zip::PassThruDecompressor.new(@archive_io, @complete_entry.size) + else + ::Zip::PassThruDecompressor.new(@archive_io, @current_entry.size) + end + elsif @current_entry.compression_method == ::Zip::Entry::DEFLATED header = @archive_io.read(@decrypter.header_bytesize) @decrypter.reset!(header) ::Zip::Inflater.new(@archive_io, @decrypter) diff --git a/lib/zip/ioextras/abstract_input_stream.rb b/lib/zip/ioextras/abstract_input_stream.rb index 5db051e1..7b7fd61d 100644 --- a/lib/zip/ioextras/abstract_input_stream.rb +++ b/lib/zip/ioextras/abstract_input_stream.rb @@ -33,7 +33,7 @@ def read(number_of_bytes = nil, buf = '') sysread(number_of_bytes, buf) end - if tbuf.nil? || tbuf.length == 0 + if tbuf.nil? || tbuf.empty? return nil if number_of_bytes return '' end diff --git a/lib/zip/ioextras/abstract_output_stream.rb b/lib/zip/ioextras/abstract_output_stream.rb index c1246f97..69d0cc7c 100644 --- a/lib/zip/ioextras/abstract_output_stream.rb +++ b/lib/zip/ioextras/abstract_output_stream.rb @@ -15,7 +15,7 @@ def print(*params) end def printf(a_format_string, *params) - self << sprintf(a_format_string, *params) + self << format(a_format_string, *params) end def putc(an_object) diff --git a/lib/zip/output_stream.rb b/lib/zip/output_stream.rb index 693678be..d9bbc4df 100644 --- a/lib/zip/output_stream.rb +++ b/lib/zip/output_stream.rb @@ -87,11 +87,11 @@ def close_buffer # +entry+ can be a ZipEntry object or a string. def put_next_entry(entry_name, comment = nil, extra = nil, compression_method = Entry::DEFLATED, level = Zip.default_compression) raise Error, 'zip stream is closed' if @closed - if entry_name.kind_of?(Entry) - new_entry = entry_name - else - new_entry = Entry.new(@file_name, entry_name.to_s) - end + new_entry = if entry_name.kind_of?(Entry) + entry_name + else + Entry.new(@file_name, entry_name.to_s) + end new_entry.comment = comment unless comment.nil? unless extra.nil? new_entry.extra = extra.is_a?(ExtraField) ? extra : ExtraField.new(extra.to_s) diff --git a/lib/zip/pass_thru_decompressor.rb b/lib/zip/pass_thru_decompressor.rb index ca30f5d4..485462c5 100644 --- a/lib/zip/pass_thru_decompressor.rb +++ b/lib/zip/pass_thru_decompressor.rb @@ -1,5 +1,5 @@ module Zip - class PassThruDecompressor < Decompressor #:nodoc:all + class PassThruDecompressor < Decompressor #:nodoc:all def initialize(input_stream, chars_to_read) super(input_stream) @chars_to_read = chars_to_read diff --git a/lib/zip/streamable_stream.rb b/lib/zip/streamable_stream.rb index fc1c9a2d..2a4bf507 100644 --- a/lib/zip/streamable_stream.rb +++ b/lib/zip/streamable_stream.rb @@ -5,7 +5,7 @@ def initialize(entry) dirname = if zipfile.is_a?(::String) ::File.dirname(zipfile) else - '.' + nil end @temp_file = Tempfile.new(::File.basename(name), dirname) @temp_file.binmode diff --git a/lib/zip/version.rb b/lib/zip/version.rb index f1dc85f4..37fba090 100644 --- a/lib/zip/version.rb +++ b/lib/zip/version.rb @@ -1,3 +1,3 @@ module Zip - VERSION = '1.2.1' + VERSION = '1.3.0' end diff --git a/rubyzip.gemspec b/rubyzip.gemspec index ccd9ba56..6b873752 100644 --- a/rubyzip.gemspec +++ b/rubyzip.gemspec @@ -1,4 +1,5 @@ #-*- encoding: utf-8 -*- + lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'zip/version' @@ -11,13 +12,21 @@ 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(README.md TODO Rakefile) + s.files = Dir.glob('{samples,lib}/**/*.rb') + %w[README.md TODO Rakefile] s.test_files = Dir.glob('test/**/*') s.require_paths = ['lib'] s.license = 'BSD 2-Clause' + s.metadata = { + 'bug_tracker_uri' => 'https://github.com/rubyzip/rubyzip/issues', + 'changelog_uri' => "https://github.com/rubyzip/rubyzip/blob/v#{s.version}/Changelog.md", + 'documentation_uri' => "https://www.rubydoc.info/gems/rubyzip/#{s.version}", + 'source_code_uri' => "https://github.com/rubyzip/rubyzip/tree/v#{s.version}", + 'wiki_uri' => 'https://github.com/rubyzip/rubyzip/wiki' + } s.required_ruby_version = '>= 1.9.2' s.add_development_dependency 'rake', '~> 10.3' s.add_development_dependency 'pry', '~> 0.10' s.add_development_dependency 'minitest', '~> 5.4' s.add_development_dependency 'coveralls', '~> 0.7' + s.add_development_dependency 'rubocop', '~> 0.49.1' end diff --git a/samples/example_recursive.rb b/samples/example_recursive.rb index c0680b72..56a5cc7c 100644 --- a/samples/example_recursive.rb +++ b/samples/example_recursive.rb @@ -19,37 +19,36 @@ def initialize(input_dir, output_file) # Zip the input directory. def write - entries = Dir.entries(@input_dir) - %w(. ..) + entries = Dir.entries(@input_dir) - %w[. ..] - ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |io| - write_entries entries, '', io + ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile| + write_entries entries, '', zipfile end end private # A helper method to make the recursion work. - def write_entries(entries, path, io) + def write_entries(entries, path, zipfile) entries.each do |e| - zip_file_path = path == '' ? e : File.join(path, e) - disk_file_path = File.join(@input_dir, zip_file_path) - puts "Deflating #{disk_file_path}" + zipfile_path = path == '' ? e : File.join(path, e) + disk_file_path = File.join(@input_dir, zipfile_path) if File.directory? disk_file_path - recursively_deflate_directory(disk_file_path, io, zip_file_path) + recursively_deflate_directory(disk_file_path, zipfile, zipfile_path) else - put_into_archive(disk_file_path, io, zip_file_path) + put_into_archive(disk_file_path, zipfile, zipfile_path) end end end - def recursively_deflate_directory(disk_file_path, io, zip_file_path) - io.mkdir zip_file_path - subdir = Dir.entries(disk_file_path) - %w(. ..) - write_entries subdir, zip_file_path, io + def recursively_deflate_directory(disk_file_path, zipfile, zipfile_path) + zipfile.mkdir zipfile_path + subdir = Dir.entries(disk_file_path) - %w[. ..] + write_entries subdir, zipfile_path, zipfile end - def put_into_archive(disk_file_path, io, zip_file_path) - io.add(zip_file_path, disk_file_path) + def put_into_archive(disk_file_path, zipfile, zipfile_path) + zipfile.add(zipfile_path, disk_file_path) end end diff --git a/samples/gtk_ruby_zip.rb b/samples/gtk_ruby_zip.rb index 2b5a2883..62f005a5 100755 --- a/samples/gtk_ruby_zip.rb +++ b/samples/gtk_ruby_zip.rb @@ -31,7 +31,7 @@ def initialize sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) box.pack_start(sw, true, true, 0) - @clist = Gtk::CList.new(%w(Name Size Compression)) + @clist = Gtk::CList.new(%w[Name Size Compression]) @clist.set_selection_mode(Gtk::SELECTION_BROWSE) @clist.set_column_width(0, 120) @clist.set_column_width(1, 120) diff --git a/samples/qtzip.rb b/samples/qtzip.rb index c47fc97c..1d450a78 100755 --- a/samples/qtzip.rb +++ b/samples/qtzip.rb @@ -65,7 +65,7 @@ def extract_files end puts "selected_items.size = #{selected_items.size}" puts "unselected_items.size = #{unselected_items.size}" - items = selected_items.size > 0 ? selected_items : unselected_items + items = !selected_items.empty? ? selected_items : unselected_items puts "items.size = #{items.size}" d = Qt::FileDialog.get_existing_directory(nil, self) diff --git a/samples/zipfind.rb b/samples/zipfind.rb index fa2aa4e6..400e0a69 100755 --- a/samples/zipfind.rb +++ b/samples/zipfind.rb @@ -31,7 +31,7 @@ def self.find_file(path, fileNamePattern, zipFilePattern = /\.zip$/i) end end -if __FILE__ == $0 +if $0 == __FILE__ module ZipFindConsoleRunner PATH_ARG_INDEX = 0 FILENAME_PATTERN_ARG_INDEX = 1 @@ -47,7 +47,7 @@ def self.run(args) end def self.check_args(args) - if (args.size != 3) + if args.size != 3 usage exit end diff --git a/test/central_directory_entry_test.rb b/test/central_directory_entry_test.rb index 35a16cde..fa0d8065 100644 --- a/test/central_directory_entry_test.rb +++ b/test/central_directory_entry_test.rb @@ -2,7 +2,7 @@ class ZipCentralDirectoryEntryTest < MiniTest::Test def test_read_from_stream - File.open('test/data/testDirectory.bin', 'rb') do |file| + File.open('test/data/testDirectory.bin', 'rb') do |file| entry = ::Zip::Entry.read_c_dir_entry(file) assert_equal('longAscii.txt', entry.name) diff --git a/test/data/gpbit3stored.zip b/test/data/gpbit3stored.zip new file mode 100644 index 00000000..3c73eeb3 Binary files /dev/null and b/test/data/gpbit3stored.zip differ diff --git a/test/data/path_traversal/Makefile b/test/data/path_traversal/Makefile new file mode 100644 index 00000000..9ff4d816 --- /dev/null +++ b/test/data/path_traversal/Makefile @@ -0,0 +1,10 @@ +# Based on 'relative2' in https://github.com/jwilk/path-traversal-samples, +# but create the local `tmp` folder before adding the symlink. Otherwise +# we may bail out before we get to trying to create the file. +all: relative1.zip +relative1.zip: + rm -f $(@) + mkdir -p -m 755 tmp/tmp + umask 022 && echo moo > moo + cd tmp && zip -X ../$(@) tmp tmp/../../moo + rm -rf tmp moo diff --git a/test/data/path_traversal/jwilk/README.md b/test/data/path_traversal/jwilk/README.md new file mode 100644 index 00000000..2ecceb23 --- /dev/null +++ b/test/data/path_traversal/jwilk/README.md @@ -0,0 +1,5 @@ +# Path Traversal Samples + +Copied from https://github.com/jwilk/path-traversal-samples on 2018-08-26. + +License: MIT diff --git a/test/data/path_traversal/jwilk/absolute1.zip b/test/data/path_traversal/jwilk/absolute1.zip new file mode 100644 index 00000000..27c615d9 Binary files /dev/null and b/test/data/path_traversal/jwilk/absolute1.zip differ diff --git a/test/data/path_traversal/jwilk/absolute2.zip b/test/data/path_traversal/jwilk/absolute2.zip new file mode 100644 index 00000000..c82c14ea Binary files /dev/null and b/test/data/path_traversal/jwilk/absolute2.zip differ diff --git a/test/data/path_traversal/jwilk/dirsymlink.zip b/test/data/path_traversal/jwilk/dirsymlink.zip new file mode 100644 index 00000000..978b5d8a Binary files /dev/null and b/test/data/path_traversal/jwilk/dirsymlink.zip differ diff --git a/test/data/path_traversal/jwilk/dirsymlink2a.zip b/test/data/path_traversal/jwilk/dirsymlink2a.zip new file mode 100644 index 00000000..443deede Binary files /dev/null and b/test/data/path_traversal/jwilk/dirsymlink2a.zip differ diff --git a/test/data/path_traversal/jwilk/dirsymlink2b.zip b/test/data/path_traversal/jwilk/dirsymlink2b.zip new file mode 100644 index 00000000..5a5a12b4 Binary files /dev/null and b/test/data/path_traversal/jwilk/dirsymlink2b.zip differ diff --git a/test/data/path_traversal/jwilk/relative0.zip b/test/data/path_traversal/jwilk/relative0.zip new file mode 100644 index 00000000..d27a0d08 Binary files /dev/null and b/test/data/path_traversal/jwilk/relative0.zip differ diff --git a/test/data/path_traversal/jwilk/relative2.zip b/test/data/path_traversal/jwilk/relative2.zip new file mode 100644 index 00000000..8957028d Binary files /dev/null and b/test/data/path_traversal/jwilk/relative2.zip differ diff --git a/test/data/path_traversal/jwilk/symlink.zip b/test/data/path_traversal/jwilk/symlink.zip new file mode 100644 index 00000000..edaa7526 Binary files /dev/null and b/test/data/path_traversal/jwilk/symlink.zip differ diff --git a/test/data/path_traversal/relative1.zip b/test/data/path_traversal/relative1.zip new file mode 100644 index 00000000..bfcb9def Binary files /dev/null and b/test/data/path_traversal/relative1.zip differ diff --git a/test/data/path_traversal/tilde.zip b/test/data/path_traversal/tilde.zip new file mode 100644 index 00000000..0442ab93 Binary files /dev/null and b/test/data/path_traversal/tilde.zip differ diff --git a/test/data/path_traversal/tuzovakaoff/README.md b/test/data/path_traversal/tuzovakaoff/README.md new file mode 100644 index 00000000..f599810e --- /dev/null +++ b/test/data/path_traversal/tuzovakaoff/README.md @@ -0,0 +1,3 @@ +# Path Traversal Samples + +Copied from https://github.com/tuzovakaoff/zip_path_traversal on 2018-08-25. diff --git a/test/data/path_traversal/tuzovakaoff/absolutepath.zip b/test/data/path_traversal/tuzovakaoff/absolutepath.zip new file mode 100644 index 00000000..59fceed7 Binary files /dev/null and b/test/data/path_traversal/tuzovakaoff/absolutepath.zip differ diff --git a/test/data/path_traversal/tuzovakaoff/symlink.zip b/test/data/path_traversal/tuzovakaoff/symlink.zip new file mode 100644 index 00000000..e74ee19a Binary files /dev/null and b/test/data/path_traversal/tuzovakaoff/symlink.zip differ diff --git a/test/data/rubycode.zip b/test/data/rubycode.zip index 8a68560e..06134bbc 100644 Binary files a/test/data/rubycode.zip and b/test/data/rubycode.zip differ diff --git a/test/errors_test.rb b/test/errors_test.rb index a65a3afa..2c6adb2f 100644 --- a/test/errors_test.rb +++ b/test/errors_test.rb @@ -1,4 +1,5 @@ # encoding: utf-8 + require 'test_helper' class ErrorsTest < MiniTest::Test diff --git a/test/file_extract_test.rb b/test/file_extract_test.rb index 57833fcb..6103aeae 100644 --- a/test/file_extract_test.rb +++ b/test/file_extract_test.rb @@ -10,6 +10,10 @@ def setup ::File.delete(EXTRACTED_FILENAME) if ::File.exist?(EXTRACTED_FILENAME) end + def teardown + ::Zip.reset! + end + def test_extract ::Zip::File.open(TEST_ZIP.zip_name) do |zf| zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) @@ -80,4 +84,62 @@ def test_extract_non_entry_2 end assert(!File.exist?(outFile)) end + + def test_extract_incorrect_size + # 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 + 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, ::Zip::File::CREATE) 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 = [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.open(fake_zip, 'wb') do |file| + file.write data + end + + 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 + a_entry.extract + assert_equal true_size, File.size(file_name) + FileUtils.rm file_name + + ::Zip.validate_entry_sizes = true + error = assert_raises ::Zip::EntrySizeError do + a_entry.extract + end + assert_equal \ + 'Entry a should be 1B but is larger when inflated', + error.message + end + end + end + end end diff --git a/test/file_permissions_test.rb b/test/file_permissions_test.rb index fd666b88..4e4573a4 100644 --- a/test/file_permissions_test.rb +++ b/test/file_permissions_test.rb @@ -1,9 +1,8 @@ require 'test_helper' class FilePermissionsTest < MiniTest::Test - - ZIPNAME = File.join(File.dirname(__FILE__), "umask.zip") - FILENAME = File.join(File.dirname(__FILE__), "umask.txt") + ZIPNAME = File.join(File.dirname(__FILE__), 'umask.zip') + FILENAME = File.join(File.dirname(__FILE__), 'umask.txt') def teardown ::File.unlink(ZIPNAME) @@ -16,7 +15,7 @@ def test_current_umask end def test_umask_000 - set_umask(0000) do + set_umask(0o000) do create_files end @@ -24,7 +23,7 @@ def test_umask_000 end def test_umask_066 - set_umask(0066) do + set_umask(0o066) do create_files end @@ -32,7 +31,7 @@ def test_umask_066 end def test_umask_027 - set_umask(0027) do + set_umask(0o027) do create_files end @@ -48,7 +47,7 @@ def assert_matching_permissions(expected_file, actual_file) def create_files ::Zip::File.open(ZIPNAME, ::Zip::File::CREATE) do |zip| - zip.comment = "test" + zip.comment = 'test' end ::File.open(FILENAME, 'w') do |file| @@ -57,13 +56,10 @@ def create_files end # If anything goes wrong, make sure the umask is restored. - def set_umask(umask, &block) - begin - saved_umask = ::File.umask(umask) - yield - ensure - ::File.umask(saved_umask) - end + def set_umask(umask) + saved_umask = ::File.umask(umask) + yield + ensure + ::File.umask(saved_umask) end - end diff --git a/test/file_test.rb b/test/file_test.rb index a6d187ea..94ff769c 100644 --- a/test/file_test.rb +++ b/test/file_test.rb @@ -55,6 +55,12 @@ def test_create_from_scratch_with_old_create_parameter assert_equal(2, zfRead.entries.length) end + def test_get_input_stream_stored_with_gpflag_bit3 + ::Zip::File.open('test/data/gpbit3stored.zip') do |zf| + assert_equal("foo\n", zf.read("foo.txt")) + end + end + def test_get_output_stream entryCount = nil ::Zip::File.open(TEST_ZIP.zip_name) do |zf| @@ -97,6 +103,13 @@ def test_get_output_stream end end + def test_open_buffer_with_string + string = File.read('test/data/rubycode.zip') + ::Zip::File.open_buffer string do |zf| + assert zf.entries.map { |e| e.name }.include?('zippedruby1.rb') + end + end + def test_open_buffer_with_stringio string_io = StringIO.new File.read('test/data/rubycode.zip') ::Zip::File.open_buffer string_io do |zf| @@ -104,6 +117,57 @@ def test_open_buffer_with_stringio end end + def test_close_buffer_with_stringio + string_io = StringIO.new File.read('test/data/rubycode.zip') + zf = ::Zip::File.open_buffer string_io + assert_nil zf.close + end + + def test_open_buffer_no_op_does_not_change_file + Dir.mktmpdir do |tmp| + test_zip = File.join(tmp, 'test.zip') + FileUtils.cp 'test/data/rubycode.zip', test_zip + + # Note: this may change the file if it is opened with r+b instead of rb. + # The 'extra fields' in this particular zip file get reordered. + File.open(test_zip, 'rb') do |file| + Zip::File.open_buffer(file) do |zf| + nil # do nothing + end + end + + assert_equal \ + File.binread('test/data/rubycode.zip'), + File.binread(test_zip) + end + end + + def test_open_buffer_close_does_not_change_file + Dir.mktmpdir do |tmp| + test_zip = File.join(tmp, 'test.zip') + FileUtils.cp 'test/data/rubycode.zip', test_zip + + File.open(test_zip, 'rb') do |file| + zf = Zip::File.open_buffer(file) + refute zf.commit_required? + assert_nil zf.close + end + + assert_equal \ + File.binread('test/data/rubycode.zip'), + File.binread(test_zip) + end + end + + def test_open_buffer_with_io_and_block + File.open('test/data/rubycode.zip') do |io| + io.set_encoding(Encoding::BINARY) # not strictly required but can be set + Zip::File.open_buffer(io) do |zip_io| + # left empty on purpose + end + end + end + def test_open_buffer_without_block string_io = StringIO.new File.read('test/data/rubycode.zip') zf = ::Zip::File.open_buffer string_io @@ -140,17 +204,37 @@ def test_add zfRead.get_input_stream(entryName) { |zis| zis.read }) end + def test_add_stored + srcFile = 'test/data/file2.txt' + entryName = 'newEntryName.rb' + assert(::File.exist?(srcFile)) + zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE) + zf.add_stored(entryName, srcFile) + zf.close + + zfRead = ::Zip::File.new(EMPTY_FILENAME) + entry = zfRead.entries.first + assert_equal('', zfRead.comment) + assert_equal(1, zfRead.entries.length) + assert_equal(entryName, entry.name) + assert_equal(File.size(srcFile), entry.size) + assert_equal(entry.size, entry.compressed_size) + assert_equal(::Zip::Entry::STORED, entry.compression_method) + AssertEntry.assert_contents(srcFile, + zfRead.get_input_stream(entryName) { |zis| zis.read }) + end + def test_recover_permissions_after_add_files_to_archive srcZip = TEST_ZIP.zip_name - ::File.chmod(0664, srcZip) + ::File.chmod(0o664, srcZip) srcFile = 'test/data/file2.txt' entryName = 'newEntryName.rb' - assert_equal(::File.stat(srcZip).mode, 0100664) + assert_equal(::File.stat(srcZip).mode, 0o100664) assert(::File.exist?(srcZip)) zf = ::Zip::File.new(srcZip, ::Zip::File::CREATE) zf.add(entryName, srcFile) zf.close - assert_equal(::File.stat(srcZip).mode, 0100664) + assert_equal(::File.stat(srcZip).mode, 0o100664) end def test_add_existing_entry_name @@ -234,7 +318,7 @@ def test_rename_with_each zf.mkdir('test') arr << 'test/' arr_renamed << 'Ztest/' - %w(a b c d).each do |f| + %w[a b c d].each do |f| zf.get_output_stream("test/#{f}") { |file| file.puts 'aaaa' } arr << "test/#{f}" arr_renamed << "Ztest/#{f}" @@ -329,7 +413,7 @@ def test_replace def test_replace_non_entry entryToReplace = 'nonExistingEntryname' - ::Zip::File.open(TEST_ZIP.zip_name) do |zf| + ::Zip::File.open(TEST_ZIP.zip_name) do |zf| assert_raises(Errno::ENOENT) { zf.replace(entryToReplace, 'test/data/file2.txt') } end end @@ -347,7 +431,7 @@ def test_commit zfRead.close zf.close - res = system("unzip -t #{TEST_ZIP.zip_name}") + res = system("unzip -tqq #{TEST_ZIP.zip_name}") assert_equal(res, true) end @@ -363,7 +447,7 @@ def test_double_commit(filename = 'test/data/generated/double_commit_test.zip') zf2 = ::Zip::File.open(filename) assert(zf2.entries.detect { |e| e.name == 'test1.txt' } != nil) assert(zf2.entries.detect { |e| e.name == 'test2.txt' } != nil) - res = system("unzip -t #{filename}") + res = system("unzip -tqq #{filename}") assert_equal(res, true) end @@ -434,7 +518,6 @@ def test_compound1 filename_to_remove = originalEntries.map(&:to_s).find { |name| name.match('longBinary') } zf.remove(filename_to_remove) assert_not_contains(zf, filename_to_remove) - ensure zf.close end @@ -558,7 +641,7 @@ def test_odd_extra_field entry_count = 0 File.open 'test/data/oddExtraField.zip', 'rb' do |zip_io| Zip::File.open_buffer zip_io.read do |zip| - zip.each do |zip_entry| + zip.each do |_zip_entry| entry_count += 1 end end diff --git a/test/filesystem/dir_iterator_test.rb b/test/filesystem/dir_iterator_test.rb index c2cf00ac..8d12ce27 100644 --- a/test/filesystem/dir_iterator_test.rb +++ b/test/filesystem/dir_iterator_test.rb @@ -2,7 +2,7 @@ require 'zip/filesystem' class ZipFsDirIteratorTest < MiniTest::Test - FILENAME_ARRAY = %w(f1 f2 f3 f4 f5 f6) + FILENAME_ARRAY = %w[f1 f2 f3 f4 f5 f6] def setup @dirIt = ::Zip::FileSystem::ZipFsDirIterator.new(FILENAME_ARRAY) diff --git a/test/filesystem/directory_test.rb b/test/filesystem/directory_test.rb index d6c029f3..f36ede53 100644 --- a/test/filesystem/directory_test.rb +++ b/test/filesystem/directory_test.rb @@ -3,6 +3,7 @@ class ZipFsDirectoryTest < MiniTest::Test TEST_ZIP = 'test/data/generated/zipWithDirs_copy.zip' + GLOB_TEST_ZIP = 'test/data/globTest.zip' def setup FileUtils.cp('test/data/zipWithDirs.zip', TEST_ZIP) @@ -51,10 +52,10 @@ def test_pwd_chdir_entries zf.dir.chdir 'file1' end - assert_equal(%w(dir1 dir2 file1).sort, zf.dir.entries('.').sort) + assert_equal(%w[dir1 dir2 file1].sort, zf.dir.entries('.').sort) zf.dir.chdir 'dir1' assert_equal('/dir1', zf.dir.pwd) - assert_equal(%w(dir11 file11 file12), zf.dir.entries('.').sort) + assert_equal(%w[dir11 file11 file12], zf.dir.entries('.').sort) zf.dir.chdir '../dir2/dir21' assert_equal('/dir2/dir21', zf.dir.pwd) @@ -77,11 +78,11 @@ def test_foreach entries = [] zf.dir.foreach('.') { |e| entries << e } - assert_equal(%w(dir1 dir2 file1).sort, entries.sort) + assert_equal(%w[dir1 dir2 file1].sort, entries.sort) entries = [] zf.dir.foreach('dir1') { |e| entries << e } - assert_equal(%w(dir11 file11 file12), entries.sort) + assert_equal(%w[dir11 file11 file12], entries.sort) end end @@ -93,11 +94,28 @@ def test_chroot end end - # Globbing not supported yet - # def test_glob - # # test alias []-operator too - # fail "implement test" - # end + def test_glob + globbed_files = [ + 'globTest/foo/bar/baz/foo.txt', + 'globTest/foo.txt', + 'globTest/food.txt' + ] + + ::Zip::File.open(GLOB_TEST_ZIP) do |zf| + zf.dir.glob('**/*.txt') do |f| + assert globbed_files.include?(f.name) + end + + zf.dir.glob('globTest/foo/**/*.txt') do |f| + assert_equal globbed_files[0], f.name + end + + zf.dir.chdir('globTest/foo') + zf.dir.glob('**/*.txt') do |f| + assert_equal globbed_files[0], f.name + end + end + end def test_open_new ::Zip::File.open(TEST_ZIP) do |zf| @@ -110,11 +128,11 @@ def test_open_new end d = zf.dir.new('.') - assert_equal(%w(file1 dir1 dir2).sort, d.entries.sort) + assert_equal(%w[file1 dir1 dir2].sort, d.entries.sort) d.close zf.dir.open('dir1') do |dir| - assert_equal(%w(dir11 file11 file12).sort, dir.entries.sort) + assert_equal(%w[dir11 file11 file12].sort, dir.entries.sort) end end end diff --git a/test/filesystem/file_mutating_test.rb b/test/filesystem/file_mutating_test.rb index e398060a..ccba6e3d 100644 --- a/test/filesystem/file_mutating_test.rb +++ b/test/filesystem/file_mutating_test.rb @@ -7,8 +7,7 @@ def setup FileUtils.cp('test/data/zipWithDirs.zip', TEST_ZIP) end - def teardown - end + def teardown; end def test_delete do_test_delete_or_unlink(:delete) @@ -51,11 +50,11 @@ def test_rename def test_chmod ::Zip::File.open(TEST_ZIP) do |zf| - zf.file.chmod(0765, 'file1') + zf.file.chmod(0o765, 'file1') end ::Zip::File.open(TEST_ZIP) do |zf| - assert_equal(0100765, zf.file.stat('file1').mode) + assert_equal(0o100765, zf.file.stat('file1').mode) end end diff --git a/test/filesystem/file_nonmutating_test.rb b/test/filesystem/file_nonmutating_test.rb index e8242258..62486666 100644 --- a/test/filesystem/file_nonmutating_test.rb +++ b/test/filesystem/file_nonmutating_test.rb @@ -14,11 +14,11 @@ def teardown def test_umask assert_equal(::File.umask, @zip_file.file.umask) - @zip_file.file.umask(0006) + @zip_file.file.umask(0o006) end def test_exists? - assert(! @zip_file.file.exists?('notAFile')) + assert(!@zip_file.file.exists?('notAFile')) assert(@zip_file.file.exists?('file1')) assert(@zip_file.file.exists?('dir1')) assert(@zip_file.file.exists?('dir1/')) @@ -114,13 +114,13 @@ def test_size? def test_file? assert(@zip_file.file.file?('file1')) assert(@zip_file.file.file?('dir2/file21')) - assert(! @zip_file.file.file?('dir1')) - assert(! @zip_file.file.file?('dir1/dir11')) + assert(!@zip_file.file.file?('dir1')) + assert(!@zip_file.file.file?('dir1/dir11')) assert(@zip_file.file.stat('file1').file?) assert(@zip_file.file.stat('dir2/file21').file?) - assert(! @zip_file.file.stat('dir1').file?) - assert(! @zip_file.file.stat('dir1/dir11').file?) + assert(!@zip_file.file.stat('dir1').file?) + assert(!@zip_file.file.stat('dir1/dir11').file?) end include ExtraAssertions @@ -160,15 +160,15 @@ def test_utime end def assert_always_false(operation) - assert(! @zip_file.file.send(operation, 'noSuchFile')) - assert(! @zip_file.file.send(operation, 'file1')) - assert(! @zip_file.file.send(operation, 'dir1')) - assert(! @zip_file.file.stat('file1').send(operation)) - assert(! @zip_file.file.stat('dir1').send(operation)) + assert(!@zip_file.file.send(operation, 'noSuchFile')) + assert(!@zip_file.file.send(operation, 'file1')) + assert(!@zip_file.file.send(operation, 'dir1')) + assert(!@zip_file.file.stat('file1').send(operation)) + assert(!@zip_file.file.stat('dir1').send(operation)) end def assert_true_if_entry_exists(operation) - assert(! @zip_file.file.send(operation, 'noSuchFile')) + assert(!@zip_file.file.send(operation, 'noSuchFile')) assert(@zip_file.file.send(operation, 'file1')) assert(@zip_file.file.send(operation, 'dir1')) assert(@zip_file.file.stat('file1').send(operation)) @@ -221,15 +221,15 @@ def test_link end def test_directory? - assert(! @zip_file.file.directory?('notAFile')) - assert(! @zip_file.file.directory?('file1')) - assert(! @zip_file.file.directory?('dir1/file11')) + assert(!@zip_file.file.directory?('notAFile')) + assert(!@zip_file.file.directory?('file1')) + assert(!@zip_file.file.directory?('dir1/file11')) assert(@zip_file.file.directory?('dir1')) assert(@zip_file.file.directory?('dir1/')) assert(@zip_file.file.directory?('dir2/dir21')) - assert(! @zip_file.file.stat('file1').directory?) - assert(! @zip_file.file.stat('dir1/file11').directory?) + assert(!@zip_file.file.stat('file1').directory?) + assert(!@zip_file.file.stat('dir1/file11').directory?) assert(@zip_file.file.stat('dir1').directory?) assert(@zip_file.file.stat('dir1/').directory?) assert(@zip_file.file.stat('dir2/dir21').directory?) @@ -243,8 +243,8 @@ def test_chown end def test_zero? - assert(! @zip_file.file.zero?('notAFile')) - assert(! @zip_file.file.zero?('file1')) + assert(!@zip_file.file.zero?('notAFile')) + assert(!@zip_file.file.zero?('file1')) assert(@zip_file.file.zero?('dir1')) blockCalled = false ::Zip::File.open('test/data/generated/5entry.zip') do |zf| @@ -253,7 +253,7 @@ def test_zero? end assert(blockCalled) - assert(! @zip_file.file.stat('file1').zero?) + assert(!@zip_file.file.stat('file1').zero?) assert(@zip_file.file.stat('dir1').zero?) blockCalled = false ::Zip::File.open('test/data/generated/5entry.zip') do |zf| @@ -309,7 +309,7 @@ def test_ntfs_time end def test_readable? - assert(! @zip_file.file.readable?('noSuchFile')) + assert(!@zip_file.file.readable?('noSuchFile')) assert(@zip_file.file.readable?('file1')) assert(@zip_file.file.readable?('dir1')) assert(@zip_file.file.stat('file1').readable?) @@ -317,7 +317,7 @@ def test_readable? end def test_readable_real? - assert(! @zip_file.file.readable_real?('noSuchFile')) + assert(!@zip_file.file.readable_real?('noSuchFile')) assert(@zip_file.file.readable_real?('file1')) assert(@zip_file.file.readable_real?('dir1')) assert(@zip_file.file.stat('file1').readable_real?) @@ -325,7 +325,7 @@ def test_readable_real? end def test_writable? - assert(! @zip_file.file.writable?('noSuchFile')) + assert(!@zip_file.file.writable?('noSuchFile')) assert(@zip_file.file.writable?('file1')) assert(@zip_file.file.writable?('dir1')) assert(@zip_file.file.stat('file1').writable?) @@ -333,7 +333,7 @@ def test_writable? end def test_writable_real? - assert(! @zip_file.file.writable_real?('noSuchFile')) + assert(!@zip_file.file.writable_real?('noSuchFile')) assert(@zip_file.file.writable_real?('file1')) assert(@zip_file.file.writable_real?('dir1')) assert(@zip_file.file.stat('file1').writable_real?) @@ -341,18 +341,18 @@ def test_writable_real? end def test_executable? - assert(! @zip_file.file.executable?('noSuchFile')) - assert(! @zip_file.file.executable?('file1')) + assert(!@zip_file.file.executable?('noSuchFile')) + assert(!@zip_file.file.executable?('file1')) assert(@zip_file.file.executable?('dir1')) - assert(! @zip_file.file.stat('file1').executable?) + assert(!@zip_file.file.stat('file1').executable?) assert(@zip_file.file.stat('dir1').executable?) end def test_executable_real? - assert(! @zip_file.file.executable_real?('noSuchFile')) - assert(! @zip_file.file.executable_real?('file1')) + assert(!@zip_file.file.executable_real?('noSuchFile')) + assert(!@zip_file.file.executable_real?('file1')) assert(@zip_file.file.executable_real?('dir1')) - assert(! @zip_file.file.stat('file1').executable_real?) + assert(!@zip_file.file.stat('file1').executable_real?) assert(@zip_file.file.stat('dir1').executable_real?) end @@ -455,7 +455,7 @@ def test_glob zf.glob('**/foo.txt') do |match| results << "<#{match.class.name}: #{match}>" end - assert((!results.empty?), 'block not run, or run out of context') + assert(!results.empty?, 'block not run, or run out of context') assert_equal 2, results.size assert_operator results, :include?, '' assert_operator results, :include?, '' diff --git a/test/filesystem/file_stat_test.rb b/test/filesystem/file_stat_test.rb index 51e60d9c..05d7fff8 100644 --- a/test/filesystem/file_stat_test.rb +++ b/test/filesystem/file_stat_test.rb @@ -32,10 +32,10 @@ def test_ftype end def test_mode - assert_equal(0600, @zip_file.file.stat('file1').mode & 0777) - assert_equal(0600, @zip_file.file.stat('file1').mode & 0777) - assert_equal(0755, @zip_file.file.stat('dir1').mode & 0777) - assert_equal(0755, @zip_file.file.stat('dir1').mode & 0777) + assert_equal(0o600, @zip_file.file.stat('file1').mode & 0o777) + assert_equal(0o600, @zip_file.file.stat('file1').mode & 0o777) + assert_equal(0o755, @zip_file.file.stat('dir1').mode & 0o777) + assert_equal(0o755, @zip_file.file.stat('dir1').mode & 0o777) end def test_dev diff --git a/test/gentestfiles.rb b/test/gentestfiles.rb index 88ffd385..3e76e7d0 100755 --- a/test/gentestfiles.rb +++ b/test/gentestfiles.rb @@ -71,12 +71,12 @@ def initialize(zip_name, entry_names, comment = '') end def self.create_test_zips - raise "failed to create test zip '#{TEST_ZIP1.zip_name}'" unless system("/usr/bin/zip #{TEST_ZIP1.zip_name} test/data/file2.txt") - raise "failed to remove entry from '#{TEST_ZIP1.zip_name}'" unless system("/usr/bin/zip #{TEST_ZIP1.zip_name} -d test/data/file2.txt") + raise "failed to create test zip '#{TEST_ZIP1.zip_name}'" unless system("/usr/bin/zip -q #{TEST_ZIP1.zip_name} test/data/file2.txt") + raise "failed to remove entry from '#{TEST_ZIP1.zip_name}'" unless system("/usr/bin/zip -q #{TEST_ZIP1.zip_name} -d test/data/file2.txt") File.open('test/data/generated/empty.txt', 'w') {} File.open('test/data/generated/empty_chmod640.txt', 'w') {} - ::File.chmod(0640, 'test/data/generated/empty_chmod640.txt') + ::File.chmod(0o640, 'test/data/generated/empty_chmod640.txt') File.open('test/data/generated/short.txt', 'w') { |file| file << 'ABCDEF' } ziptestTxt = '' @@ -93,34 +93,34 @@ def self.create_test_zips file << testBinaryPattern << rand << "\0" while file.tell < 6E5 end - raise "failed to create test zip '#{TEST_ZIP2.zip_name}'" unless system("/usr/bin/zip #{TEST_ZIP2.zip_name} #{TEST_ZIP2.entry_names.join(' ')}") + raise "failed to create test zip '#{TEST_ZIP2.zip_name}'" unless system("/usr/bin/zip -q #{TEST_ZIP2.zip_name} #{TEST_ZIP2.entry_names.join(' ')}") if RUBY_PLATFORM =~ /mswin|mingw|cygwin/ - raise "failed to add comment to test zip '#{TEST_ZIP2.zip_name}'" unless system("echo #{TEST_ZIP2.comment}| /usr/bin/zip -z #{TEST_ZIP2.zip_name}\"") + raise "failed to add comment to test zip '#{TEST_ZIP2.zip_name}'" unless system("echo #{TEST_ZIP2.comment}| /usr/bin/zip -zq #{TEST_ZIP2.zip_name}\"") else # without bash system interprets everything after echo as parameters to # echo including | zip -z ... - raise "failed to add comment to test zip '#{TEST_ZIP2.zip_name}'" unless system("bash -c \"echo #{TEST_ZIP2.comment} | /usr/bin/zip -z #{TEST_ZIP2.zip_name}\"") + raise "failed to add comment to test zip '#{TEST_ZIP2.zip_name}'" unless system("bash -c \"echo #{TEST_ZIP2.comment} | /usr/bin/zip -zq #{TEST_ZIP2.zip_name}\"") end - raise "failed to create test zip '#{TEST_ZIP3.zip_name}'" unless system("/usr/bin/zip #{TEST_ZIP3.zip_name} #{TEST_ZIP3.entry_names.join(' ')}") + raise "failed to create test zip '#{TEST_ZIP3.zip_name}'" unless system("/usr/bin/zip -q #{TEST_ZIP3.zip_name} #{TEST_ZIP3.entry_names.join(' ')}") - raise "failed to create test zip '#{TEST_ZIP4.zip_name}'" unless system("/usr/bin/zip #{TEST_ZIP4.zip_name} #{TEST_ZIP4.entry_names.join(' ')}") + raise "failed to create test zip '#{TEST_ZIP4.zip_name}'" unless system("/usr/bin/zip -q #{TEST_ZIP4.zip_name} #{TEST_ZIP4.entry_names.join(' ')}") rescue # If there are any Windows developers wanting to use a command line zip.exe # to help create the following files, there's a free one available from # http://stahlworks.com/dev/index.php?tool=zipunzip # that works with the above code raise $!.to_s + - "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" \ - "to create test data. If you don't have it you can download\n" \ - 'the necessary test files at http://sf.net/projects/rubyzip.' + "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" \ + "to create test data. If you don't have it you can download\n" \ + 'the necessary test files at http://sf.net/projects/rubyzip.' end TEST_ZIP1 = TestZipFile.new('test/data/generated/empty.zip', []) - TEST_ZIP2 = TestZipFile.new('test/data/generated/5entry.zip', %w(test/data/generated/longAscii.txt test/data/generated/empty.txt test/data/generated/empty_chmod640.txt test/data/generated/short.txt test/data/generated/longBinary.bin), + TEST_ZIP2 = TestZipFile.new('test/data/generated/5entry.zip', %w[test/data/generated/longAscii.txt test/data/generated/empty.txt test/data/generated/empty_chmod640.txt test/data/generated/short.txt test/data/generated/longBinary.bin], 'my zip comment') - TEST_ZIP3 = TestZipFile.new('test/data/generated/test1.zip', %w(test/data/file1.txt)) + TEST_ZIP3 = TestZipFile.new('test/data/generated/test1.zip', %w[test/data/file1.txt]) TEST_ZIP4 = TestZipFile.new('test/data/generated/zipWithDir.zip', ['test/data/file1.txt', TestFiles::EMPTY_TEST_DIR]) end diff --git a/test/input_stream_test.rb b/test/input_stream_test.rb index 3a31684d..773ee6b5 100644 --- a/test/input_stream_test.rb +++ b/test/input_stream_test.rb @@ -70,7 +70,7 @@ def test_incomplete_reads entry = zis.get_next_entry # longAscii.txt assert_equal(false, zis.eof?) assert_equal(TestZipFile::TEST_ZIP2.entry_names[0], entry.name) - assert zis.gets.length > 0 + assert !zis.gets.empty? assert_equal(false, zis.eof?) entry = zis.get_next_entry # empty.txt assert_equal(TestZipFile::TEST_ZIP2.entry_names[1], entry.name) @@ -84,10 +84,10 @@ def test_incomplete_reads assert_equal(true, zis.eof?) entry = zis.get_next_entry # short.txt assert_equal(TestZipFile::TEST_ZIP2.entry_names[3], entry.name) - assert zis.gets.length > 0 + assert !zis.gets.empty? entry = zis.get_next_entry # longBinary.bin assert_equal(TestZipFile::TEST_ZIP2.entry_names[4], entry.name) - assert zis.gets.length > 0 + assert !zis.gets.empty? end end @@ -97,7 +97,7 @@ def test_incomplete_reads_from_string_io entry = zis.get_next_entry # longAscii.txt assert_equal(false, zis.eof?) assert_equal(TestZipFile::TEST_ZIP2.entry_names[0], entry.name) - assert zis.gets.length > 0 + assert !zis.gets.empty? assert_equal(false, zis.eof?) entry = zis.get_next_entry # empty.txt assert_equal(TestZipFile::TEST_ZIP2.entry_names[1], entry.name) @@ -111,10 +111,10 @@ def test_incomplete_reads_from_string_io assert_equal(true, zis.eof?) entry = zis.get_next_entry # short.txt assert_equal(TestZipFile::TEST_ZIP2.entry_names[3], entry.name) - assert zis.gets.length > 0 + assert !zis.gets.empty? entry = zis.get_next_entry # longBinary.bin assert_equal(TestZipFile::TEST_ZIP2.entry_names[4], entry.name) - assert zis.gets.length > 0 + assert !zis.gets.empty? end end diff --git a/test/ioextras/abstract_output_stream_test.rb b/test/ioextras/abstract_output_stream_test.rb index 3c2cefa0..3077db43 100644 --- a/test/ioextras/abstract_output_stream_test.rb +++ b/test/ioextras/abstract_output_stream_test.rb @@ -92,11 +92,11 @@ def test_puts assert_equal("hello\nworld\n", @output_stream.buffer) @output_stream.buffer = '' - @output_stream.puts(["hello\n", "world\n"]) + @output_stream.puts(%W[hello\n world\n]) assert_equal("hello\nworld\n", @output_stream.buffer) @output_stream.buffer = '' - @output_stream.puts(["hello\n", "world\n"], 'bingo') + @output_stream.puts(%W[hello\n world\n], 'bingo') assert_equal("hello\nworld\nbingo\n", @output_stream.buffer) @output_stream.buffer = '' diff --git a/test/path_traversal_test.rb b/test/path_traversal_test.rb new file mode 100644 index 00000000..e5bdd722 --- /dev/null +++ b/test/path_traversal_test.rb @@ -0,0 +1,141 @@ +class PathTraversalTest < MiniTest::Test + TEST_FILE_ROOT = File.absolute_path('test/data/path_traversal') + + def setup + # With apologies to anyone using these files... but they are the files in + # the sample zips, so we don't have much choice here. + FileUtils.rm_f '/tmp/moo' + FileUtils.rm_f '/tmp/file.txt' + end + + def extract_path_traversal_zip(name) + Zip::File.open(File.join(TEST_FILE_ROOT, name)) do |zip_file| + zip_file.each do |entry| + entry.extract + end + end + end + + def in_tmpdir + Dir.mktmpdir do |tmp| + test_path = File.join(tmp, 'test') + Dir.mkdir test_path + Dir.chdir test_path do + yield test_path + end + end + end + + def test_leading_slash + in_tmpdir do + extract_path_traversal_zip 'jwilk/absolute1.zip' + refute File.exist?('/tmp/moo') + end + end + + def test_multiple_leading_slashes + in_tmpdir do + extract_path_traversal_zip 'jwilk/absolute2.zip' + refute File.exist?('/tmp/moo') + end + end + + def test_leading_dot_dot + in_tmpdir do + extract_path_traversal_zip 'jwilk/relative0.zip' + refute File.exist?('../moo') + end + end + + def test_non_leading_dot_dot_with_existing_folder + in_tmpdir do + extract_path_traversal_zip 'relative1.zip' + assert Dir.exist?('tmp') + refute File.exist?('../moo') + end + end + + def test_non_leading_dot_dot_without_existing_folder + in_tmpdir do + extract_path_traversal_zip 'jwilk/relative2.zip' + refute File.exist?('../moo') + end + end + + def test_file_symlink + in_tmpdir do + extract_path_traversal_zip 'jwilk/symlink.zip' + assert File.exist?('moo') + refute File.exist?('/tmp/moo') + end + end + + def test_directory_symlink + in_tmpdir do + # Can't create tmp/moo, because the tmp symlink is skipped. + assert_raises Errno::ENOENT do + extract_path_traversal_zip 'jwilk/dirsymlink.zip' + end + refute File.exist?('/tmp/moo') + end + end + + def test_two_directory_symlinks_a + in_tmpdir do + # Can't create par/moo because the symlinks are skipped. + assert_raises Errno::ENOENT do + extract_path_traversal_zip 'jwilk/dirsymlink2a.zip' + end + refute File.exist?('cur') + refute File.exist?('par') + refute File.exist?('par/moo') + end + end + + def test_two_directory_symlinks_b + in_tmpdir do + # Can't create par/moo, because the symlinks are skipped. + assert_raises Errno::ENOENT do + extract_path_traversal_zip 'jwilk/dirsymlink2b.zip' + end + refute File.exist?('cur') + refute File.exist?('../moo') + end + end + + def test_entry_name_with_absolute_path_does_not_extract + in_tmpdir do + extract_path_traversal_zip 'tuzovakaoff/absolutepath.zip' + refute File.exist?('/tmp/file.txt') + end + end + + def test_entry_name_with_absolute_path_extract_when_given_different_path + in_tmpdir do |test_path| + zip_path = File.join(TEST_FILE_ROOT, 'tuzovakaoff/absolutepath.zip') + Zip::File.open(zip_path) do |zip_file| + zip_file.each do |entry| + entry.extract(File.join(test_path, entry.name)) + end + end + refute File.exist?('/tmp/file.txt') + end + end + + def test_entry_name_with_relative_symlink + in_tmpdir do + # Doesn't create the symlink path, so can't create path/file.txt. + assert_raises Errno::ENOENT do + extract_path_traversal_zip 'tuzovakaoff/symlink.zip' + end + refute File.exist?('/tmp/file.txt') + end + end + + def test_entry_name_with_tilde + in_tmpdir do + extract_path_traversal_zip 'tilde.zip' + assert File.exist?('~tilde~') + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index c7cbfb95..ddeba58b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -103,7 +103,7 @@ def assert_entry_contents_for_stream(filename, zis, entryName) File.open(filename, 'rb') do |file| expected = file.read actual = zis.read - if (expected != actual) + if expected != actual if (expected && actual) && (expected.length > 400 || actual.length > 400) zipEntryFilename = entryName + '.zipEntry' File.open(zipEntryFilename, 'wb') { |entryfile| entryfile << actual } @@ -118,7 +118,7 @@ def assert_entry_contents_for_stream(filename, zis, entryName) def self.assert_contents(filename, aString) fileContents = '' File.open(filename, 'rb') { |f| fileContents = f.read } - if (fileContents != aString) + if fileContents != aString if fileContents.length > 400 || aString.length > 400 stringFile = filename + '.other' File.open(stringFile, 'wb') { |f| f << aString } diff --git a/test/unicode_file_names_and_comments_test.rb b/test/unicode_file_names_and_comments_test.rb index b9b1967a..aac3e256 100644 --- a/test/unicode_file_names_and_comments_test.rb +++ b/test/unicode_file_names_and_comments_test.rb @@ -33,6 +33,18 @@ def test_unicode_file_name assert(filepath == entry_name) end end + + ::Zip.force_entry_names_encoding = 'UTF-8' + ::Zip::File.open(FILENAME) do |zip| + file_entrys.each do |filename| + refute_nil(zip.find_entry(filename)) + end + directory_entrys.each do |filepath| + refute_nil(zip.find_entry(filepath)) + end + end + ::Zip.force_entry_names_encoding = nil + ::File.unlink(FILENAME) end diff --git a/test/zip64_full_test.rb b/test/zip64_full_test.rb index d7fccbb4..ed11ed65 100644 --- a/test/zip64_full_test.rb +++ b/test/zip64_full_test.rb @@ -36,7 +36,7 @@ def test_large_zip_file end ::Zip::File.open(test_filename) do |zf| - assert_equal %w(first_file.txt huge_file last_file.txt), zf.entries.map(&:name) + assert_equal %w[first_file.txt huge_file last_file.txt], zf.entries.map(&:name) assert_equal first_text, zf.read('first_file.txt') assert_equal last_text, zf.read('last_file.txt') end @@ -44,7 +44,7 @@ def test_large_zip_file # note: if this fails, be sure you have UnZip version 6.0 or newer # as this is the first version to support zip64 extensions # but some OSes (*cough* OSX) still bundle a 5.xx release - assert system("unzip -t #{test_filename}"), 'third-party zip validation failed' + assert system("unzip -tqq #{test_filename}"), 'third-party zip validation failed' end end