/*
 * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;

import java.io.SerializablePermission;
import java.security.Security;
import java.util.Objects;

import org.testng.Assert;
import org.testng.annotations.Test;
import org.testng.annotations.DataProvider;

import sun.misc.ObjectInputFilter;

/* @test
 * @bug 8231422
 * @build GlobalFilterTest SerialFilterTest
 * @run testng/othervm GlobalFilterTest
 * @run testng/othervm -Djdk.serialFilter=java.**
 *          -Dexpected-jdk.serialFilter=java.** GlobalFilterTest
 * @run testng/othervm/policy=security.policy GlobalFilterTest
 * @run testng/othervm/policy=security.policy
 *        -Djava.security.properties=${test.src}/java.security-extra1
 *        -Djava.security.debug=properties GlobalFilterTest
 *
 * @summary Test Global Filters
 */
@Test
public class GlobalFilterTest {
    private static final String serialPropName = "jdk.serialFilter";
    private static final String badSerialFilter = "java.lang.StringBuffer;!*";
    private static final String origSerialFilterProperty =
            System.setProperty(serialPropName, badSerialFilter);

    /**
     * DataProvider of patterns and objects derived from the configured process-wide filter.
     * @return Array of arrays of pattern, object, allowed boolean, and API factory
     */
    @DataProvider(name="globalPatternElements")
    Object[][] globalPatternElements() {
        String globalFilter =
                System.getProperty("expected-" + serialPropName,
                        Security.getProperty(serialPropName));
        if (globalFilter == null) {
            return new Object[0][];
        }

        String[] patterns = globalFilter.split(";");
        Object[][] objects = new Object[patterns.length][];

        for (int i = 0; i < patterns.length; i++) {
            Object o;
            boolean allowed;
            String pattern = patterns[i].trim();
            if (pattern.contains("=")) {
                allowed = false;
                o = SerialFilterTest.genTestObject(pattern, false);
            } else {
                allowed = !pattern.startsWith("!");
                o = (allowed)
                    ? SerialFilterTest.genTestObject(pattern, true)
                    : SerialFilterTest.genTestObject(pattern.substring(1), false);

                Assert.assertNotNull(o, "fail generation failed");
            }
            objects[i] = new Object[3];
            objects[i][0] = pattern;
            objects[i][1] = allowed;
            objects[i][2] = o;
        }
        return objects;
    }

    /**
     * Test that the process-wide filter is set when the properties are set
     * and has the toString matching the configured pattern.
     */
    @Test()
    static void globalFilter() {
        ObjectInputFilter filter = ObjectInputFilter.Config.getSerialFilter();

        // Check that the System.setProperty(jdk.serialFilter) DOES NOT affect the filter.
        String asSetSystemProp = System.getProperty(serialPropName,
                Security.getProperty(serialPropName));
        Assert.assertNotEquals(Objects.toString(filter, null), asSetSystemProp,
                "System.setProperty(\"jdk.serialfilter\", ...) should not change filter: " +
                asSetSystemProp);

        String pattern =
               System.getProperty("expected-" + serialPropName,
                       Security.getProperty(serialPropName));
        System.out.printf("global pattern: %s, filter: %s%n", pattern, filter);
        Assert.assertEquals(Objects.toString(filter, null), pattern,
                "process-wide filter pattern does not match");
    }

    /**
     * If the Global filter is already set, it should always refuse to be
     * set again.
     * If there is a security manager, setting the serialFilter should fail
     * without the appropriate permission.
     * If there is no security manager then setting it should work.
     */
    @Test()
    static void setGlobalFilter() {
        SecurityManager sm = System.getSecurityManager();
        ObjectInputFilter filter = new SerialFilterTest.Validator();
        ObjectInputFilter global = ObjectInputFilter.Config.getSerialFilter();
        if (global != null) {
            // once set, can never be re-set
            try {
                ObjectInputFilter.Config.setSerialFilter(filter);
                Assert.fail("set only once process-wide filter");
            } catch (IllegalStateException ise) {
                if (sm != null) {
                    Assert.fail("wrong exception when security manager is set", ise);
                }
            } catch (SecurityException se) {
                if (sm == null) {
                    Assert.fail("wrong exception when security manager is not set", se);
                }
            }
        } else {
            if (sm == null) {
                // no security manager
                try {
                    ObjectInputFilter.Config.setSerialFilter(filter);
                    // Note once set, it can not be reset; so other tests
                    System.out.printf("Global Filter set to Validator%n");
                } catch (SecurityException se) {
                    Assert.fail("setGlobalFilter should not get SecurityException", se);
                }
                try {
                    // Try to set it again, expecting it to throw
                    ObjectInputFilter.Config.setSerialFilter(filter);
                    Assert.fail("set only once process-wide filter");
                } catch (IllegalStateException ise) {
                    // Normal case
                }
            } else {
                // Security manager
                SecurityException expectSE = null;
                try {
                    sm.checkPermission(new SerializablePermission("serialFilter"));
                } catch (SecurityException se1) {
                    expectSE = se1;
                }
                SecurityException actualSE = null;
                try {
                    ObjectInputFilter.Config.setSerialFilter(filter);
                } catch (SecurityException se2) {
                    actualSE = se2;
                }
                if (expectSE == null | actualSE == null) {
                    Assert.assertEquals(expectSE, actualSE, "SecurityException");
                } else {
                    Assert.assertEquals(expectSE.getClass(), actualSE.getClass(),
                            "SecurityException class");
                }
            }
        }
    }

    /**
     * For each pattern in the process-wide filter test a generated object
     * against the default process-wide filter.
     *
     * @param pattern a pattern extracted from the configured global pattern
     */
    @Test(dataProvider = "globalPatternElements")
    static void globalFilterElements(String pattern, boolean allowed,Object obj) {
        testGlobalPattern(pattern, obj, allowed);
    }

    /**
     * Serialize and deserialize an object using the default process-wide filter
     * and check allowed or reject.
     *
     * @param pattern the pattern
     * @param object the test object
     * @param allowed the expected result from ObjectInputStream (exception or not)
     */
    static void testGlobalPattern(String pattern, Object object, boolean allowed) {
        try {
//            System.out.printf("global %s pattern: %s, obj: %s%n", (allowed ? "allowed" : "not allowed"), pattern, object);
            byte[] bytes = SerialFilterTest.writeObjects(object);
            try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
                 ObjectInputStream ois = new ObjectInputStream(bais)) {
                Object o = ois.readObject();
            } catch (EOFException eof) {
                // normal completion
            } catch (ClassNotFoundException cnf) {
                Assert.fail("Deserializing", cnf);
            }
            Assert.assertTrue(allowed, "filter should have thrown an exception");
        } catch (IllegalArgumentException iae) {
            Assert.fail("bad format pattern", iae);
        } catch (InvalidClassException ice) {
            Assert.assertFalse(allowed, "filter should not have thrown an exception: " + ice);
        } catch (IOException ioe) {
            Assert.fail("Unexpected IOException", ioe);
        }
    }
}
