# The file mode conversion functions in this file are based on those
# of Ruby's FileUtils, more specifically some methods found in
# lib/fileutils.rb in MRI 2.1.1.
#
# The following is a verbatim copy of the original license:
#
#   Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
#
#   Redistribution and use in source and binary forms, with or without
#   modification, are permitted provided that the following conditions
#   are met:
#   1. Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.
#   2. Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in the
#   documentation and/or other materials provided with the distribution.
#
#   THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
#   ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#   ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
#   FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
#   DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
#   OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
#   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
#   LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
#   OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
#   SUCH DAMAGE.

module Wright
  module Util
    # Various file methods.
    module File
      USER_MAP = {
        'u' => 04700,
        'g' => 02070,
        'o' => 01007,
        'a' => 07777
      }
      private_constant :USER_MAP

      def self.user_mask(target)
        target.chars.reduce(0) { |a, e| a | USER_MAP[e] }
      end
      private_class_method :user_mask

      MODE_MAP = {
        'r' => 0444,
        'w' => 0222,
        'x' => 0111,
        's' => 06000,
        't' => 01000
      }
      private_constant :MODE_MAP

      def self.mode_mask(mode, is_directory)
        mode.tr!('X', 'x') if is_directory
        mode.chars.reduce(0) { |a, e| a | MODE_MAP[e].to_i }
      end
      private_class_method :mode_mask

      # Converts a symbolic mode string to an integer mode value.
      #
      # @param mode [String] the symbolic mode string
      # @param base_mode [Integer] the base mode
      # @param filetype [Symbol] the filetype
      #
      # @example
      #   Wright::Util::File.symbolic_mode_to_i('u=rw,go=r', 0400).to_s(8)
      #   # => "644"
      #
      #   Wright::Util::File.symbolic_mode_to_i('u=rw,g+r', 0200).to_s(8)
      #   # => "640"
      #
      # @return [Integer] the integer mode
      def self.symbolic_mode_to_i(mode, base_mode, filetype = :file)
        is_directory = (filetype == :directory)
        unless symbolic_mode?(mode)
          fail ArgumentError, "Invalid file mode \"#{mode}\""
        end
        mode_i = base_mode
        mode.split(/,/).each do |mode_clause|
          mode_i = mode_clause_to_i(mode_clause, mode_i, is_directory)
        end
        mode_i
      end

      # Converts a single symbolic mode clause to an integer mode
      # value.
      #
      # @param mode_clause [String] the symbolic mode clause
      # @param base_mode_i [Integer] the integer base mode
      # @param is_directory [Bool] denotes whether the mode_clause
      #   should be treated as a symbolic directory mode clause
      #
      # @example
      #   Wright::Util::File.mode_clause_to_i('g+r', 0600, false).to_s(8)
      #   # => "640"
      #
      #   Wright::Util::File.mode_clause_to_i('+rw', 0600, false).to_s(8)
      #   # => "666"
      #
      # @return [Integer] the mode clause as an integer
      def self.mode_clause_to_i(mode_clause, base_mode_i, is_directory)
        mode_clause = "a#{mode_clause}" if mode_clause =~ /\A[+-=]/
        who, op, perm = mode_clause.split(/([+-=])/)
        perm ||= ''
        user_mask = user_mask(who)
        mode_mask = mode_mask(perm, is_directory)
        apply_user_mode_masks(base_mode_i, user_mask, op, mode_mask)
      end
      private_class_method :mode_clause_to_i

      def self.apply_user_mode_masks(base_mode_i, user_mask, op, mode_mask)
        case op
        when '='
          (base_mode_i & ~user_mask) | (user_mask & mode_mask)
        when '+'
          base_mode_i | (user_mask & mode_mask)
        when '-'
          base_mode_i & ~(user_mask & mode_mask)
        end
      end
      private_class_method :apply_user_mode_masks

      # Converts a numeric mode string to an integer mode.
      #
      # @param mode [String, #to_i] the numeric mode string
      #
      # @example
      #   Wright::Util::File.numeric_mode_to_i('0600').to_s(8)
      #   # => "600"
      #
      #   Wright::Util::File.numeric_mode_to_i('644').to_s(8)
      #   # => "644"
      #
      #   Wright::Util::File.numeric_mode_to_i(0644).to_s(8)
      #   # => "644"
      #
      #   Wright::Util::File.numeric_mode_to_i('invalid_mode').to_s(8)
      #   # => nil
      #
      # @return [Integer] the mode in integer form or +nil+ if the
      #   mode could not be converted
      def self.numeric_mode_to_i(mode)
        return mode.to_i unless mode.is_a?(String)
        mode =~ /\A[0-7]{3,4}\Z/ ? mode.to_i(8) : nil
      end

      def self.symbolic_mode?(mode_str)
        return true if mode_str.empty?
        mode_fragment = /([augo]*[+-=][rwxXst]*)/
        mode_re = /\A#{mode_fragment}(,#{mode_fragment})*\Z/
        !(mode_str =~ mode_re).nil?
      end
      private_class_method :symbolic_mode?

      # Returns a file's current mode.
      #
      # @param path [String] the file's path
      #
      # @example
      #   FileUtils.touch('foo')
      #   FileUtils.chmod(0644, 'foo')
      #   Wright::Util::File.file_mode('foo').to_s(8)
      #   # => "644"
      #
      # @return [Integer] the file mode as an integer or +nil+ if the
      #   file does not exist
      def self.file_mode(path)
        ::File.exist?(path) ? (::File.stat(path).mode & 07777) : nil
      end

      # Returns a file's owner.
      #
      # @param path [String] the file's path
      #
      # @example
      #   FileUtils.touch('foo')
      #   FileUtils.chown(0, 0, 'foo')
      #   Wright::Util::File.file_owner('foo')
      #   # => 0
      #
      #   Wright::Util::File.file_owner('nonexistent')
      #   # => nil
      #
      # @return [Integer] the file owner's uid or +nil+ if the file
      #   does not exist
      def self.file_owner(path)
        ::File.exist?(path) ? ::File.stat(path).uid : nil
      end

      # Returns a file's owner.
      #
      # @param path [String] the file's path
      #
      # @example
      #   FileUtils.touch('foo')
      #   FileUtils.chown(0, 0, 'foo')
      #   Wright::Util::File.file_group('foo')
      #   # => 0
      #
      #   Wright::Util::File.file_group('nonexistent')
      #   # => nil
      #
      # @return [Integer] the file owner's uid or nil if the file does
      #   not exist.
      def self.file_group(path)
        ::File.exist?(path) ? ::File.stat(path).gid : nil
      end

      # Expands tilde symbols in file paths. Path elements other than
      # the first one are left alone.
      #
      # @param path [String] the file path
      #
      # @example
      #   Wright::Util::File.expand_tilde_path('~root/foo')
      #   # => "/root/foo"
      #
      #   Wright::Util::File.expand_tilde_path('~root/foo/..')
      #   # => "/root/foo/.."
      #
      #   Wright::Util::File.expand_tilde_path('../foo/bar')
      #   # => "../foo/bar"
      #
      # @return [String] the expanded path
      def self.expand_tilde_path(path)
        return path unless path.start_with?('~')

        first, *rest = path.split(::File::SEPARATOR)
        ::File.join(::File.expand_path(first), rest)
      end

      # Creates symlinks without descending into directories.
      #
      # If the file denoted by link_name is a symlink to a directory,
      # {ln_sfn} does not descend into it. Behaves similar to GNU
      # ln(1) or OpenBSD ln(1) when using +ln -sfn target link_name+.
      #
      # @param target [String] the link target
      # @param link_name [String] the link name
      #
      # @return [void]
      def self.ln_sfn(target, link_name)
        if ::File.symlink?(link_name) && ::File.directory?(link_name)
          FileUtils.rm(link_name)
        end
        FileUtils.ln_sf(target, link_name)
      end
    end
  end
end
