# frozen_string_literal: true

# Utility methods for interacting with POSIX objects; mostly user and group
module Puppet::Util::POSIX
  # This is a list of environment variables that we will set when we want to override the POSIX locale
  LOCALE_ENV_VARS = %w[LANG LC_ALL LC_MESSAGES LANGUAGE
                       LC_COLLATE LC_CTYPE LC_MONETARY LC_NUMERIC LC_TIME]

  # This is a list of user-related environment variables that we will unset when we want to provide a pristine
  # environment for "exec" runs
  USER_ENV_VARS = %w[HOME USER LOGNAME]

  class << self
    # Returns an array of all the groups that the user's a member of.
    def groups_of(user)
      begin
        require_relative '../../puppet/ffi/posix'
        groups = get_groups_list(user)
      rescue StandardError, LoadError => e
        Puppet.debug("Falling back to Puppet::Etc.group: #{e.message}")

        groups = []
        Puppet::Etc.group do |group|
          groups << group.name if group.mem.include?(user)
        end
      end

      uniq_groups = groups.uniq
      if uniq_groups != groups
        Puppet.debug(_('Removing any duplicate group entries'))
      end

      uniq_groups
    end

    private

    def get_groups_list(user)
      raise LoadError, "The 'getgrouplist' method is not available" unless Puppet::FFI::POSIX::Functions.respond_to?(:getgrouplist)

      user_gid = Puppet::Etc.getpwnam(user).gid
      ngroups = Puppet::FFI::POSIX::Constants::MAXIMUM_NUMBER_OF_GROUPS

      loop do
        FFI::MemoryPointer.new(:int) do |ngroups_ptr|
          FFI::MemoryPointer.new(:uint, ngroups) do |groups_ptr|
            old_ngroups = ngroups
            ngroups_ptr.write_int(ngroups)

            if Puppet::FFI::POSIX::Functions.getgrouplist(user, user_gid, groups_ptr, ngroups_ptr) != -1
              groups_gids = groups_ptr.get_array_of_uint(0, ngroups_ptr.read_int)

              result = []
              groups_gids.each do |group_gid|
                group_info = Puppet::Etc.getgrgid(group_gid)
                result |= [group_info.name] if group_info.mem.include?(user)
              end
              return result
            end

            ngroups = ngroups_ptr.read_int
            if ngroups <= old_ngroups
              ngroups *= 2
            end
          end
        end
      end
    end
  end

  # Retrieve a field from a POSIX Etc object.  The id can be either an integer
  # or a name.  This only works for users and groups.  It's also broken on
  # some platforms, unfortunately, which is why we fall back to the other
  # method search_posix_field in the gid and uid methods if a sanity check
  # fails
  def get_posix_field(space, field, id)
    raise Puppet::DevError, _("Did not get id from caller") unless id

    if id.is_a?(Integer)
      if id > Puppet[:maximum_uid].to_i
        Puppet.err _("Tried to get %{field} field for silly id %{id}") % { field: field, id: id }
        return nil
      end
      method = methodbyid(space)
    else
      method = methodbyname(space)
    end

    begin
      Etc.send(method, id).send(field)
    rescue NoMethodError, ArgumentError
      # ignore it; we couldn't find the object
      nil
    end
  end

  # A degenerate method of retrieving name/id mappings.  The job of this method is
  # to retrieve all objects of a certain type, search for a specific entry
  # and then return a given field from that entry.
  def search_posix_field(type, field, id)
    idmethod = idfield(type)
    integer = false
    if id.is_a?(Integer)
      integer = true
      if id > Puppet[:maximum_uid].to_i
        Puppet.err _("Tried to get %{field} field for silly id %{id}") % { field: field, id: id }
        return nil
      end
    end

    Etc.send(type) do |object|
      if integer and object.send(idmethod) == id
        return object.send(field)
      elsif object.name == id
        return object.send(field)
      end
    end

    # Apparently the group/passwd methods need to get reset; if we skip
    # this call, then new users aren't found.
    case type
    when :passwd; Etc.send(:endpwent)
    when :group; Etc.send(:endgrent)
    end
    nil
  end

  # Determine what the field name is for users and groups.
  def idfield(space)
    case space.intern
    when :gr, :group; :gid
    when :pw, :user, :passwd; :uid
    else
      raise ArgumentError, _("Can only handle users and groups")
    end
  end

  # Determine what the method is to get users and groups by id
  def methodbyid(space)
    case space.intern
    when :gr, :group; :getgrgid
    when :pw, :user, :passwd; :getpwuid
    else
      raise ArgumentError, _("Can only handle users and groups")
    end
  end

  # Determine what the method is to get users and groups by name
  def methodbyname(space)
    case space.intern
    when :gr, :group; :getgrnam
    when :pw, :user, :passwd; :getpwnam
    else
      raise ArgumentError, _("Can only handle users and groups")
    end
  end

  # Get the GID
  def gid(group)
    get_posix_value(:group, :gid, group)
  end

  # Get the UID
  def uid(user)
    get_posix_value(:passwd, :uid, user)
  end

  private

  # Get the specified id_field of a given field (user or group),
  # whether an ID name is provided
  def get_posix_value(location, id_field, field)
    begin
      field = Integer(field)
    rescue ArgumentError
      # pass
    end
    if field.is_a?(Integer)
      name = get_posix_field(location, :name, field)
      return nil unless name

      id = get_posix_field(location, id_field, name)
      check_value = id
    else
      id = get_posix_field(location, id_field, field)
      return nil unless id

      name = get_posix_field(location, :name, id)
      check_value = name
    end

    if check_value != field
      check_value_id = get_posix_field(location, id_field, check_value) if check_value

      if id == check_value_id
        Puppet.debug("Multiple entries found for resource: '#{location}' with #{id_field}: #{id}")
        id
      else
        Puppet.debug("The value retrieved: '#{check_value}' is different than the required state: '#{field}', searching in all entries")
        search_posix_field(location, id_field, field)
      end
    else
      id
    end
  end
end
