# frozen_string_literal: true

begin
  require "socket"
rescue LoadError
end

require "test/unit"
require "tempfile"
require "timeout"
require "tmpdir"
require "io/nonblock"

class TestSocket_UNIXSocket < Test::Unit::TestCase
  def test_fd_passing
    r1, w = IO.pipe
    s1, s2 = UNIXSocket.pair
    begin
      s1.send_io(nil)
    rescue NotImplementedError
      assert_raise(NotImplementedError) { s2.recv_io }
    rescue TypeError
      s1.send_io(r1)
      r2 = s2.recv_io
      assert_equal(r1.stat.ino, r2.stat.ino)
      assert_not_equal(r1.fileno, r2.fileno)
      assert(r2.close_on_exec?)
      w.syswrite "a"
      assert_equal("a", r2.sysread(10))
    ensure
      s1.close
      s2.close
      w.close
      r1.close
      r2.close if r2 && !r2.closed?
    end
  end

  def test_fd_passing_class_mode
    UNIXSocket.pair do |s1, s2|
      s1.send_io(s1.fileno)
      r = s2.recv_io(nil)
      assert_kind_of Integer, r, 'recv_io with klass=nil returns integer FD'
      assert_not_equal s1.fileno, r
      r = IO.for_fd(r)
      assert_equal s1.stat.ino, r.stat.ino
      r.close

      s1.send_io(s1)
      klass = UNIXSocket
      r = s2.recv_io(klass)
      assert_instance_of klass, r, 'recv_io with proper klass'
      assert_not_equal s1.fileno, r.fileno
      r.close

      s1.send_io(s1)
      klass = IO
      r = s2.recv_io(klass, 'r+')
      assert_instance_of klass, r, 'recv_io with proper klass and mode'
      assert_not_equal s1.fileno, r.fileno
      r.close
    end
  end

  def test_fd_passing_n
    io_ary = []
    return if !defined?(Socket::SCM_RIGHTS)
    io_ary.concat IO.pipe
    io_ary.concat IO.pipe
    io_ary.concat IO.pipe
    send_io_ary = []
    io_ary.each {|io|
      send_io_ary << io
      UNIXSocket.pair {|s1, s2|
        begin
          ret = s1.sendmsg("\0", 0, nil, [Socket::SOL_SOCKET, Socket::SCM_RIGHTS,
                                          send_io_ary.map {|io2| io2.fileno }.pack("i!*")])
        rescue NotImplementedError
          return
        end
        assert_equal(1, ret)
        ret = s2.recvmsg(:scm_rights=>true)
        _, _, _, *ctls = ret
        recv_io_ary = []
        begin
          ctls.each {|ctl|
            next if ctl.level != Socket::SOL_SOCKET || ctl.type != Socket::SCM_RIGHTS
            recv_io_ary.concat ctl.unix_rights
          }
          assert_equal(send_io_ary.length, recv_io_ary.length)
          send_io_ary.length.times {|i|
            assert_not_equal(send_io_ary[i].fileno, recv_io_ary[i].fileno)
            assert(File.identical?(send_io_ary[i], recv_io_ary[i]))
            assert(recv_io_ary[i].close_on_exec?)
          }
        ensure
          recv_io_ary.each {|io2| io2.close if !io2.closed? }
        end
      }
    }
  ensure
    io_ary.each {|io| io.close if !io.closed? }
  end

  def test_fd_passing_n2
    io_ary = []
    return if !defined?(Socket::SCM_RIGHTS)
    return if !defined?(Socket::AncillaryData)
    io_ary.concat IO.pipe
    io_ary.concat IO.pipe
    io_ary.concat IO.pipe
    send_io_ary = []
    io_ary.each {|io|
      send_io_ary << io
      UNIXSocket.pair {|s1, s2|
        begin
          ancdata = Socket::AncillaryData.unix_rights(*send_io_ary)
          ret = s1.sendmsg("\0", 0, nil, ancdata)
        rescue NotImplementedError
          return
        end
        assert_equal(1, ret)
        ret = s2.recvmsg(:scm_rights=>true)
        _, _, _, *ctls = ret
        recv_io_ary = []
        begin
          ctls.each {|ctl|
            next if ctl.level != Socket::SOL_SOCKET || ctl.type != Socket::SCM_RIGHTS
            recv_io_ary.concat ctl.unix_rights
          }
          assert_equal(send_io_ary.length, recv_io_ary.length)
          send_io_ary.length.times {|i|
            assert_not_equal(send_io_ary[i].fileno, recv_io_ary[i].fileno)
            assert(File.identical?(send_io_ary[i], recv_io_ary[i]))
            assert(recv_io_ary[i].close_on_exec?)
          }
        ensure
          recv_io_ary.each {|io2| io2.close if !io2.closed? }
        end
      }
    }
  ensure
    io_ary.each {|io| io.close if !io.closed? }
  end

  def test_fd_passing_race_condition
    r1, w = IO.pipe
    s1, s2 = UNIXSocket.pair
    s1.nonblock = s2.nonblock = true
    lock = Thread::Mutex.new
    nr = 0
    x = 2
    y = 1000
    begin
      s1.send_io(nil)
    rescue NotImplementedError
      assert_raise(NotImplementedError) { s2.recv_io }
    rescue TypeError
      thrs = x.times.map do
        Thread.new do
          y.times do
            s2.recv_io.close
            lock.synchronize { nr += 1 }
          end
          true
        end
      end
      (x * y).times { s1.send_io r1 }
      assert_equal([true]*x, thrs.map { |t| t.value })
      assert_equal x * y, nr
    ensure
      s1.close
      s2.close
      w.close
      r1.close
    end
  end

  def test_sendmsg
    return if !defined?(Socket::SCM_RIGHTS)
    IO.pipe {|r1, w|
      UNIXSocket.pair {|s1, s2|
        begin
          ret = s1.sendmsg("\0", 0, nil, [Socket::SOL_SOCKET, Socket::SCM_RIGHTS, [r1.fileno].pack("i!")])
        rescue NotImplementedError
          return
        end
        assert_equal(1, ret)
        r2 = s2.recv_io
        begin
          assert(File.identical?(r1, r2))
          assert(r2.close_on_exec?)
        ensure
          r2.close
        end
      }
    }
  end

  def test_sendmsg_ancillarydata_int
    return if !defined?(Socket::SCM_RIGHTS)
    return if !defined?(Socket::AncillaryData)
    IO.pipe {|r1, w|
      UNIXSocket.pair {|s1, s2|
        begin
          ad = Socket::AncillaryData.int(:UNIX, :SOCKET, :RIGHTS, r1.fileno)
          ret = s1.sendmsg("\0", 0, nil, ad)
        rescue NotImplementedError
          return
        end
        assert_equal(1, ret)
        r2 = s2.recv_io
        begin
          assert(File.identical?(r1, r2))
        ensure
          r2.close
        end
      }
    }
  end

  def test_sendmsg_ancillarydata_unix_rights
    return if !defined?(Socket::SCM_RIGHTS)
    return if !defined?(Socket::AncillaryData)
    IO.pipe {|r1, w|
      UNIXSocket.pair {|s1, s2|
        begin
          ad = Socket::AncillaryData.unix_rights(r1)
          ret = s1.sendmsg("\0", 0, nil, ad)
        rescue NotImplementedError
          return
        end
        assert_equal(1, ret)
        r2 = s2.recv_io
        begin
          assert(File.identical?(r1, r2))
        ensure
          r2.close
        end
      }
    }
  end

  def test_recvmsg
    return if !defined?(Socket::SCM_RIGHTS)
    return if !defined?(Socket::AncillaryData)
    IO.pipe {|r1, w|
      UNIXSocket.pair {|s1, s2|
        s1.send_io(r1)
        ret = s2.recvmsg(:scm_rights=>true)
        data, srcaddr, flags, *ctls = ret
        assert_equal("\0", data)
	if flags == nil
	  # struct msghdr is 4.3BSD style (msg_accrights field).
	  assert_instance_of(Array, ctls)
	  assert_equal(0, ctls.length)
	else
	  # struct msghdr is POSIX/4.4BSD style (msg_control field).
	  assert_equal(0, flags & (Socket::MSG_TRUNC|Socket::MSG_CTRUNC))
	  assert_instance_of(Addrinfo, srcaddr)
	  assert_instance_of(Array, ctls)
	  assert_equal(1, ctls.length)
          ctl = ctls[0]
	  assert_instance_of(Socket::AncillaryData, ctl)
	  assert_equal(Socket::SOL_SOCKET, ctl.level)
	  assert_equal(Socket::SCM_RIGHTS, ctl.type)
	  assert_instance_of(String, ctl.data)
          ios = ctl.unix_rights
          assert_equal(1, ios.length)
	  r2 = ios[0]
	  begin
	    assert(File.identical?(r1, r2))
            assert(r2.close_on_exec?)
	  ensure
	    r2.close
	  end
	end
      }
    }
  end

  def bound_unix_socket(klass)
    tmpfile = Tempfile.new("s")
    path = tmpfile.path
    tmpfile.close(true)
    io = klass.new(path)
    yield io, path
  ensure
    io.close
    File.unlink path if path && File.socket?(path)
  end

  def test_open_nul_byte
    tmpfile = Tempfile.new("s")
    path = tmpfile.path
    tmpfile.close(true)
    assert_raise(ArgumentError) {UNIXServer.open(path+"\0")}
    assert_raise(ArgumentError) {UNIXSocket.open(path+"\0")}
  ensure
    File.unlink path if path && File.socket?(path)
  end

  def test_addr
    bound_unix_socket(UNIXServer) {|serv, path|
      UNIXSocket.open(path) {|c|
        s = serv.accept
        begin
          assert_equal(["AF_UNIX", path], c.peeraddr)
          assert_equal(["AF_UNIX", ""], c.addr)
          assert_equal(["AF_UNIX", ""], s.peeraddr)
          assert_equal(["AF_UNIX", path], s.addr)
          assert_equal(path, s.path)
          assert_equal("", c.path)
        ensure
          s.close
        end
      }
    }
  end

  def test_cloexec
    bound_unix_socket(UNIXServer) {|serv, path|
      UNIXSocket.open(path) {|c|
        s = serv.accept
        begin
          assert(serv.close_on_exec?)
          assert(c.close_on_exec?)
          assert(s.close_on_exec?)
        ensure
          s.close
        end
      }
    }
  end

  def test_noname_path
    s1, s2 = UNIXSocket.pair
    assert_equal("", s1.path)
    assert_equal("", s2.path)
  ensure
    s1.close
    s2.close
  end

  def test_noname_addr
    s1, s2 = UNIXSocket.pair
    assert_equal(["AF_UNIX", ""], s1.addr)
    assert_equal(["AF_UNIX", ""], s2.addr)
  ensure
    s1.close
    s2.close
  end

  def test_noname_peeraddr
    s1, s2 = UNIXSocket.pair
    assert_equal(["AF_UNIX", ""], s1.peeraddr)
    assert_equal(["AF_UNIX", ""], s2.peeraddr)
  ensure
    s1.close
    s2.close
  end

  def test_noname_unpack_sockaddr_un
    s1, s2 = UNIXSocket.pair
    n = nil
    assert_equal("", Socket.unpack_sockaddr_un(n)) if (n = s1.getsockname) != ""
    assert_equal("", Socket.unpack_sockaddr_un(n)) if (n = s1.getsockname) != ""
    assert_equal("", Socket.unpack_sockaddr_un(n)) if (n = s2.getsockname) != ""
    assert_equal("", Socket.unpack_sockaddr_un(n)) if (n = s1.getpeername) != ""
    assert_equal("", Socket.unpack_sockaddr_un(n)) if (n = s2.getpeername) != ""
  ensure
    s1.close
    s2.close
  end

  def test_noname_recvfrom
    s1, s2 = UNIXSocket.pair
    s2.write("a")
    assert_equal(["a", ["AF_UNIX", ""]], s1.recvfrom(10))
  ensure
    s1.close
    s2.close
  end

  def test_noname_recv_nonblock
    s1, s2 = UNIXSocket.pair
    s2.write("a")
    IO.select [s1]
    assert_equal("a", s1.recv_nonblock(10))
  ensure
    s1.close
    s2.close
  end

  def test_too_long_path
    assert_raise(ArgumentError) { Socket.sockaddr_un("a" * 3000) }
    assert_raise(ArgumentError) { UNIXServer.new("a" * 3000) }
  end

  def test_abstract_namespace
    return if /linux/ !~ RUBY_PLATFORM
    addr = Socket.pack_sockaddr_un("\0foo")
    assert_match(/\0foo\z/, addr)
    assert_equal("\0foo", Socket.unpack_sockaddr_un(addr))
  end

  def test_dgram_pair
    s1, s2 = UNIXSocket.pair(Socket::SOCK_DGRAM)
    begin
      s1.recv_nonblock(10)
      fail
    rescue => e
      assert(IO::EAGAINWaitReadable === e)
      assert(IO::WaitReadable === e)
    end
    s2.send("", 0)
    s2.send("haha", 0)
    s2.send("", 0)
    s2.send("", 0)
    assert_equal("", s1.recv(10))
    assert_equal("haha", s1.recv(10))
    assert_equal("", s1.recv(10))
    assert_equal("", s1.recv(10))
    assert_raise(IO::EAGAINWaitReadable) { s1.recv_nonblock(10) }

    buf = "".dup
    s2.send("BBBBBB", 0)
    IO.select([s1])
    rv = s1.recv(100, 0, buf)
    assert_equal buf.object_id, rv.object_id
    assert_equal "BBBBBB", rv
  ensure
    s1.close if s1
    s2.close if s2
  end

  def test_dgram_pair_sendrecvmsg_errno_set
    s1, s2 = to_close = UNIXSocket.pair(Socket::SOCK_DGRAM)
    pipe = IO.pipe
    to_close.concat(pipe)
    set_errno = lambda do
      begin
        pipe[0].read_nonblock(1)
        fail
      rescue => e
        assert(IO::EAGAINWaitReadable === e)
      end
    end
    Timeout.timeout(10) do
      set_errno.call
      assert_equal(2, s1.sendmsg("HI"))
      set_errno.call
      assert_equal("HI", s2.recvmsg[0])
    end
  ensure
    to_close.each(&:close) if to_close
  end

  def test_epipe # [ruby-dev:34619]
    UNIXSocket.pair {|s1, s2|
      s1.shutdown(Socket::SHUT_WR)
      assert_raise(Errno::EPIPE) { s1.write "a" }
      assert_equal(nil, s2.read(1))
      s2.write "a"
      assert_equal("a", s1.read(1))
    }
  end

  def test_socket_pair_with_block
    pair = nil
    ret = Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0) {|s1, s2|
      pair = [s1, s2]
      :return_value
    }
    assert_equal(:return_value, ret)
    assert_kind_of(Socket, pair[0])
    assert_kind_of(Socket, pair[1])
  end

  def test_unix_socket_pair_with_block
    pair = nil
    UNIXSocket.pair {|s1, s2|
      pair = [s1, s2]
    }
    assert_kind_of(UNIXSocket, pair[0])
    assert_kind_of(UNIXSocket, pair[1])
  end

  def test_unix_socket_pair_close_on_exec
    UNIXSocket.pair {|s1, s2|
      assert(s1.close_on_exec?)
      assert(s2.close_on_exec?)
    }
  end

  def test_initialize
    Dir.mktmpdir {|d|
      Socket.open(Socket::AF_UNIX, Socket::SOCK_STREAM, 0) {|s|
	s.bind(Socket.pack_sockaddr_un("#{d}/s1"))
	addr = s.getsockname
	assert_nothing_raised { Socket.unpack_sockaddr_un(addr) }
	assert_raise(ArgumentError) { Socket.unpack_sockaddr_in(addr) }
      }
      Socket.open("AF_UNIX", "SOCK_STREAM", 0) {|s|
	s.bind(Socket.pack_sockaddr_un("#{d}/s2"))
	addr = s.getsockname
	assert_nothing_raised { Socket.unpack_sockaddr_un(addr) }
	assert_raise(ArgumentError) { Socket.unpack_sockaddr_in(addr) }
      }
    }
  end

  def test_unix_server_socket
    Dir.mktmpdir {|d|
      path = "#{d}/sock"
      s0 = nil
      Socket.unix_server_socket(path) {|s|
        assert_equal(path, s.local_address.unix_path)
        assert(File.socket?(path))
        s0 = s
      }
      assert(s0.closed?)
      assert_raise(Errno::ENOENT) { File.stat path }
    }
  end

  def test_getcred_ucred
    return if /linux/ !~ RUBY_PLATFORM
    Dir.mktmpdir {|d|
      sockpath = "#{d}/sock"
      Socket.unix_server_socket(sockpath) {|serv|
        Socket.unix(sockpath) {|c|
          s, = serv.accept
          begin
            cred = s.getsockopt(:SOCKET, :PEERCRED)
            inspect = cred.inspect
            assert_match(/ pid=#{$$} /, inspect)
            assert_match(/ euid=#{Process.euid} /, inspect)
            assert_match(/ egid=#{Process.egid} /, inspect)
            assert_match(/ \(ucred\)/, inspect)
          ensure
            s.close
          end
        }
      }
    }
  end

  def test_getcred_xucred
    return if /freebsd|darwin/ !~ RUBY_PLATFORM
    Dir.mktmpdir do |d|
      sockpath = "#{d}/sock"
      serv = Socket.unix_server_socket(sockpath)
      u = Socket.unix(sockpath)
      s, = serv.accept
      cred = s.getsockopt(0, Socket::LOCAL_PEERCRED)
      inspect = cred.inspect
      assert_match(/ euid=#{Process.euid} /, inspect)
      assert_match(/ \(xucred\)/, inspect)
    ensure
      s&.close
      u&.close
      serv&.close
    end
  end

  def test_sendcred_ucred
    return if /linux/ !~ RUBY_PLATFORM
    Dir.mktmpdir {|d|
      sockpath = "#{d}/sock"
      Socket.unix_server_socket(sockpath) {|serv|
        Socket.unix(sockpath) {|c|
          s, = serv.accept
          begin
            s.setsockopt(:SOCKET, :PASSCRED, 1)
            c.print "a"
            msg, _, _, cred = s.recvmsg
            inspect = cred.inspect
            assert_equal("a", msg)
            assert_match(/ pid=#{$$} /, inspect)
            assert_match(/ uid=#{Process.uid} /, inspect)
            assert_match(/ gid=#{Process.gid} /, inspect)
            assert_match(/ \(ucred\)/, inspect)
          ensure
            s.close
          end
        }
      }
    }
  end

  def test_sendcred_sockcred
    return if /netbsd|freebsd/ !~ RUBY_PLATFORM
    Dir.mktmpdir {|d|
      sockpath = "#{d}/sock"
      serv = Socket.unix_server_socket(sockpath)
      c = Socket.unix(sockpath)
      s, = serv.accept
      s.setsockopt(0, Socket::LOCAL_CREDS, 1)
      c.print "a"
      msg, _, _, cred = s.recvmsg
      assert_equal("a", msg)
      inspect = cred.inspect
      assert_match(/ uid=#{Process.uid} /, inspect)
      assert_match(/ euid=#{Process.euid} /, inspect)
      assert_match(/ gid=#{Process.gid} /, inspect)
      assert_match(/ egid=#{Process.egid} /, inspect)
      assert_match(/ \(sockcred\)/, inspect)
    }
  end

  def test_sendcred_cmsgcred
    return if /freebsd/ !~ RUBY_PLATFORM
    Dir.mktmpdir {|d|
      sockpath = "#{d}/sock"
      serv = Socket.unix_server_socket(sockpath)
      c = Socket.unix(sockpath)
      s, = serv.accept
      c.sendmsg("a", 0, nil, [:SOCKET, Socket::SCM_CREDS, ""])
      msg, _, _, cred = s.recvmsg
      assert_equal("a", msg)
      inspect = cred.inspect
      assert_match(/ pid=#{$$} /, inspect)
      assert_match(/ uid=#{Process.uid} /, inspect)
      assert_match(/ euid=#{Process.euid} /, inspect)
      assert_match(/ gid=#{Process.gid} /, inspect)
      assert_match(/ \(cmsgcred\)/, inspect)
    }
  end

  def test_getpeereid
    Dir.mktmpdir {|d|
      path = "#{d}/sock"
      Socket.unix_server_socket(path) {|serv|
        Socket.unix(path) {|c|
          s, = serv.accept
          begin
            assert_equal([Process.euid, Process.egid], c.getpeereid)
            assert_equal([Process.euid, Process.egid], s.getpeereid)
          rescue NotImplementedError
          ensure
            s.close
          end
        }
      }
    }
  end

  def test_abstract_unix_server
    return if /linux/ !~ RUBY_PLATFORM
    name = "\0ruby-test_unix"
    s0 = nil
    UNIXServer.open(name) {|s|
      assert_equal(name, s.local_address.unix_path)
      s0 = s
      UNIXSocket.open(name) {|c|
        sock = s.accept
        begin
          assert_equal(name, c.remote_address.unix_path)
        ensure
          sock.close
        end
      }
    }
    assert(s0.closed?)
  end

  def test_abstract_unix_socket_econnrefused
    return if /linux/ !~ RUBY_PLATFORM
    name = "\0ruby-test_unix"
    assert_raise(Errno::ECONNREFUSED) do
      UNIXSocket.open(name) {}
    end
  end

  def test_abstract_unix_server_socket
    return if /linux/ !~ RUBY_PLATFORM
    name = "\0ruby-test_unix"
    s0 = nil
    Socket.unix_server_socket(name) {|s|
      assert_equal(name, s.local_address.unix_path)
      s0 = s
      Socket.unix(name) {|c|
        sock, = s.accept
        begin
          assert_equal(name, c.remote_address.unix_path)
        ensure
          sock.close
        end
      }
    }
    assert(s0.closed?)
  end

  def test_autobind
    return if /linux/ !~ RUBY_PLATFORM
    s0 = nil
    Socket.unix_server_socket("") {|s|
      name = s.local_address.unix_path
      assert_match(/\A\0[0-9a-f]{5}\z/, name)
      s0 = s
      Socket.unix(name) {|c|
        sock, = s.accept
        begin
          assert_equal(name, c.remote_address.unix_path)
        ensure
          sock.close
        end
      }
    }
    assert(s0.closed?)
  end

  def test_accept_nonblock
    bound_unix_socket(UNIXServer) {|serv, path|
      assert_raise(IO::WaitReadable) { serv.accept_nonblock }
      assert_raise(IO::WaitReadable) { serv.accept_nonblock(exception: true) }
      assert_equal :wait_readable, serv.accept_nonblock(exception: false)
    }
  end
end if defined?(UNIXSocket) && /cygwin/ !~ RUBY_PLATFORM
