/*
 * 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 org.conscrypt;

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.conscrypt.OpenSSLX509CertificateFactory.ParsingException;

/**
 * An implementation of {@link CertPath} based on BoringSSL.
 */
final class OpenSSLX509CertPath extends CertPath {
    private static final long serialVersionUID = -3249106005255170761L;

    private static final byte[] PKCS7_MARKER = new byte[] {
            '-', '-', '-', '-', '-', 'B', 'E', 'G', 'I', 'N', ' ', 'P', 'K', 'C', 'S', '7'
    };

    private static final int PUSHBACK_SIZE = 64;

    /**
     * Supported encoding types for CerthPath. Used by the various APIs that
     * encode this into bytes such as {@link #getEncoded()}.
     */
    private enum Encoding {
        PKI_PATH("PkiPath"),
        PKCS7("PKCS7");

        private final String apiName;

        Encoding(String apiName) {
            this.apiName = apiName;
        }

        static Encoding findByApiName(String apiName) throws CertificateEncodingException {
            for (Encoding element : values()) {
                if (element.apiName.equals(apiName)) {
                    return element;
                }
            }

            return null;
        }
    }

    /** Unmodifiable list of encodings for the API. */
    private static final List<String> ALL_ENCODINGS = Collections.unmodifiableList(Arrays
            .asList(new String[] {
                    Encoding.PKI_PATH.apiName,
                    Encoding.PKCS7.apiName,
            }));

    private static final Encoding DEFAULT_ENCODING = Encoding.PKI_PATH;

    private final List<? extends X509Certificate> mCertificates;

    static Iterator<String> getEncodingsIterator() {
        return ALL_ENCODINGS.iterator();
    }

    OpenSSLX509CertPath(List<? extends X509Certificate> certificates) {
        super("X.509");

        mCertificates = certificates;
    }

    @Override
    public List<? extends Certificate> getCertificates() {
        return Collections.unmodifiableList(mCertificates);
    }

    private byte[] getEncoded(Encoding encoding) throws CertificateEncodingException {
        final OpenSSLX509Certificate[] certs = new OpenSSLX509Certificate[mCertificates.size()];
        final long[] certRefs = new long[certs.length];

        for (int i = 0, j = certs.length - 1; j >= 0; i++, j--) {
            final X509Certificate cert = mCertificates.get(i);

            if (cert instanceof OpenSSLX509Certificate) {
                certs[j] = (OpenSSLX509Certificate) cert;
            } else {
                certs[j] = OpenSSLX509Certificate.fromX509Der(cert.getEncoded());
            }

            certRefs[j] = certs[j].getContext();
        }

        switch (encoding) {
            case PKI_PATH:
                return NativeCrypto.ASN1_seq_pack_X509(certRefs);
            case PKCS7:
                return NativeCrypto.i2d_PKCS7(certRefs);
            default:
                throw new CertificateEncodingException("Unknown encoding");
        }
    }

    @Override
    public byte[] getEncoded() throws CertificateEncodingException {
        return getEncoded(DEFAULT_ENCODING);
    }

    @Override
    public byte[] getEncoded(String encoding) throws CertificateEncodingException {
        Encoding enc = Encoding.findByApiName(encoding);
        if (enc == null) {
            throw new CertificateEncodingException("Invalid encoding: " + encoding);
        }

        return getEncoded(enc);
    }

    @Override
    public Iterator<String> getEncodings() {
        return getEncodingsIterator();
    }

    private static CertPath fromPkiPathEncoding(InputStream inStream) throws CertificateException {
        OpenSSLBIOInputStream bis = new OpenSSLBIOInputStream(inStream, true);

        final boolean markable = inStream.markSupported();
        if (markable) {
            inStream.mark(PUSHBACK_SIZE);
        }

        final long[] certRefs;
        try {
            certRefs = NativeCrypto.ASN1_seq_unpack_X509_bio(bis.getBioContext());
        } catch (Exception e) {
            if (markable) {
                try {
                    inStream.reset();
                } catch (IOException ignored) {
                    // Ignored
                }
            }
            throw new CertificateException(e);
        } finally {
            bis.release();
        }

        if (certRefs == null) {
            return new OpenSSLX509CertPath(Collections.<X509Certificate> emptyList());
        }

        final List<OpenSSLX509Certificate> certs =
                new ArrayList<OpenSSLX509Certificate>(certRefs.length);
        for (int i = certRefs.length - 1; i >= 0; i--) {
            if (certRefs[i] == 0) {
                continue;
            }
            try {
                certs.add(new OpenSSLX509Certificate(certRefs[i]));
            } catch (ParsingException e) {
                throw new CertificateParsingException(e);
            }
        }

        return new OpenSSLX509CertPath(certs);
    }

    private static CertPath fromPkcs7Encoding(InputStream inStream) throws CertificateException {
        try {
            if (inStream == null || inStream.available() == 0) {
                return new OpenSSLX509CertPath(Collections.<X509Certificate> emptyList());
            }
        } catch (IOException e) {
            throw new CertificateException("Problem reading input stream", e);
        }

        final boolean markable = inStream.markSupported();
        if (markable) {
            inStream.mark(PUSHBACK_SIZE);
        }

        /* Attempt to see if this is a PKCS#7 bag. */
        final PushbackInputStream pbis = new PushbackInputStream(inStream, PUSHBACK_SIZE);
        try {
            final byte[] buffer = new byte[PKCS7_MARKER.length];

            final int len = pbis.read(buffer);
            if (len < 0) {
                /* No need to reset here. The stream was empty or EOF. */
                throw new ParsingException("inStream is empty");
            }
            pbis.unread(buffer, 0, len);

            if (len == PKCS7_MARKER.length && Arrays.equals(PKCS7_MARKER, buffer)) {
                return new OpenSSLX509CertPath(OpenSSLX509Certificate.fromPkcs7PemInputStream(pbis));
            }

            return new OpenSSLX509CertPath(OpenSSLX509Certificate.fromPkcs7DerInputStream(pbis));
        } catch (Exception e) {
            if (markable) {
                try {
                    inStream.reset();
                } catch (IOException ignored) {
                    // Ignored
                }
            }
            throw new CertificateException(e);
        }
    }

    private static CertPath fromEncoding(InputStream inStream, Encoding encoding)
            throws CertificateException {
        switch (encoding) {
            case PKI_PATH:
                return fromPkiPathEncoding(inStream);
            case PKCS7:
                return fromPkcs7Encoding(inStream);
            default:
                throw new CertificateEncodingException("Unknown encoding");
        }
    }

    static CertPath fromEncoding(InputStream inStream, String encoding)
            throws CertificateException {
        if (inStream == null) {
            throw new CertificateException("inStream == null");
        }

        Encoding enc = Encoding.findByApiName(encoding);
        if (enc == null) {
            throw new CertificateException("Invalid encoding: " + encoding);
        }

        return fromEncoding(inStream, enc);
    }

    static CertPath fromEncoding(InputStream inStream) throws CertificateException {
        if (inStream == null) {
            throw new CertificateException("inStream == null");
        }

        return fromEncoding(inStream, DEFAULT_ENCODING);
    }
}
