/*
 * Copyright (c) 2012, 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.
 */

/**
 * @test
 * @bug 7142509
 * @summary Cipher.doFinal(ByteBuffer,ByteBuffer) fails to
 *     process when in.remaining() == 0
 */

import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/*
 * Simple test case to show that Cipher.doFinal(ByteBuffer, ByteBuffer) fails to
 * process the data internally buffered inBB the cipher when input.remaining()
 * == 0 and at least one buffer is a direct buffer.
 */
public class DirectBBRemaining {

    private static Random random = new SecureRandom();
    private static int testSizes = 40;
    private static int outputFrequency = 5;

    public static void main(String args[]) throws Exception {
        boolean failedOnce = false;
        Exception failedReason = null;

        byte[] keyBytes = new byte[8];
        random.nextBytes(keyBytes);
        SecretKey key = new SecretKeySpec(keyBytes, "DES");

        Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding", "SunJCE");
        cipher.init(Cipher.ENCRYPT_MODE, key);

        /*
         * Iterate through various sizes to make sure that the code does empty
         * blocks, single partial blocks, 1 full block, full + partial blocks,
         * multiple full blocks, etc. 5 blocks (using DES) is probably overkill
         * but will feel more confident the fix isn't breaking anything.
         */
        System.out.println("Output test results for every "
                + outputFrequency + " tests...");

        for (int size = 0; size <= testSizes; size++) {
            boolean output = (size % outputFrequency) == 0;
            if (output) {
                System.out.print("\nTesting buffer size: " + size + ":");
            }

            int outSize = cipher.getOutputSize(size);

            try {
                encrypt(cipher, size,
                        ByteBuffer.allocate(size),
                        ByteBuffer.allocate(outSize),
                        ByteBuffer.allocateDirect(size),
                        ByteBuffer.allocateDirect(outSize),
                        output);
            } catch (Exception e) {
                System.out.print("\n    Failed with size " + size);
                failedOnce = true;
                failedReason = e;

                // If we got an exception, let's be safe for future
                // testing and reset the cipher to a known good state.
                cipher.init(Cipher.ENCRYPT_MODE, key);
            }
        }
        if (failedOnce) {
            throw failedReason;
        }
        System.out.println("\nTest Passed...");
    }

    private enum TestVariant {

        HEAP_HEAP, HEAP_DIRECT, DIRECT_HEAP, DIRECT_DIRECT
    };

    private static void encrypt(Cipher cipher, int size,
            ByteBuffer heapIn, ByteBuffer heapOut,
            ByteBuffer directIn, ByteBuffer directOut,
            boolean output) throws Exception {

        ByteBuffer inBB = null;
        ByteBuffer outBB = null;

        // Set up data and encrypt to known/expected values.
        byte[] testdata = new byte[size];
        random.nextBytes(testdata);
        byte[] expected = cipher.doFinal(testdata);

        for (TestVariant tv : TestVariant.values()) {
            if (output) {
                System.out.print(" " + tv);
            }

            switch (tv) {
            case HEAP_HEAP:
                inBB = heapIn;
                outBB = heapOut;
                break;
            case HEAP_DIRECT:
                inBB = heapIn;
                outBB = directOut;
                break;
            case DIRECT_HEAP:
                inBB = directIn;
                outBB = heapOut;
                break;
            case DIRECT_DIRECT:
                inBB = directIn;
                outBB = directOut;
                break;
            }

            inBB.clear();
            outBB.clear();

            inBB.put(testdata);
            inBB.flip();

            // Process all data in one shot, but don't call doFinal() yet.
            // May store up to n-1 bytes (w/block size n) internally.
            cipher.update(inBB, outBB);
            if (inBB.hasRemaining()) {
                throw new Exception("buffer not empty");
            }

            // finish encryption and process all data buffered
            cipher.doFinal(inBB, outBB);
            outBB.flip();

            // validate output size
            if (outBB.remaining() != expected.length) {
                throw new Exception(
                        "incomplete encryption output, expected "
                        + expected.length + " bytes but was only "
                        + outBB.remaining() + " bytes");
            }

            // validate output data
            byte[] encrypted = new byte[outBB.remaining()];
            outBB.get(encrypted);
            if (!Arrays.equals(expected, encrypted)) {
                throw new Exception("bad encryption output");
            }

            if (!Arrays.equals(cipher.doFinal(), cipher.doFinal())) {
                throw new Exception("Internal buffers still held data!");
            }
        }
    }
}
