Repository: rubyzip/rubyzip (Ruby)

Issue:
Set correct file permissions given a umask of 0027
We lock down our umask to 0027 so that files are not globally readable. When creating files via `File.open` this results in files with permissions of 0640 (i.e. `rw-r----`) but zip files created with this library end up with the very odd permission of 0637 (e.g. `rw--wxrwx`).

[I've added a failing test case to `test/file_permissions_test.rb`](https://github.com/rubyzip/rubyzip/compare/7aa3666e341b4ecf3b66321d734802a77bba962b...metavida:umask-0027) on my branch but haven't had time to actually write a solution nor to do any testing on windows.



Discussion:
Note, I believe bug exists because the permission calculation introduced in Pull #237 relies on direct subtraction (0666 - 0027 = 0637) and in reality linux must not actually use direct subtraction internally.

I've had a go at fixing this in #300. Can people comment on whether how I've fixed it is sensible?




Patch:
diff --git a/lib/zip/file.rb b/lib/zip/file.rb
index 77f00d17..75bc7b3b 100644
--- a/lib/zip/file.rb
+++ b/lib/zip/file.rb
@@ -43,7 +43,7 @@ module Zip
   # interface for accessing the filesystem, ie. the File and Dir classes.
 
   class File < CentralDirectory
-    CREATE               = 1
+    CREATE               = true
     SPLIT_SIGNATURE      = 0x08074b50
     ZIP64_EOCD_SIGNATURE = 0x06064b50
     MAX_SEGMENT_SIZE     = 3_221_225_472
@@ -64,20 +64,19 @@ 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 = nil, buffer = false, options = {})
+    def initialize(file_name, create = false, buffer = false, options = {})
       super()
       @name    = file_name
       @comment = ''
-      @create  = create
+      @create  = create ? true : false # allow any truthy value to mean true
       case
       when !buffer && ::File.size?(file_name)
-        @create = nil
+        @create = false
         @file_permissions = ::File.stat(file_name).mode
         ::File.open(name, 'rb') do |f|
           read_from_stream(f)
         end
-      when create
-        @file_permissions = create_file_permissions
+      when @create
         @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?"
@@ -95,7 +94,7 @@ class << self
       # Same as #new. If a block is passed the ZipFile object is passed
       # to the block and is automatically closed afterwards just as with
       # ruby's builtin File.open method.
-      def open(file_name, create = nil)
+      def open(file_name, create = false)
         zf = ::Zip::File.new(file_name, create)
         return zf unless block_given?
         begin
@@ -340,7 +339,7 @@ def commit_required?
       @entry_set.each do |e|
         return true if e.dirty
       end
-      @comment != @stored_comment || @entry_set != @stored_entries || @create == ::Zip::File::CREATE
+      @comment != @stored_comment || @entry_set != @stored_entries || @create
     end
 
     # Searches for entry with the specified name. Returns nil if
@@ -403,27 +402,18 @@ def check_file(path)
     end
 
     def on_success_replace
-      tmp_filename = create_tmpname
-      if yield tmp_filename
-        ::File.rename(tmp_filename, name)
-        ::File.chmod(@file_permissions, name) if defined?(@file_permissions)
-      end
-    ensure
-      ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
-    end
-
-    def create_tmpname
       dirname, basename = ::File.split(name)
-      ::Dir::Tmpname.create(basename, dirname) do |tmpname|
-        opts = {perm: 0600, mode: ::File::CREAT | ::File::WRONLY | ::File::EXCL}
-        f = File.open(tmpname, opts)
-        f.close
+      ::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
+        begin
+          if yield tmp_filename
+            ::File.rename(tmp_filename, name)
+            ::File.chmod(@file_permissions, name) unless @create
+          end
+        ensure
+          ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
+        end
       end
     end
-
-    def create_file_permissions
-      ::Zip::RUNNING_ON_WINDOWS ? 0644 : 0666 - ::File.umask
-    end
   end
 end
 


Test patch:
diff --git a/test/file_permissions_test.rb b/test/file_permissions_test.rb
index 67ae30b0..fd666b88 100644
--- a/test/file_permissions_test.rb
+++ b/test/file_permissions_test.rb
@@ -2,58 +2,58 @@
 
 class FilePermissionsTest < MiniTest::Test
 
-  FILENAME = File.join(File.dirname(__FILE__), "umask.zip")
+  ZIPNAME = File.join(File.dirname(__FILE__), "umask.zip")
+  FILENAME = File.join(File.dirname(__FILE__), "umask.txt")
 
   def teardown
+    ::File.unlink(ZIPNAME)
     ::File.unlink(FILENAME)
   end
 
-  if ::Zip::RUNNING_ON_WINDOWS
-    # Windows tests
-
-    DEFAULT_PERMS = 0644
-
-    def test_windows_perms
-      create_file
+  def test_current_umask
+    create_files
+    assert_matching_permissions FILENAME, ZIPNAME
+  end
 
-      assert_equal DEFAULT_PERMS, ::File.stat(FILENAME).mode
+  def test_umask_000
+    set_umask(0000) do
+      create_files
     end
 
-  else
-    # Unix tests
-
-    DEFAULT_PERMS = 0100666
-
-    def test_current_umask
-      umask = DEFAULT_PERMS - ::File.umask
-      create_file
+    assert_matching_permissions FILENAME, ZIPNAME
+  end
 
-      assert_equal umask, ::File.stat(FILENAME).mode
+  def test_umask_066
+    set_umask(0066) do
+      create_files
     end
 
-    def test_umask_000
-      set_umask(0000) do
-        create_file
-      end
+    assert_matching_permissions FILENAME, ZIPNAME
+  end
 
-      assert_equal DEFAULT_PERMS, ::File.stat(FILENAME).mode
+  def test_umask_027
+    set_umask(0027) do
+      create_files
     end
 
-    def test_umask_066
-      umask = 0066
-      set_umask(umask) do
-        create_file
-      end
-
-      assert_equal((DEFAULT_PERMS - umask), ::File.stat(FILENAME).mode)
-    end
+    assert_matching_permissions FILENAME, ZIPNAME
+  end
 
+  def assert_matching_permissions(expected_file, actual_file)
+    assert_equal(
+      ::File.stat(expected_file).mode.to_s(8).rjust(4, '0'),
+      ::File.stat(actual_file).mode.to_s(8).rjust(4, '0')
+    )
   end
 
-  def create_file
-    ::Zip::File.open(FILENAME, ::Zip::File::CREATE) do |zip|
+  def create_files
+    ::Zip::File.open(ZIPNAME, ::Zip::File::CREATE) do |zip|
       zip.comment = "test"
     end
+
+    ::File.open(FILENAME, 'w') do |file|
+      file << 'test'
+    end
   end
 
   # If anything goes wrong, make sure the umask is restored.
diff --git a/test/file_test.rb b/test/file_test.rb
index c703b199..de0dfef5 100644
--- a/test/file_test.rb
+++ b/test/file_test.rb
@@ -40,6 +40,20 @@ def test_create_from_scratch
     assert_equal(2, zfRead.entries.length)
   end
 
+  def test_create_from_scratch_with_old_create_parameter
+    comment = 'a short comment'
+
+    zf = ::Zip::File.new(EMPTY_FILENAME, 1)
+    zf.get_output_stream('myFile') { |os| os.write 'myFile contains just this' }
+    zf.mkdir('dir1')
+    zf.comment = comment
+    zf.close
+
+    zfRead = ::Zip::File.new(EMPTY_FILENAME)
+    assert_equal(comment, zfRead.comment)
+    assert_equal(2, zfRead.entries.length)
+  end
+
   def test_get_output_stream
     entryCount = nil
     ::Zip::File.open(TEST_ZIP.zip_name) do |zf|