/*
 * Copyright (C) 2013 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 com.android.ide.common.rendering;

import static java.io.File.separator;

import com.android.ide.common.res2.RecordingLogger;
import com.android.testutils.TestUtils;
import com.android.utils.SdkUtils;
import com.google.common.io.Files;

import junit.framework.TestCase;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FilePermission;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.security.Permission;
import java.util.Collections;
import java.util.TimeZone;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.imageio.ImageIO;
import javax.swing.*;

public class RenderSecurityManagerTest extends TestCase {

    private Object myCredential = new Object();

    public void testExec() throws Exception {
        assertNull(RenderSecurityManager.getCurrent());
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        RecordingLogger logger = new RecordingLogger();
        manager.setLogger(logger);
        try {
            assertNull(RenderSecurityManager.getCurrent());
            manager.setActive(true, myCredential);
            assertSame(manager, RenderSecurityManager.getCurrent());
            if (new File("/bin/ls").exists()) {
                Runtime.getRuntime().exec("/bin/ls");
            } else {
                manager.checkExec("/bin/ls");
            }
            fail("Should have thrown security exception");
        } catch (SecurityException exception) {
            //noinspection ConstantConditions
            assertEquals(
                    RenderSecurityManager.RESTRICT_READS ?
                        "Read access not allowed during rendering (/bin/ls)" :
                        "Exec access not allowed during rendering (/bin/ls)",
                    exception.toString());
            // pass
        } finally {
            manager.dispose(myCredential);
            assertNull(RenderSecurityManager.getCurrent());
            assertNull(System.getSecurityManager());
            assertEquals(Collections.<String>emptyList(), logger.getWarningMsgs());
        }
    }

    public void testSetSecurityManager() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);
            System.setSecurityManager(null);
            fail("Should have thrown security exception");
        } catch (SecurityException exception) {
            assertEquals("Security access not allowed during rendering", exception.toString());
            // pass
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testReadWrite() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);
            manager.checkPermission(new FilePermission("/foo", "read,write"));
            fail("Should have thrown security exception");
        } catch (SecurityException exception) {
            assertEquals("Write access not allowed during rendering (/foo)", exception.toString());
            // pass
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testExecute() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);
            manager.checkPermission(new FilePermission("/foo", "execute"));
            fail("Should have thrown security exception");
        } catch (SecurityException exception) {
            assertEquals("Write access not allowed during rendering (/foo)", exception.toString());
            // pass
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testDelete() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);
            manager.checkPermission(new FilePermission("/foo", "delete"));
            fail("Should have thrown security exception");
        } catch (SecurityException exception) {
            assertEquals("Write access not allowed during rendering (/foo)", exception.toString());
            // pass
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testLoadLibrary() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            // Unit test only runs on OSX
            if (SdkUtils.startsWithIgnoreCase(System.getProperty("os.name"), "Mac")
                    && new File("/usr/lib/libc.dylib").exists()) {
                System.load("/usr/lib/libc.dylib");
                fail("Should have thrown security exception");
            }
        } catch (SecurityException exception) {
            assertEquals("Link access not allowed during rendering (/usr/lib/libc.dylib)",
                    exception.toString());
            // pass
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testAllowedLoadLibrary() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            System.loadLibrary("jsound");
        } catch (UnsatisfiedLinkError e) {
            // pass - library may not be present on all JDKs
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testInvalidRead() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            if (RenderSecurityManager.RESTRICT_READS) {
                try {
                    File file = new File(System.getProperty("user.home"));
                    //noinspection ResultOfMethodCallIgnored
                    file.lastModified();

                    fail("Should have thrown security exception");
                } catch (SecurityException exception) {
                    assertEquals("Read access not allowed during rendering (" +
                            System.getProperty("user.home") + ")", exception.toString());
                    // pass
                }
            } else {
                try {
                    File file = new File(System.getProperty("user.home"));
                    //noinspection ResultOfMethodCallIgnored
                    file.lastModified();
                } catch (SecurityException exception) {
                    fail("Reading should be allowed");
                }
            }
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testInvalidPropertyWrite() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            // Try to make java.io.tmpdir point to user.home to grant myself access:
            String userHome = System.getProperty("user.home");
            System.setProperty("java.io.tmpdir", userHome);

            fail("Should have thrown security exception");
        } catch (SecurityException exception) {
            assertEquals("Write access not allowed during rendering (java.io.tmpdir)",
                    exception.toString());
            // pass
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testReadOk() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null,  null);
        try {
            manager.setActive(true, myCredential);

            File jdkHome = new File(System.getProperty("java.home"));
            assertTrue(jdkHome.exists());
            //noinspection ResultOfMethodCallIgnored
            File[] files = jdkHome.listFiles();
            if (files != null) {
                for (File file : files) {
                    if (file.isFile()) {
                        Files.toByteArray(file);
                    }
                }
            }
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testProperties() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            System.getProperties();

            fail("Should have thrown security exception");
        } catch (SecurityException exception) {
            assertEquals("Property access not allowed during rendering", exception.toString());
            // pass
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testExit() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            System.exit(-1);

            fail("Should have thrown security exception");
        } catch (SecurityException exception) {
            assertEquals("Exit access not allowed during rendering (-1)", exception.toString());
            // pass
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testThread() throws Exception {
        final AtomicBoolean failedUnexpectedly = new AtomicBoolean(false);
        Thread otherThread = new Thread("other") {
            @Override
            public void run() {
                try {
                    assertNull(RenderSecurityManager.getCurrent());
                    System.getProperties();
                } catch (SecurityException e) {
                    failedUnexpectedly.set(true);
                }
            }
        };
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            // Threads cloned from this one should inherit the same security constraints
            final AtomicBoolean failedAsExpected = new AtomicBoolean(false);
            final Thread renderThread = new Thread("render") {
                @Override
                public void run() {
                    try {
                        System.getProperties();
                    } catch (SecurityException e) {
                        failedAsExpected.set(true);
                    }
                }
            };
            renderThread.start();
            renderThread.join();
            assertTrue(failedAsExpected.get());
            otherThread.start();
            otherThread.join();
            assertFalse(failedUnexpectedly.get());
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testActive() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            try {
                System.getProperties();
                fail("Should have thrown security exception");
            } catch (SecurityException exception) {
                // pass
            }

            manager.setActive(false, myCredential);

            try {
                System.getProperties();
            } catch (SecurityException exception) {
                fail(exception.toString());
            }

            manager.setActive(true, myCredential);

            try {
                System.getProperties();
                fail("Should have thrown security exception");
            } catch (SecurityException exception) {
                // pass
            }
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testThread2() throws Exception {
        // Check that when a new thread is created simultaneously from an unrelated
        // thread during rendering, that new thread does not pick up the security manager.
        //
        final CyclicBarrier barrier1 = new CyclicBarrier(2);
        final CyclicBarrier barrier2 = new CyclicBarrier(2);
        final CyclicBarrier barrier3 = new CyclicBarrier(4);
        final CyclicBarrier barrier4 = new CyclicBarrier(4);
        final CyclicBarrier barrier5 = new CyclicBarrier(4);
        final CyclicBarrier barrier6 = new CyclicBarrier(4);

        // First the threads reach barrier1. Then from barrier1 to barrier2, thread1
        // installs the security manager. Then from barrier2 to barrier3, thread2
        // checks that it does not have any security restrictions, and creates thread3.
        // Thread1 will ensure that the security manager is working there, and it will
        // create thread4. Then after barrier3 (where thread3 and thread4 are now also
        // participating) thread3 will ensure that it too has no security restrictions,
        // and thread4 will ensure that it does. At barrier4 the security manager gets
        // uninstalled, and at barrier5 all threads will check that there are no more
        // restrictions. At barrier6 all threads are done.

        final Thread thread1 = new Thread("render") {
            @Override
            public void run() {
                try {
                    barrier1.await();
                    assertNull(RenderSecurityManager.getCurrent());

                    RenderSecurityManager manager = new RenderSecurityManager(null, null);
                    manager.setActive(true, myCredential);

                    barrier2.await();

                    Thread thread4 = new Thread() {
                        @Override
                        public void run() {
                            try {
                                barrier3.await();

                                try {
                                    System.getProperties();
                                    fail("Should have thrown security exception");
                                } catch (SecurityException e) {
                                    // pass
                                }

                                barrier4.await();
                                barrier5.await();
                                assertNull(RenderSecurityManager.getCurrent());
                                assertNull(System.getSecurityManager());
                                barrier6.await();
                            } catch (InterruptedException e) {
                                fail(e.toString());
                            } catch (BrokenBarrierException e) {
                                fail(e.toString());
                            }
                        }
                    };
                    thread4.start();

                    try {
                        System.getProperties();
                        fail("Should have thrown security exception");
                    } catch (SecurityException e) {
                        // expected
                    }

                    barrier3.await();
                    barrier4.await();
                    manager.dispose(myCredential);

                    assertNull(RenderSecurityManager.getCurrent());
                    assertNull(System.getSecurityManager());

                    barrier5.await();
                    barrier6.await();

                } catch (InterruptedException e) {
                    fail(e.toString());
                } catch (BrokenBarrierException e) {
                    fail(e.toString());
                }

            }
        };

        final Thread thread2 = new Thread("unrelated") {
            @Override
            public void run() {
                try {
                    barrier1.await();
                    assertNull(RenderSecurityManager.getCurrent());
                    barrier2.await();
                    assertNull(RenderSecurityManager.getCurrent());
                    assertNotNull(System.getSecurityManager());

                    try {
                        System.getProperties();
                    } catch (SecurityException e) {
                        fail("Should not have been affected by security manager");
                    }

                    Thread thread3 = new Thread() {
                        @Override
                        public void run() {
                            try {
                                barrier3.await();

                                try {
                                    System.getProperties();
                                } catch (SecurityException e) {
                                    fail("Should not have been affected by security manager");
                                }

                                barrier4.await();
                                barrier5.await();
                                assertNull(RenderSecurityManager.getCurrent());
                                assertNull(System.getSecurityManager());
                                barrier6.await();

                            } catch (InterruptedException e) {
                                fail(e.toString());
                            } catch (BrokenBarrierException e) {
                                fail(e.toString());
                            }
                        }
                    };
                    thread3.start();

                    barrier3.await();
                    barrier4.await();
                    barrier5.await();
                    assertNull(RenderSecurityManager.getCurrent());
                    assertNull(System.getSecurityManager());
                    barrier6.await();

                } catch (InterruptedException e) {
                    fail(e.toString());
                } catch (BrokenBarrierException e) {
                    fail(e.toString());
                }

            }
        };

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }

    public void testDisabled() throws Exception {
        assertNull(RenderSecurityManager.getCurrent());

        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        RenderSecurityManager.sEnabled = false;
        try {
            assertNull(RenderSecurityManager.getCurrent());
            manager.setActive(true, myCredential);
            assertSame(manager, System.getSecurityManager());
            if (new File("/bin/ls").exists()) {
                Runtime.getRuntime().exec("/bin/ls");
            } else {
                manager.checkExec("/bin/ls");
            }
        } catch (SecurityException exception) {
            fail("Should have been disabled");
        } finally {
            RenderSecurityManager.sEnabled = true;
            manager.dispose(myCredential);
            assertNull(RenderSecurityManager.getCurrent());
            assertNull(System.getSecurityManager());
        }
    }

    public void testLogger() throws Exception {
        assertNull(RenderSecurityManager.getCurrent());

        final CyclicBarrier barrier1 = new CyclicBarrier(2);
        final CyclicBarrier barrier2 = new CyclicBarrier(2);
        final CyclicBarrier barrier3 = new CyclicBarrier(2);

        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    barrier1.await();
                    barrier2.await();

                    System.setSecurityManager(new SecurityManager() {
                        @Override
                        public String toString() {
                            return "MyTestSecurityManager";
                        }

                        @Override
                        public void checkPermission(Permission permission) {
                        }
                    });

                    barrier3.await();
                    assertNull(RenderSecurityManager.getCurrent());
                    assertNotNull(System.getSecurityManager());
                    assertEquals("MyTestSecurityManager", System.getSecurityManager().toString());
                } catch (InterruptedException e) {
                    fail(e.toString());
                } catch (BrokenBarrierException e) {
                    fail(e.toString());
                }
            }
        };
        thread.start();

        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        RecordingLogger logger = new RecordingLogger();
        manager.setLogger(logger);
        try {
            barrier1.await();
            assertNull(RenderSecurityManager.getCurrent());
            manager.setActive(true, myCredential);
            assertSame(manager, RenderSecurityManager.getCurrent());
            barrier2.await();
            barrier3.await();

            assertNull(RenderSecurityManager.getCurrent());
            manager.setActive(false, myCredential);
            assertNull(RenderSecurityManager.getCurrent());

            assertEquals(Collections.singletonList(
                    "RenderSecurityManager being replaced by another thread"),
                    logger.getWarningMsgs());
        } catch (InterruptedException e) {
            fail(e.toString());
        } catch (BrokenBarrierException e) {
            fail(e.toString());
        } finally {
            manager.dispose(myCredential);
            assertNull(RenderSecurityManager.getCurrent());
            assertNotNull(System.getSecurityManager());
            assertEquals("MyTestSecurityManager", System.getSecurityManager().toString());
            System.setSecurityManager(null);
        }
    }

    public void testEnterExitSafeRegion() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        Object credential = new Object();
        try {
            manager.setActive(true, credential);

            boolean token = RenderSecurityManager.enterSafeRegion(credential);
            manager.checkPermission(new FilePermission("/foo", "execute"));
            RenderSecurityManager.exitSafeRegion(token);

            assertNotNull(RenderSecurityManager.getCurrent());
            boolean tokenOuter = RenderSecurityManager.enterSafeRegion(credential);
            assertNull(RenderSecurityManager.getCurrent());
            boolean tokenInner = RenderSecurityManager.enterSafeRegion(credential);
            assertNull(RenderSecurityManager.getCurrent());
            manager.checkPermission(new FilePermission("/foo", "execute"));
            assertNull(RenderSecurityManager.getCurrent());
            manager.checkPermission(new FilePermission("/foo", "execute"));
            RenderSecurityManager.exitSafeRegion(tokenInner);
            assertNull(RenderSecurityManager.getCurrent());
            RenderSecurityManager.exitSafeRegion(tokenOuter);
            assertNotNull(RenderSecurityManager.getCurrent());

            // Wrong credential
            Object wrongCredential = new Object();
            try {
                token = RenderSecurityManager.enterSafeRegion(wrongCredential);
                manager.checkPermission(new FilePermission("/foo", "execute"));
                RenderSecurityManager.exitSafeRegion(token);
                fail("Should have thrown exception");
            } catch (SecurityException e) {
                // pass
            }

            // Try turning off the security manager
            try {
                manager.setActive(false, wrongCredential);
            } catch (SecurityException e) {
                // pass
            }
            try {
                manager.setActive(false, null);
            } catch (SecurityException e) {
                // pass
            }
            try {
                manager.dispose(wrongCredential);
            } catch (SecurityException e) {
                // pass
            }

            // Try looking up the secret
            try {
                Field field = RenderSecurityManager.class.getField("sCredential");
                field.setAccessible(true);
                Object secret = field.get(null);
                manager.dispose(secret);
                fail("Shouldn't be able to find our way to the credential");
            } catch (Exception e) {
                // pass
                assertEquals("java.lang.NoSuchFieldException: sCredential", e.toString());
            }

            // Try looking up the secret (with getDeclaredField instead of getField)
            try {
                Field field = RenderSecurityManager.class.getDeclaredField("sCredential");
                field.setAccessible(true);
                Object secret = field.get(null);
                manager.dispose(secret);
                fail("Shouldn't be able to find our way to the credential");
            } catch (Exception e) {
                // pass
                assertEquals("Reflection access not allowed during rendering "
                        + "(com.android.ide.common.rendering.RenderSecurityManager)",
                        e.toString());
            }
        } finally {
            manager.dispose(credential);
        }
    }

    public void testImageIo() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            File root = TestUtils.getRoot("resources", "baseMerge");
            assertNotNull(root);
            assertTrue(root.exists());
            final File icon = new File(root, "overlay" + separator + "drawable" + separator
                    + "icon2.png");
            assertTrue(icon.exists());
            final byte[] buf = Files.toByteArray(icon);
            InputStream stream = new ByteArrayInputStream(buf);
            assertNotNull(stream);
            BufferedImage image = ImageIO.read(stream);
            assertNotNull(image);
            assertNull(ImageIO.getCacheDirectory());

            // Also run in non AWT thread to test ImageIO thread locals cache dir behavior
            Thread thread = new Thread() {
                @Override
                public void run() {
                    try {
                        assertFalse(SwingUtilities.isEventDispatchThread());
                        final byte[] buf = Files.toByteArray(icon);
                        InputStream stream = new ByteArrayInputStream(buf);
                        assertNotNull(stream);
                        BufferedImage image = ImageIO.read(stream);
                        assertNotNull(image);
                        assertNull(ImageIO.getCacheDirectory());
                    } catch (Throwable t) {
                        t.printStackTrace();
                        fail(t.toString());
                    }
                }
            };

            thread.start();
            thread.join();
        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testTempDir() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            String temp = System.getProperty("java.io.tmpdir");
            assertNotNull(temp);

            manager.checkPermission(new FilePermission(temp, "read,write"));
            manager.checkPermission(new FilePermission(temp + File.separator, "read,write"));

            temp = new File(temp).getCanonicalPath();
            manager.checkPermission(new FilePermission(temp, "read,write"));

        } finally {
            manager.dispose(myCredential);
        }
    }

    public void testAppTempDir() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setAppTempDir("/random/path/");
            manager.setActive(true, myCredential);
            manager.checkPermission(new FilePermission("/random/path/myfile.tmp", "read,write"));
        } finally {
            manager.dispose(myCredential);
        }
    }


    public void testSetTimeZone() throws Exception {
        RenderSecurityManager manager = new RenderSecurityManager(null, null);
        try {
            manager.setActive(true, myCredential);

            /* ICU needs this (needed for Calendar widget rendering)
                at java.util.TimeZone.hasPermission(TimeZone.java:597)
                at java.util.TimeZone.setDefault(TimeZone.java:619)
                at com.ibm.icu.util.TimeZone.setDefault(TimeZone.java:973)
                at libcore.icu.DateIntervalFormat_Delegate.createDateIntervalFormat(DateIntervalFormat_Delegate.java:61)
                at libcore.icu.DateIntervalFormat.createDateIntervalFormat(DateIntervalFormat.java)
                at libcore.icu.DateIntervalFormat.getFormatter(DateIntervalFormat.java:112)
                at libcore.icu.DateIntervalFormat.formatDateRange(DateIntervalFormat.java:102)
                at libcore.icu.DateIntervalFormat.formatDateRange(DateIntervalFormat.java:71)
                at android.text.format.DateUtils.formatDateRange(DateUtils.java:826)
             */
            TimeZone deflt = TimeZone.getDefault();
            String[] availableIDs = TimeZone.getAvailableIDs();
            TimeZone timeZone = TimeZone.getTimeZone(availableIDs[0]);
            TimeZone.setDefault(timeZone);
            TimeZone.setDefault(deflt);
        } finally {
            manager.dispose(myCredential);
        }
    }


}
