diff --git a/.travis.yml b/.travis.yml index b197c86b..358e2a8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,33 +1,29 @@ language: ruby -sudo: false +dist: trusty cache: bundler rvm: - - 2.0.0 - - 2.1.10 - - 2.2.9 - - 2.3.6 - - 2.4.3 - - 2.5.0 + - 2.0 + - 2.1 + - 2.2 + - 2.3 + - 2.4 + - 2.5 + - 2.6 - ruby-head matrix: include: - rvm: jruby jdk: oraclejdk8 - - rvm: jruby + - rvm: jruby-9.1 jdk: openjdk7 - rvm: jruby-head jdk: oraclejdk8 - - rvm: rbx-3 - env: - - RUBYOPT="-rbundler/deprecate" + - rvm: rbx-4 allow_failures: - rvm: ruby-head - - rvm: rbx-3 + - rvm: rbx-4 - rvm: jruby-head - - rvm: jruby before_install: - - gem update --system - - gem install bundler - gem --version before_script: - echo `whereis zip` @@ -35,3 +31,6 @@ before_script: 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 d5dbe76b..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: ``` @@ -59,7 +61,8 @@ 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,7 +86,7 @@ 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 |zipfile| write_entries entries, '', zipfile @@ -97,7 +100,6 @@ class ZipFileGenerator entries.each do |e| zipfile_path = path == '' ? e : File.join(path, e) disk_file_path = File.join(@input_dir, zipfile_path) - puts "Deflating #{disk_file_path}" if File.directory? disk_file_path recursively_deflate_directory(disk_file_path, zipfile, zipfile_path) @@ -109,14 +111,12 @@ class ZipFileGenerator def recursively_deflate_directory(disk_file_path, zipfile, zipfile_path) zipfile.mkdir zipfile_path - subdir = Dir.entries(disk_file_path) - %w(. ..) + subdir = Dir.entries(disk_file_path) - %w[. ..] write_entries subdir, zipfile_path, zipfile end def put_into_archive(disk_file_path, zipfile, zipfile_path) - zipfile.get_output_stream(zipfile_path) do |f| - f.write(File.open(disk_file_path, 'rb').read) - end + 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 ``` @@ -177,7 +181,6 @@ 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 `::Zip::InputStream` finds such entry in the zip archive it will raise an exception. ### Password Protection (Experimental) @@ -220,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 @@ -234,32 +239,82 @@ 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` -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: +### Zip64 Support + +By default, Zip64 support is disabled for writing. To enable it do this: ```ruby -Zip.force_entry_names_encoding = 'UTF-8' +Zip.write_zip64_support = true ``` -Allowed encoding names are the same as accepted by `String#force_encoding` +_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: @@ -272,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 9145207b..eeac96a0 100644 --- a/lib/zip.rb +++ b/lib/zip.rb @@ -42,7 +42,8 @@ module Zip :write_zip64_support, :warn_invalid_date, :case_insensitive_match, - :force_entry_names_encoding + :force_entry_names_encoding, + :validate_entry_sizes def reset! @_ran_once = false @@ -54,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/entry.rb b/lib/zip/entry.rb index fddab51e..677e49ef 100644 --- a/lib/zip/entry.rb +++ b/lib/zip/entry.rb @@ -1,3 +1,4 @@ +require 'pathname' module Zip class Entry STORED = 0 @@ -117,7 +118,7 @@ def name_safe? return false unless cleanpath.relative? root = ::File::SEPARATOR naive_expanded_path = ::File.join(root, cleanpath.to_s) - cleanpath.expand_path(root).to_s == naive_expanded_path + ::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path end def local_entry_offset #:nodoc:all @@ -275,10 +276,10 @@ 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, @@ -432,11 +433,11 @@ 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, @@ -602,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 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 cbc2fa8d..72c36764 100644 --- a/lib/zip/extra_field.rb +++ b/lib/zip/extra_field.rb @@ -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/file.rb b/lib/zip/file.rb index 6952ba99..9c7f3cbd 100644 --- a/lib/zip/file.rb +++ b/lib/zip/file.rb @@ -64,24 +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 - if !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 + elsif buffer && path_or_io.size > 0 + # This zip is probably a non-empty StringIO. + read_from_stream(path_or_io) elsif @create + # This zip is completely new/empty and is to be created. @entry_set = EntrySet.new - elsif ::File.zero?(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 @@ -119,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 @@ -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)) diff --git a/lib/zip/inflater.rb b/lib/zip/inflater.rb index ef952f07..f1b26d45 100644 --- a/lib/zip/inflater.rb +++ b/lib/zip/inflater.rb @@ -3,7 +3,7 @@ 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 end diff --git a/lib/zip/version.rb b/lib/zip/version.rb index 14a9f99e..37fba090 100644 --- a/lib/zip/version.rb +++ b/lib/zip/version.rb @@ -1,3 +1,3 @@ module Zip - VERSION = '1.2.2' + VERSION = '1.3.0' end diff --git a/rubyzip.gemspec b/rubyzip.gemspec index 4ca36c2d..6b873752 100644 --- a/rubyzip.gemspec +++ b/rubyzip.gemspec @@ -16,6 +16,13 @@ Gem::Specification.new do |s| 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' 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/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_test.rb b/test/file_test.rb index b23ecb21..94ff769c 100644 --- a/test/file_test.rb +++ b/test/file_test.rb @@ -103,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| @@ -113,14 +120,52 @@ def test_open_buffer_with_stringio def test_close_buffer_with_stringio string_io = StringIO.new File.read('test/data/rubycode.zip') zf = ::Zip::File.open_buffer string_io - assert(zf.close || true) # Poor man's refute_raises + 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_close_buffer_with_io - f = File.open('test/data/rubycode.zip') - zf = ::Zip::File.open_buffer f - assert zf.close - f.close + 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 @@ -159,6 +204,26 @@ 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(0o664, srcZip) diff --git a/test/path_traversal_test.rb b/test/path_traversal_test.rb index 9a361a59..e5bdd722 100644 --- a/test/path_traversal_test.rb +++ b/test/path_traversal_test.rb @@ -131,4 +131,11 @@ def test_entry_name_with_relative_symlink 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