/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.
 */

/**
* @author Vladimir N. Molotkov
* @version $Revision$
*/

package org.apache.harmony.security.tests.java.security;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import junit.framework.TestCase;
import org.apache.harmony.security.tests.support.MDGoldenData;
import org.apache.harmony.security.tests.support.MyMessageDigest1;
import tests.support.Support_OutputStream;

/**
 * Tests for fields and methods of class <code>DigestInputStream</code>
 *
 */
public class DigestOutputStreamTest extends TestCase {

    /**
     * Message digest algorithm name used during testing
     */
    private static final String algorithmName[] = {
            "SHA-1",
            "SHA",
            "SHA1",
            "SHA-256",
            "SHA-384",
            "SHA-512",
            "MD5",
    };
    /**
     * Chunk size for read(byte, off, len) tests
     */
    private static final int CHUNK_SIZE = 32;
    /**
     * Test message for digest computations
     */
    private static final byte[] myMessage = MDGoldenData.getMessage();
    /**
     * The length of test message
     */
    private static final int MY_MESSAGE_LEN = myMessage.length;

    //
    // Tests
    //

    /**
     * java.security.DigestOutputStream#DigestOutputStream(java.io.OutputStream,
     *        java.security.MessageDigest)
     */
    public void test_CtorLjava_io_OutputStreamLjava_security_MessageDigest() {

        // non-null parameters
        MessageDigest md = new MyMessageDigest1();
        MyOutputStream out = new MyOutputStream();

        MyDigestOutputStream dos = new MyDigestOutputStream(out, md);
        assertSame(out, dos.myOutputStream());
        assertSame(md, dos.myMessageDigest());

        // null parameters
        dos = new MyDigestOutputStream(null, null);
        assertNull(dos.myOutputStream());
        assertNull(dos.myMessageDigest());

        dos = new MyDigestOutputStream(null, md);
        assertNull(dos.myOutputStream());
        assertNotNull(dos.myMessageDigest());

        dos = new MyDigestOutputStream(out, null);
        assertNotNull(dos.myOutputStream());
        assertNull(dos.myMessageDigest());
    }

    /**
     * java.security.DigestOutputStream#getMessageDigest()
     */
    public void test_getMessageDigest() {

        MessageDigest digest = new MyMessageDigest1();
        OutputStream out = new MyOutputStream();

        // non-null parameter
        DigestOutputStream dos = new DigestOutputStream(out, digest);
        assertSame(digest, dos.getMessageDigest());

        // null parameter
        dos = new DigestOutputStream(out, null);
        assertNull("getMessageDigest should have returned null", dos.getMessageDigest());
    }

    /**
     * java.security.DigestOutputStream#setMessageDigest(MessageDigest)
     */
    public void test_setMessageDigestLjava_security_MessageDigest() {

        MessageDigest digest = new MyMessageDigest1();
        OutputStream out = new MyOutputStream();

        DigestOutputStream dos = new DigestOutputStream(out, null);

        // non-null parameter
        dos.setMessageDigest(digest);
        assertSame(digest, dos.getMessageDigest());

        // null parameter
        dos.setMessageDigest(null);
        assertNull("getMessageDigest should have returned null", dos.getMessageDigest());
    }


    /**
     * Test #1 for <code>write(int)</code> method<br>
     *
     * Assertion: writes the byte to the output stream<br>
     * Assertion: updates associated digest<br>
     */
    public final void testWriteint01()
        throws IOException {
        for (int k=0; k<algorithmName.length; k++) {
            try {
                MessageDigest md = MessageDigest.getInstance(algorithmName[k]);
                ByteArrayOutputStream bos = new ByteArrayOutputStream(MY_MESSAGE_LEN);
                DigestOutputStream dos = new DigestOutputStream(bos, md);
                for (int i=0; i<MY_MESSAGE_LEN; i++) {
                    dos.write(myMessage[i]);
                }
                // check that bytes have been written correctly
                assertTrue("write", Arrays.equals(MDGoldenData.getMessage(), bos.toByteArray()));
                // check that associated digest has been updated properly
                assertTrue("update", Arrays.equals(dos.getMessageDigest().digest(),
                                                   MDGoldenData.getDigest(algorithmName[k])));
                return;
            } catch (NoSuchAlgorithmException e) {
                // allowed failure
            }
        }
        fail(getName() + ": no MessageDigest algorithms available - test not performed");
    }

    /**
     * Test #2 for <code>write(int)</code> method<br>
     * Test #1 for <code>on(boolean)</code> method<br>
     *
     * Assertion: <code>write(int)</code> must not update digest if it is off<br>
     * Assertion: <code>on(boolean)</code> turns digest functionality on
     * if <code>true</code> passed as a parameter or off if <code>false</code>
     * passed
     */
    public final void testWriteint02()
        throws IOException {
        for (int k=0; k<algorithmName.length; k++) {
            try {
                MessageDigest md = MessageDigest.getInstance(algorithmName[k]);
                ByteArrayOutputStream bos = new ByteArrayOutputStream(MY_MESSAGE_LEN);
                DigestOutputStream dos = new DigestOutputStream(bos, md);

                // turn digest off
                dos.on(false);

                for (int i=0; i<MY_MESSAGE_LEN; i++) {
                    dos.write(myMessage[i]);
                }

                // check that bytes have been written correctly
                assertTrue("write", Arrays.equals(MDGoldenData.getMessage(), bos.toByteArray()));
                // check that digest value has not been updated by write()
                assertTrue("update", Arrays.equals(dos.getMessageDigest().digest(),
                                                   MDGoldenData.getDigest(algorithmName[k]+"_NU")));
                return;
            } catch (NoSuchAlgorithmException e) {
                // allowed failure
            }
        }
        fail(getName() + ": no MessageDigest algorithms available - test not performed");
    }

    /**
     * Test #3 for <code>write(int)</code> method<br>
     *
     * Assertion: broken <code>DigestOutputStream</code>instance:
     * <code>OutputStream</code> not set. <code>write(int)</code> must
     * not work
     */
    public final void testWriteint03() throws IOException {
        for (int k=0; k<algorithmName.length; k++) {
            try {
                MessageDigest md = MessageDigest.getInstance(algorithmName[k]);
                DigestOutputStream dos = new DigestOutputStream(null, md);
                // must result in an exception
                try {
                    for (int i=0; i<MY_MESSAGE_LEN; i++) {
                        dos.write(myMessage[i]);
                    }
                    fail("OutputStream not set. write(int) must not work");
                } catch (Exception e) {
                    return;
                }
            } catch (NoSuchAlgorithmException e) {
                // allowed failure
            }
        }
        fail(getName() + ": no MessageDigest algorithms available - test not performed");
    }

    /**
     * Test #4 for <code>write(int)</code> method<br>
     *
     * Assertion: broken <code>DigestOutputStream</code>instance:
     * associated <code>MessageDigest</code> not set.
     * <code>write(int)</code> must not work when digest
     * functionality is on
     */
    public final void testWriteint04() throws IOException {
        OutputStream os = new ByteArrayOutputStream(MY_MESSAGE_LEN);
        DigestOutputStream dos = new DigestOutputStream(os, null);

        // must result in an exception
        try {
            for (int i=0; i<MY_MESSAGE_LEN; i++) {
                dos.write(myMessage[i]);
            }
            fail("OutputStream not set. write(int) must not work");
        } catch (Exception e) {
            return;
        }
    }

    /**
     * Test #5 for <code>write(int)</code> method<br>
     * Test #2 for <code>on(boolean)</code> method<br>
     *
     * Assertion: broken <code>DigestOutputStream</code>instance:
     * associated <code>MessageDigest</code> not set.
     * <code>write(int)</code> must work when digest
     * functionality is off
     */
    public final void testWriteint05() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream(MY_MESSAGE_LEN);
        DigestOutputStream dos = new DigestOutputStream(bos, null);
        // set digest functionality to off
        dos.on(false);
        // the following must pass without any exception
        for (int i=0; i<MY_MESSAGE_LEN; i++) {
            dos.write(myMessage[i]);
        }
        // check that bytes have been written correctly
        assertTrue(Arrays.equals(MDGoldenData.getMessage(), bos.toByteArray()));
    }

    /**
     * Test #1 for <code>write(byte[],int,int)</code> method<br>
     *
     * Assertion: put bytes into output stream<br>
     *
     * Assertion: updates associated digest<br>
     */
    public final void test_write$BII_1() throws IOException {
        for (int k=0; k<algorithmName.length; k++) {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream(MY_MESSAGE_LEN);
                MessageDigest md = MessageDigest.getInstance(algorithmName[k]);
                DigestOutputStream dos = new DigestOutputStream(bos, md);

                // write message at once
                dos.write(myMessage, 0, MY_MESSAGE_LEN);

                // check write
                assertTrue("write", Arrays.equals(myMessage, bos.toByteArray()));
                // check that associated digest has been updated properly
                assertTrue("update", Arrays.equals(dos.getMessageDigest().digest(),
                                                   MDGoldenData.getDigest(algorithmName[k])));
                return;
            } catch (NoSuchAlgorithmException e) {
                // allowed failure
            }
        }
        fail(getName() + ": no MessageDigest algorithms available - test not performed");
    }

    /**
     * Test #2 for <code>write(byte[],int,int)</code> method<br>
     *
     * Assertion: put bytes into output stream<br>
     *
     * Assertion: updates associated digest<br>
     */
    public final void test_write$BII_2() throws IOException {
        // check precondition
        assertEquals(0, MY_MESSAGE_LEN % CHUNK_SIZE);
        for (int k=0; k<algorithmName.length; k++) {
            try {

                ByteArrayOutputStream bos = new ByteArrayOutputStream(MY_MESSAGE_LEN);
                MessageDigest md = MessageDigest.getInstance(algorithmName[k]);
                DigestOutputStream dos = new DigestOutputStream(bos, md);

                // write message by chunks
                for (int i=0; i<MY_MESSAGE_LEN/CHUNK_SIZE; i++) {
                    dos.write(myMessage, i*CHUNK_SIZE, CHUNK_SIZE);
                }
                // check write
                assertTrue("write", Arrays.equals(myMessage, bos.toByteArray()));
                // check that associated digest has been updated properly
                assertTrue("update", Arrays.equals(dos.getMessageDigest().digest(),
                        MDGoldenData.getDigest(algorithmName[k])));
                return;
            } catch (NoSuchAlgorithmException e) {
                // allowed failure
            }
        }
        fail(getName() + ": no MessageDigest algorithms available - test not performed");
    }


    /**
     * Test #3 for <code>write(byte[],int,int)</code> method<br>
     *
     * Assertion: put bytes into output stream<br>
     *
     * Assertion: updates associated digest<br>
     */
    public final void test_write$BII_3()
        throws NoSuchAlgorithmException,
               IOException {
        // check precondition
        assertTrue(MY_MESSAGE_LEN % (CHUNK_SIZE+1) != 0);

        for (int k=0; k<algorithmName.length; k++) {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream(MY_MESSAGE_LEN);
                MessageDigest md = MessageDigest.getInstance(algorithmName[k]);
                DigestOutputStream dos = new DigestOutputStream(bos, md);

                // write message by chunks
                for (int i=0; i<MY_MESSAGE_LEN/(CHUNK_SIZE+1); i++) {
                    dos.write(myMessage, i*(CHUNK_SIZE+1), CHUNK_SIZE+1);
                }
                // write remaining bytes
                dos.write(myMessage,
                        MY_MESSAGE_LEN/(CHUNK_SIZE+1)*(CHUNK_SIZE+1),
                        MY_MESSAGE_LEN % (CHUNK_SIZE+1));
                // check write
                assertTrue("write", Arrays.equals(myMessage, bos.toByteArray()));
                // check that associated digest has been updated properly
                assertTrue("update", Arrays.equals(dos.getMessageDigest().digest(),
                        MDGoldenData.getDigest(algorithmName[k])));
                return;
            } catch (NoSuchAlgorithmException e) {
                // allowed failure
            }
        }
        fail(getName() + ": no MessageDigest algorithms available - test not performed");
    }

    /**
     * Test #4 for <code>write(byte[],int,int)</code> method<br>
     *
     * Assertion: put bytes into output stream<br>
     *
     * Assertion: does not update associated digest if digest
     * functionality is off<br>
     */
    public final void test_write$BII_4()
        throws NoSuchAlgorithmException,
               IOException {
        // check precondition
        assertEquals(0, MY_MESSAGE_LEN % CHUNK_SIZE);

        for (int k=0; k<algorithmName.length; k++) {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream(MY_MESSAGE_LEN);
                MessageDigest md = MessageDigest.getInstance(algorithmName[k]);
                DigestOutputStream dos = new DigestOutputStream(bos, md);

                // set digest functionality off
                dos.on(false);

                // write message by chunks
                for (int i=0; i<MY_MESSAGE_LEN/CHUNK_SIZE; i++) {
                    dos.write(myMessage, i*CHUNK_SIZE, CHUNK_SIZE);
                }

                // check write
                assertTrue("write", Arrays.equals(myMessage, bos.toByteArray()));
                // check that associated digest has not been updated
                assertTrue("update", Arrays.equals(dos.getMessageDigest().digest(),
                        MDGoldenData.getDigest(algorithmName[k]+"_NU")));
                return;
            } catch (NoSuchAlgorithmException e) {
                // allowed failure
            }
        }
        fail(getName() + ": no MessageDigest algorithms available - test not performed");
    }

    /**
     * java.security.DigestOutputStream#write(byte[], int, int)
     */
    public void test_write$BII_6() throws Exception {

        // Regression form HARMONY-1091.
        MessageDigest md = new MyMessageDigest1();
        byte[] bytes = new byte[] { 1, 2 };
        DigestOutputStream dig = new DigestOutputStream(
                new ByteArrayOutputStream(), md);
        // buf == null
        try {
            dig.write(null, -1, 0);
            fail("No expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
        }
        // offset + len > buf.length
        try {
            dig.write(bytes, 0, bytes.length + 1);
            fail("No expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
        }
        // offset < 0
        try {
            dig.write(bytes, -1, 1);
            fail("No expected IndexOutOfBoundsException");
        } catch (IndexOutOfBoundsException e) {
        }
        // len < 0
        try {
            dig.write(bytes, 0, -1);
            fail("No expected IndexOutOfBoundsException");
        } catch (IndexOutOfBoundsException e) {
        }
    }

    /**
     * java.io.DigestOutputStream#write(byte[], int, int)
     */
    public void test_write$BII_7()
        throws IOException, NoSuchAlgorithmException {
        Support_OutputStream sos = new Support_OutputStream(MY_MESSAGE_LEN);
        MessageDigest md = MessageDigest.getInstance(algorithmName[0]);
        DigestOutputStream dos = new DigestOutputStream(sos, md);

        dos.write(myMessage, 0, MY_MESSAGE_LEN);

        try {
            // Support_OutputStream throws an IOException if the internal
            // buffer is full, which it should be now.
            dos.write(myMessage, 0, MY_MESSAGE_LEN);
            fail("Test 1: IOException expected.");
        } catch (IOException e) {
            // Expected.
        }
    }

    /**
     * Test for <code>on()</code> method<br>
     * Assertion: turns digest functionality on or off
     */
    public final void testOn() throws IOException {
        for (int k=0; k<algorithmName.length; k++) {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream(MY_MESSAGE_LEN);
                MessageDigest md = MessageDigest.getInstance(algorithmName[k]);
                DigestOutputStream dos = new DigestOutputStream(bos, md);

                // turn digest off
                dos.on(false);

                for (int i=0; i<MY_MESSAGE_LEN-1; i++) {
                    dos.write(myMessage[i]);
                }

                // turn digest on
                dos.on(true);

                // read remaining byte
                dos.write(myMessage[MY_MESSAGE_LEN-1]);

                byte[] digest = dos.getMessageDigest().digest();

                // check that digest value has been
                // updated by the last write(int) call
                assertFalse(Arrays.equals(digest,MDGoldenData.getDigest(algorithmName[k])));
                assertFalse(Arrays.equals(digest,MDGoldenData.getDigest(algorithmName[k]+"_NU")));
                return;
            } catch (NoSuchAlgorithmException e) {
                // allowed failure
            }
        }
        fail(getName() + ": no MessageDigest algorithms available - test not performed");
    }

    /**
     * Test for <code>toString()</code> method<br>
     * Assertion: returns <code>String</code> representation of this object
     */
    public final void testToString() throws NoSuchAlgorithmException {
        for (int k=0; k<algorithmName.length; k++) {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream(MY_MESSAGE_LEN);
                MessageDigest md = MessageDigest.getInstance(algorithmName[k]);
                DigestOutputStream dos = new DigestOutputStream(bos, md);

                assertNotNull(dos.toString());
                return;
            } catch (NoSuchAlgorithmException e) {
                // allowed failure
            }
        }
        fail(getName() + ": no MessageDigest algorithms available - test not performed");
    }

    /**
     * java.security.DigestOutputStream#on(boolean)
     */
    public void test_onZ() throws Exception {
        // Test for method void java.security.DigestOutputStream.on(boolean)
        DigestOutputStream dos = new DigestOutputStream(
                new ByteArrayOutputStream(), MessageDigest.getInstance("SHA"));
        dos.on(false);
        byte digestArray[] = { 23, 43, 44 };
        dos.write(digestArray, 1, 1);
        byte digestResult[] = dos.getMessageDigest().digest();
        byte expected[] = { -38, 57, -93, -18, 94, 107, 75, 13, 50, 85,
                -65, -17, -107, 96, 24, -112, -81, -40, 7, 9 };
        assertTrue("Digest did not return expected result.",
                   Arrays.equals(digestResult, expected));
        // now turn on processing and re-run
        dos.on(true);
        dos.write(digestArray, 1, 1);
        digestResult = dos.getMessageDigest().digest();
        byte expected1[] = { -87, 121, -17, 16, -52, 111, 106, 54, -33,
                107, -118, 50, 51, 7, -18, 59, -78, -30, -37, -100 };

        assertTrue("Digest did not return expected result.",
                   Arrays.equals(digestResult, expected1));
    }

    /**
     * java.security.DigestOutputStream#write(byte[], int, int)
     */
    public void test_write$BII_5() throws Exception {
        // Test for method void java.security.DigestOutputStream.write(byte [],
        // int, int)
            DigestOutputStream dos = new DigestOutputStream(
                new ByteArrayOutputStream(), MessageDigest.getInstance("SHA"));
            byte digestArray[] = { 23, 43, 44 };
            dos.write(digestArray, 1, 1);
            byte digestResult[] = dos.getMessageDigest().digest();
            byte expected[] = { -87, 121, -17, 16, -52, 111, 106, 54, -33, 107,
                    -118, 50, 51, 7, -18, 59, -78, -30, -37, -100 };

            assertTrue("Digest did not return expected result.",
                       Arrays.equals(digestResult, expected));
    }

    /**
     * java.security.DigestOutputStream#write(int)
     */
    public void test_writeI() throws Exception {
        // Test for method void java.security.DigestOutputStream.write(int)
            DigestOutputStream dos = new DigestOutputStream(
                new ByteArrayOutputStream(), MessageDigest.getInstance("SHA"));
            dos.write((byte) 43);
            byte digestResult[] = dos.getMessageDigest().digest();
            byte expected[] = { -87, 121, -17, 16, -52, 111, 106, 54, -33, 107,
                    -118, 50, 51, 7, -18, 59, -78, -30, -37, -100 };

            assertTrue("Digest did not return expected result.",
                       Arrays.equals(digestResult, expected));
    }

    private class MessageDigestWithUnsupportedUpdate extends MessageDigest {
        private MessageDigestWithUnsupportedUpdate() {
            super("SomeAlgorithm");
        }

        @Override
        protected void engineUpdate(byte input) {
            throw new UnsupportedOperationException();
        }

        @Override
        protected void engineUpdate(byte[] input, int offset, int len) {
            throw new UnsupportedOperationException();
        }

        @Override
        protected byte[] engineDigest() {
            return new byte[0];
        }

        @Override
        protected void engineReset() {

        }
    }

    public void test_write_writeToUnderlyingStreamBeforeUpdatingDigest() {
        MessageDigest messageDigestWithUnsupportedUpdate = new MessageDigestWithUnsupportedUpdate();
        OutputStream outputStreamThatThrowsIOException = new OutputStream() {
            @Override
            public void write(int b) throws IOException {
                throw new IOException();
            }
        };

        DigestOutputStream digestOutputStream = new DigestOutputStream(
                outputStreamThatThrowsIOException, messageDigestWithUnsupportedUpdate);

        // Writing throws an IOException (and not an UnsupportedOperationException) meaning than
        // it tried to write to the underlying stream before updating the digest.
        digestOutputStream.on(true);
        try {
            digestOutputStream.write(3);
            fail();
        } catch (IOException expected) {
        }

        digestOutputStream.on(true);
        try {
            digestOutputStream.write(new byte[10], 0, 10);
            fail();
        } catch (IOException expected) {
        }

        digestOutputStream.on(true);
        try {
            digestOutputStream.write(new byte[10]);
            fail();
        } catch (IOException expected) {
        }
    }

    private class MyOutputStream extends OutputStream {
        @Override
        public void write(int arg0) throws IOException {
        }
    }

    private class MyDigestOutputStream extends DigestOutputStream {
        public MyDigestOutputStream(OutputStream out, MessageDigest digest) {
            super(out, digest);
        }

        public MessageDigest myMessageDigest() {
            return digest;
        }

        public OutputStream myOutputStream() {
            return out;
        }
    }
}
