#
# Author:: Adam Jacob (<adam@opscode.com>)
# Author:: Daniel DeLeo (<dan@opscode.com>)
# Author:: Seth Chisamore (<schisamo@opscode.com>)
# Copyright:: Copyright (c) 2008-2011 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'chef/log'

class Chef
  class FileAccessControl
    module Unix
      UINT = (1 << 32)
      UID_MAX = (1 << 32) - 10

      def set_all
        set_owner
        set_group
        set_mode unless resource.instance_of?(Chef::Resource::Link)
      end

      # Workaround the fact that Ruby's Etc module doesn't believe in negative
      # uids, so negative uids show up as the diminished radix complement of
      # a uint. For example, a uid of -2 is reported as 4294967294
      def diminished_radix_complement(int)
        if int > UID_MAX
          int - UINT
        else
          int
        end
      end

      def target_uid
        return nil if resource.owner.nil?
        if resource.owner.kind_of?(String)
          diminished_radix_complement( Etc.getpwnam(resource.owner).uid )
        elsif resource.owner.kind_of?(Integer)
          resource.owner
        else
          Chef::Log.error("The `owner` parameter of the #@resource resource is set to an invalid value (#{resource.owner.inspect})")
          raise ArgumentError, "cannot resolve #{resource.owner.inspect} to uid, owner must be a string or integer"
        end
      rescue ArgumentError
        raise Chef::Exceptions::UserIDNotFound, "cannot determine user id for '#{resource.owner}', does the user exist on this system?"
      end

      def set_owner
        if (uid = target_uid) && (uid != stat.uid)
          chown(uid, nil, file)
          Chef::Log.info("#{log_string} owner changed to #{uid}")
          modified
        end
      end

      def target_gid
        return nil if resource.group.nil?
        if resource.group.kind_of?(String)
          diminished_radix_complement( Etc.getgrnam(resource.group).gid )
        elsif resource.group.kind_of?(Integer)
          resource.group
        else
          Chef::Log.error("The `group` parameter of the #@resource resource is set to an invalid value (#{resource.owner.inspect})")
          raise ArgumentError, "cannot resolve #{resource.group.inspect} to gid, group must be a string or integer"
        end
      rescue ArgumentError
        raise Chef::Exceptions::GroupIDNotFound, "cannot determine group id for '#{resource.group}', does the group exist on this system?"
      end

      def set_group
        if (gid = target_gid) && (gid != stat.gid)
          chown(nil, gid, file)
          Chef::Log.info("#{log_string} group changed to #{gid}")
          modified
        end
      end

      # TODO rename this to a more generic target_permissions
      def target_mode
        return nil if resource.mode.nil?
        (resource.mode.respond_to?(:oct) ? resource.mode.oct : resource.mode.to_i) & 007777
      end

      # TODO rename this to a more generic set_permissions
      def set_mode
        if (mode = target_mode) && (mode != (stat.mode & 007777))
          File.chmod(target_mode, file)
          Chef::Log.info("#{log_string} mode changed to #{mode.to_s(8)}")
          modified
        end
      end

      def stat
        @stat ||= ::File.stat(file)
      end

      private
      def chown(uid, gid, file)
        if resource.instance_of?(Chef::Resource::Link)
          File.lchown(uid, gid, file)
        else
          File.chown(uid, gid, file)
        end
      end
    end
  end
end