/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * 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.
 */

package libcore.io;

import android.system.ErrnoException;
import android.system.NetlinkSocketAddress;
import android.system.OsConstants;
import android.system.PacketSocketAddress;
import android.system.StructTimeval;
import android.system.StructUcred;
import android.system.UnixSocketAddress;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.SocketOptions;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
import junit.framework.TestCase;
import static android.system.OsConstants.*;

public class OsTest extends TestCase {
  public void testIsSocket() throws Exception {
    File f = new File("/dev/null");
    FileInputStream fis = new FileInputStream(f);
    assertFalse(S_ISSOCK(Libcore.os.fstat(fis.getFD()).st_mode));
    fis.close();

    ServerSocket s = new ServerSocket();
    assertTrue(S_ISSOCK(Libcore.os.fstat(s.getImpl().getFD$()).st_mode));
    s.close();
  }

  public void testFcntlInt() throws Exception {
    File f = File.createTempFile("OsTest", "tst");
    FileInputStream fis = null;
    try {
      fis = new FileInputStream(f);
      Libcore.os.fcntlInt(fis.getFD(), F_SETFD, FD_CLOEXEC);
      int flags = Libcore.os.fcntlVoid(fis.getFD(), F_GETFD);
      assertTrue((flags & FD_CLOEXEC) != 0);
    } finally {
      IoUtils.closeQuietly(fis);
      f.delete();
    }
  }

  public void testUnixDomainSockets_in_file_system() throws Exception {
    String path = System.getProperty("java.io.tmpdir") + "/test_unix_socket";
    new File(path).delete();
    checkUnixDomainSocket(UnixSocketAddress.createFileSystem(path), false);
  }

  public void testUnixDomainSocket_abstract_name() throws Exception {
    // Linux treats a sun_path starting with a NUL byte as an abstract name. See unix(7).
    checkUnixDomainSocket(UnixSocketAddress.createAbstract("/abstract_name_unix_socket"), true);
  }

  public void testUnixDomainSocket_unnamed() throws Exception {
    final FileDescriptor fd = Libcore.os.socket(AF_UNIX, SOCK_STREAM, 0);
    // unix(7) says an unbound socket is unnamed.
    checkNoSockName(fd);
    Libcore.os.close(fd);
  }

  private void checkUnixDomainSocket(final UnixSocketAddress address, final boolean isAbstract)
      throws Exception {
    final FileDescriptor serverFd = Libcore.os.socket(AF_UNIX, SOCK_STREAM, 0);
    Libcore.os.bind(serverFd, address);
    Libcore.os.listen(serverFd, 5);

    checkSockName(serverFd, isAbstract, address);

    Thread server = new Thread(new Runnable() {
      public void run() {
        try {
          UnixSocketAddress peerAddress = UnixSocketAddress.createUnnamed();
          FileDescriptor clientFd = Libcore.os.accept(serverFd, peerAddress);
          checkSockName(clientFd, isAbstract, address);
          checkNoName(peerAddress);

          checkNoPeerName(clientFd);

          StructUcred credentials = Libcore.os.getsockoptUcred(clientFd, SOL_SOCKET, SO_PEERCRED);
          assertEquals(Libcore.os.getpid(), credentials.pid);
          assertEquals(Libcore.os.getuid(), credentials.uid);
          assertEquals(Libcore.os.getgid(), credentials.gid);

          byte[] request = new byte[256];
          Libcore.os.read(clientFd, request, 0, request.length);

          String s = new String(request, "UTF-8");
          byte[] response = s.toUpperCase(Locale.ROOT).getBytes("UTF-8");
          Libcore.os.write(clientFd, response, 0, response.length);

          Libcore.os.close(clientFd);
        } catch (Exception ex) {
          throw new RuntimeException(ex);
        }
      }
    });
    server.start();

    FileDescriptor clientFd = Libcore.os.socket(AF_UNIX, SOCK_STREAM, 0);

    Libcore.os.connect(clientFd, address);
    checkNoSockName(clientFd);

    String string = "hello, world!";

    byte[] request = string.getBytes("UTF-8");
    assertEquals(request.length, Libcore.os.write(clientFd, request, 0, request.length));

    byte[] response = new byte[request.length];
    assertEquals(response.length, Libcore.os.read(clientFd, response, 0, response.length));

    assertEquals(string.toUpperCase(Locale.ROOT), new String(response, "UTF-8"));

    Libcore.os.close(clientFd);
  }

  private static void checkSockName(FileDescriptor fd, boolean isAbstract,
      UnixSocketAddress address) throws Exception {
    UnixSocketAddress isa = (UnixSocketAddress) Libcore.os.getsockname(fd);
    assertEquals(address, isa);
    if (isAbstract) {
      assertEquals(0, isa.getSunPath()[0]);
    }
  }

  private void checkNoName(UnixSocketAddress usa) {
    assertEquals(0, usa.getSunPath().length);
  }

  private void checkNoPeerName(FileDescriptor fd) throws Exception {
    checkNoName((UnixSocketAddress) Libcore.os.getpeername(fd));
  }

  private void checkNoSockName(FileDescriptor fd) throws Exception {
    checkNoName((UnixSocketAddress) Libcore.os.getsockname(fd));
  }

  public void test_strsignal() throws Exception {
    assertEquals("Killed", Libcore.os.strsignal(9));
    assertEquals("Unknown signal -1", Libcore.os.strsignal(-1));
  }

  public void test_byteBufferPositions_write_pwrite() throws Exception {
    FileOutputStream fos = new FileOutputStream(new File("/dev/null"));
    FileDescriptor fd = fos.getFD();
    final byte[] contents = new String("goodbye, cruel world").getBytes(StandardCharsets.US_ASCII);
    ByteBuffer byteBuffer = ByteBuffer.wrap(contents);

    byteBuffer.position(0);
    int written = Libcore.os.write(fd, byteBuffer);
    assertTrue(written > 0);
    assertEquals(written, byteBuffer.position());

    byteBuffer.position(4);
    written = Libcore.os.write(fd, byteBuffer);
    assertTrue(written > 0);
    assertEquals(written + 4, byteBuffer.position());

    byteBuffer.position(0);
    written = Libcore.os.pwrite(fd, byteBuffer, 64 /* offset */);
    assertTrue(written > 0);
    assertEquals(written, byteBuffer.position());

    byteBuffer.position(4);
    written = Libcore.os.pwrite(fd, byteBuffer, 64 /* offset */);
    assertTrue(written > 0);
    assertEquals(written + 4, byteBuffer.position());

    fos.close();
  }

  public void test_byteBufferPositions_read_pread() throws Exception {
    FileInputStream fis = new FileInputStream(new File("/dev/zero"));
    FileDescriptor fd = fis.getFD();
    ByteBuffer byteBuffer = ByteBuffer.allocate(64);

    byteBuffer.position(0);
    int read = Libcore.os.read(fd, byteBuffer);
    assertTrue(read > 0);
    assertEquals(read, byteBuffer.position());

    byteBuffer.position(4);
    read = Libcore.os.read(fd, byteBuffer);
    assertTrue(read > 0);
    assertEquals(read + 4, byteBuffer.position());

    byteBuffer.position(0);
    read = Libcore.os.pread(fd, byteBuffer, 64 /* offset */);
    assertTrue(read > 0);
    assertEquals(read, byteBuffer.position());

    byteBuffer.position(4);
    read = Libcore.os.pread(fd, byteBuffer, 64 /* offset */);
    assertTrue(read > 0);
    assertEquals(read + 4, byteBuffer.position());

    fis.close();
  }

  static void checkByteBufferPositions_sendto_recvfrom(
      int family, InetAddress loopback) throws Exception {
    final FileDescriptor serverFd = Libcore.os.socket(family, SOCK_STREAM, 0);
    Libcore.os.bind(serverFd, loopback, 0);
    Libcore.os.listen(serverFd, 5);

    InetSocketAddress address = (InetSocketAddress) Libcore.os.getsockname(serverFd);

    final Thread server = new Thread(new Runnable() {
      public void run() {
        try {
          InetSocketAddress peerAddress = new InetSocketAddress();
          FileDescriptor clientFd = Libcore.os.accept(serverFd, peerAddress);

          // Attempt to receive a maximum of 24 bytes from the client, and then
          // close the connection.
          ByteBuffer buffer = ByteBuffer.allocate(16);
          int received = Libcore.os.recvfrom(clientFd, buffer, 0, null);
          assertTrue(received > 0);
          assertEquals(received, buffer.position());

          ByteBuffer buffer2 = ByteBuffer.allocate(16);
          buffer2.position(8);
          received = Libcore.os.recvfrom(clientFd, buffer2, 0, null);
          assertTrue(received > 0);
          assertEquals(received + 8, buffer.position());

          Libcore.os.close(clientFd);
        } catch (Exception ex) {
          throw new RuntimeException(ex);
        }
      }
    });

    server.start();

    FileDescriptor clientFd = Libcore.os.socket(family, SOCK_STREAM, 0);
    Libcore.os.connect(clientFd, address.getAddress(), address.getPort());

    final byte[] bytes = "good bye, cruel black hole with fancy distortion"
        .getBytes(StandardCharsets.US_ASCII);
    assertTrue(bytes.length > 24);

    ByteBuffer input = ByteBuffer.wrap(bytes);
    input.position(0);
    input.limit(16);

    int sent = Libcore.os.sendto(clientFd, input, 0, address.getAddress(), address.getPort());
    assertTrue(sent > 0);
    assertEquals(sent, input.position());

    input.position(16);
    input.limit(24);
    sent = Libcore.os.sendto(clientFd, input, 0, address.getAddress(), address.getPort());
    assertTrue(sent > 0);
    assertEquals(sent + 16, input.position());

    Libcore.os.close(clientFd);
  }

  public void test_NetlinkSocket() throws Exception {
    FileDescriptor nlSocket = Libcore.os.socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
    Libcore.os.bind(nlSocket, new NetlinkSocketAddress());
    NetlinkSocketAddress address = (NetlinkSocketAddress) Libcore.os.getsockname(nlSocket);
    assertTrue(address.getPortId() > 0);
    assertEquals(0, address.getGroupsMask());

    NetlinkSocketAddress nlKernel = new NetlinkSocketAddress();
    Libcore.os.connect(nlSocket, nlKernel);
    NetlinkSocketAddress nlPeer = (NetlinkSocketAddress) Libcore.os.getpeername(nlSocket);
    assertEquals(0, nlPeer.getPortId());
    assertEquals(0, nlPeer.getGroupsMask());
    Libcore.os.close(nlSocket);
  }

  public void test_PacketSocketAddress() throws Exception {
    NetworkInterface lo = NetworkInterface.getByName("lo");
    FileDescriptor fd = Libcore.os.socket(AF_PACKET, SOCK_DGRAM, ETH_P_IPV6);
    PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IPV6, lo.getIndex());
    Libcore.os.bind(fd, addr);

    PacketSocketAddress bound = (PacketSocketAddress) Libcore.os.getsockname(fd);
    assertEquals((short) ETH_P_IPV6, bound.sll_protocol);  // ETH_P_IPV6 is an int.
    assertEquals(lo.getIndex(), bound.sll_ifindex);
    assertEquals(ARPHRD_LOOPBACK, bound.sll_hatype);
    assertEquals(0, bound.sll_pkttype);

    // The loopback address is ETH_ALEN bytes long and is all zeros.
    // http://lxr.free-electrons.com/source/drivers/net/loopback.c?v=3.10#L167
    assertEquals(6, bound.sll_addr.length);
    for (int i = 0; i < 6; i++) {
      assertEquals(0, bound.sll_addr[i]);
    }
  }

  public void test_byteBufferPositions_sendto_recvfrom_af_inet() throws Exception {
    checkByteBufferPositions_sendto_recvfrom(AF_INET, InetAddress.getByName("127.0.0.1"));
  }

  public void test_byteBufferPositions_sendto_recvfrom_af_inet6() throws Exception {
    checkByteBufferPositions_sendto_recvfrom(AF_INET6, InetAddress.getByName("::1"));
  }

  private void checkSendToSocketAddress(int family, InetAddress loopback) throws Exception {
    FileDescriptor recvFd = Libcore.os.socket(family, SOCK_DGRAM, 0);
    Libcore.os.bind(recvFd, loopback, 0);
    StructTimeval tv = StructTimeval.fromMillis(20);
    Libcore.os.setsockoptTimeval(recvFd, SOL_SOCKET, SO_RCVTIMEO, tv);

    InetSocketAddress to = ((InetSocketAddress) Libcore.os.getsockname(recvFd));
    FileDescriptor sendFd = Libcore.os.socket(family, SOCK_DGRAM, 0);
    byte[] msg = ("Hello, I'm going to a socket address: " + to.toString()).getBytes("UTF-8");
    int len = msg.length;

    assertEquals(len, Libcore.os.sendto(sendFd, msg, 0, len, 0, to));
    byte[] received = new byte[msg.length + 42];
    InetSocketAddress from = new InetSocketAddress();
    assertEquals(len, Libcore.os.recvfrom(recvFd, received, 0, received.length, 0, from));
    assertEquals(loopback, from.getAddress());
  }

  public void test_sendtoSocketAddress_af_inet() throws Exception {
    checkSendToSocketAddress(AF_INET, InetAddress.getByName("127.0.0.1"));
  }

  public void test_sendtoSocketAddress_af_inet6() throws Exception {
    checkSendToSocketAddress(AF_INET6, InetAddress.getByName("::1"));
  }

  public void test_socketFamilies() throws Exception {
    FileDescriptor fd = Libcore.os.socket(AF_INET6, SOCK_STREAM, 0);
    Libcore.os.bind(fd, InetAddress.getByName("::"), 0);
    InetSocketAddress localSocketAddress = (InetSocketAddress) Libcore.os.getsockname(fd);
    assertEquals(Inet6Address.ANY, localSocketAddress.getAddress());

    fd = Libcore.os.socket(AF_INET6, SOCK_STREAM, 0);
    Libcore.os.bind(fd, InetAddress.getByName("0.0.0.0"), 0);
    localSocketAddress = (InetSocketAddress) Libcore.os.getsockname(fd);
    assertEquals(Inet6Address.ANY, localSocketAddress.getAddress());

    fd = Libcore.os.socket(AF_INET, SOCK_STREAM, 0);
    Libcore.os.bind(fd, InetAddress.getByName("0.0.0.0"), 0);
    localSocketAddress = (InetSocketAddress) Libcore.os.getsockname(fd);
    assertEquals(Inet4Address.ANY, localSocketAddress.getAddress());
    try {
      Libcore.os.bind(fd, InetAddress.getByName("::"), 0);
      fail("Expected ErrnoException binding IPv4 socket to ::");
    } catch (ErrnoException expected) {
      assertEquals("Expected EAFNOSUPPORT binding IPv4 socket to ::", EAFNOSUPPORT, expected.errno);
    }
  }

  private static void assertArrayEquals(byte[] expected, byte[] actual) {
    assertTrue("Expected=" + Arrays.toString(expected) + ", actual=" + Arrays.toString(actual),
        Arrays.equals(expected, actual));
  }

  private static void checkSocketPing(FileDescriptor fd, InetAddress to, byte[] packet,
      byte type, byte responseType, boolean useSendto) throws Exception {
    int len = packet.length;
    packet[0] = type;
    if (useSendto) {
      assertEquals(len, Libcore.os.sendto(fd, packet, 0, len, 0, to, 0));
    } else {
      Libcore.os.connect(fd, to, 0);
      assertEquals(len, Libcore.os.sendto(fd, packet, 0, len, 0, null, 0));
    }

    int icmpId = ((InetSocketAddress) Libcore.os.getsockname(fd)).getPort();
    byte[] received = new byte[4096];
    InetSocketAddress srcAddress = new InetSocketAddress();
    assertEquals(len, Libcore.os.recvfrom(fd, received, 0, received.length, 0, srcAddress));
    assertEquals(to, srcAddress.getAddress());
    assertEquals(responseType, received[0]);
    assertEquals(received[4], (byte) (icmpId >> 8));
    assertEquals(received[5], (byte) (icmpId & 0xff));

    received = Arrays.copyOf(received, len);
    received[0] = (byte) type;
    received[2] = received[3] = 0;  // Checksum.
    received[4] = received[5] = 0;  // ICMP ID.
    assertArrayEquals(packet, received);
  }

  public void test_socketPing() throws Exception {
    final byte ICMP_ECHO = 8, ICMP_ECHOREPLY = 0;
    final byte ICMPV6_ECHO_REQUEST = (byte) 128, ICMPV6_ECHO_REPLY = (byte) 129;
    final byte[] packet = ("\000\000\000\000" +  // ICMP type, code.
        "\000\000\000\003" +  // ICMP ID (== port), sequence number.
        "Hello myself").getBytes(StandardCharsets.US_ASCII);

    FileDescriptor fd = Libcore.os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
    InetAddress ipv6Loopback = InetAddress.getByName("::1");
    checkSocketPing(fd, ipv6Loopback, packet, ICMPV6_ECHO_REQUEST, ICMPV6_ECHO_REPLY, true);
    checkSocketPing(fd, ipv6Loopback, packet, ICMPV6_ECHO_REQUEST, ICMPV6_ECHO_REPLY, false);

    fd = Libcore.os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
    InetAddress ipv4Loopback = InetAddress.getByName("127.0.0.1");
    checkSocketPing(fd, ipv4Loopback, packet, ICMP_ECHO, ICMP_ECHOREPLY, true);
    checkSocketPing(fd, ipv4Loopback, packet, ICMP_ECHO, ICMP_ECHOREPLY, false);
  }

  public void test_Ipv4Fallback() throws Exception {
    // This number of iterations gives a ~60% chance of creating the conditions that caused
    // http://b/23088314 without making test times too long. On a hammerhead running MRZ37C using
    // vogar, this test takes about 4s.
    final int ITERATIONS = 10000;
    for (int i = 0; i < ITERATIONS; i++) {
      FileDescriptor mUdpSock = Libcore.os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
      try {
          Libcore.os.bind(mUdpSock, Inet4Address.ANY, 0);
      } catch(ErrnoException e) {
          fail("ErrnoException after " + i + " iterations: " + e);
      } finally {
          Libcore.os.close(mUdpSock);
      }
    }
  }

  public void test_unlink() throws Exception {
    File f = File.createTempFile("OsTest", "tst");
    assertTrue(f.exists());
    Libcore.os.unlink(f.getAbsolutePath());
    assertFalse(f.exists());

    try {
      Libcore.os.unlink(f.getAbsolutePath());
      fail();
    } catch (ErrnoException e) {
      assertEquals(OsConstants.ENOENT, e.errno);
    }
  }

  // b/27294715
  public void test_recvfrom_concurrentShutdown() throws Exception {
      final FileDescriptor serverFd = Libcore.os.socket(AF_INET, SOCK_DGRAM, 0);
      Libcore.os.bind(serverFd, InetAddress.getByName("127.0.0.1"), 0);
      // Set 4s timeout
      IoBridge.setSocketOption(serverFd, SocketOptions.SO_TIMEOUT, new Integer(4000));

      final AtomicReference<Exception> killerThreadException = new AtomicReference<Exception>(null);
      final Thread killer = new Thread(new Runnable() {
          public void run() {
              try {
                  Thread.sleep(2000);
                  try {
                      Libcore.os.shutdown(serverFd, SHUT_RDWR);
                  } catch (ErrnoException expected) {
                      if (OsConstants.ENOTCONN != expected.errno) {
                          killerThreadException.set(expected);
                      }
                  }
              } catch (Exception ex) {
                  killerThreadException.set(ex);
              }
          }
      });
      killer.start();

      ByteBuffer buffer = ByteBuffer.allocate(16);
      InetSocketAddress srcAddress = new InetSocketAddress();
      int received = Libcore.os.recvfrom(serverFd, buffer, 0, srcAddress);
      assertTrue(received == 0);
      Libcore.os.close(serverFd);

      killer.join();
      assertNull(killerThreadException.get());
  }

  public void test_xattr() throws Exception {
    final String NAME_TEST = "user.meow";

    final byte[] VALUE_CAKE = "cake cake cake".getBytes(StandardCharsets.UTF_8);
    final byte[] VALUE_PIE = "pie".getBytes(StandardCharsets.UTF_8);

    File file = File.createTempFile("xattr", "test");
    String path = file.getAbsolutePath();

    byte[] tmp = new byte[1024];
    try {
      try {
        Libcore.os.getxattr(path, NAME_TEST, tmp);
        fail("Expected ENODATA");
      } catch (ErrnoException e) {
        assertEquals(OsConstants.ENODATA, e.errno);
      }

      Libcore.os.setxattr(path, NAME_TEST, VALUE_CAKE, OsConstants.XATTR_CREATE);
      assertEquals(VALUE_CAKE.length, Libcore.os.getxattr(path, NAME_TEST, tmp));
      assertStartsWith(VALUE_CAKE, tmp);

      try {
        Libcore.os.setxattr(path, NAME_TEST, VALUE_PIE, OsConstants.XATTR_CREATE);
        fail("Expected EEXIST");
      } catch (ErrnoException e) {
        assertEquals(OsConstants.EEXIST, e.errno);
      }

      Libcore.os.setxattr(path, NAME_TEST, VALUE_PIE, OsConstants.XATTR_REPLACE);
      assertEquals(VALUE_PIE.length, Libcore.os.getxattr(path, NAME_TEST, tmp));
      assertStartsWith(VALUE_PIE, tmp);

      Libcore.os.removexattr(path, NAME_TEST);
      try {
        Libcore.os.getxattr(path, NAME_TEST, tmp);
        fail("Expected ENODATA");
      } catch (ErrnoException e) {
        assertEquals(OsConstants.ENODATA, e.errno);
      }

    } finally {
      file.delete();
    }
  }

  public void test_realpath() throws Exception {
      File tmpDir = new File(System.getProperty("java.io.tmpdir"));
      // This is a chicken and egg problem. We have no way of knowing whether
      // the temporary directory or one of its path elements were symlinked, so
      // we'll need this call to realpath.
      String canonicalTmpDir = Libcore.os.realpath(tmpDir.getAbsolutePath());

      // Test that "." and ".." are resolved correctly.
      assertEquals(canonicalTmpDir,
          Libcore.os.realpath(canonicalTmpDir + "/./../" + tmpDir.getName()));

      // Test that symlinks are resolved correctly.
      File target = new File(tmpDir, "target");
      File link = new File(tmpDir, "link");
      try {
          assertTrue(target.createNewFile());
          Libcore.os.symlink(target.getAbsolutePath(), link.getAbsolutePath());

          assertEquals(canonicalTmpDir + "/target",
              Libcore.os.realpath(canonicalTmpDir + "/link"));
      } finally {
          boolean deletedTarget = target.delete();
          boolean deletedLink = link.delete();
          // Asserting this here to provide a definitive reason for
          // a subsequent failure on the same run.
          assertTrue("deletedTarget = " + deletedTarget + ", deletedLink =" + deletedLink,
              deletedTarget && deletedLink);
      }
  }

  private static void assertStartsWith(byte[] expectedContents, byte[] container) {
    for (int i = 0; i < expectedContents.length; i++) {
      if (expectedContents[i] != container[i]) {
        fail("Expected " + Arrays.toString(expectedContents) + " but found "
            + Arrays.toString(expectedContents));
      }
    }
  }
}
