/*
 * Copyright (c) 1995, 2024, 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * 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.
 */

/*-
 *      news stream opener
 */

package sun.net.www;

import java.io.*;
import java.lang.reflect.Array;
import java.net.ProtocolException;
import java.util.Collections;
import java.util.*;

/** An RFC 844 or MIME message header.  Includes methods
    for parsing headers from incoming streams, fetching
    values, setting values, and printing headers.
    Key values of null are legal: they indicate lines in
    the header that don't have a valid key, but do have
    a value (this isn't legal according to the standard,
    but lines like this are everywhere). */
public final class MessageHeader {
    private String[] keys;
    private String[] values;
    private int nkeys;

    // max number of bytes for headers, <=0 means unlimited;
    // this corresponds to the length of the names, plus the length
    // of the values, plus an overhead of 32 bytes per name: value
    // pair.
    // Note: we use the same definition as HTTP/2 SETTINGS_MAX_HEADER_LIST_SIZE
    // see RFC 9113, section 6.5.2.
    // https://www.rfc-editor.org/rfc/rfc9113.html#SETTINGS_MAX_HEADER_LIST_SIZE
    private final int maxHeaderSize;

    // Aggregate size of the field lines (name + value + 32) x N
    // that have been parsed and accepted so far.
    // This is defined as a long to force promotion to long
    // and avoid overflows; see checkNewSize;
    private long size;

    public MessageHeader () {
       this(0);
    }

    public MessageHeader (int maxHeaderSize) {
        this.maxHeaderSize = maxHeaderSize;
        grow();
    }

    public MessageHeader (InputStream is) throws java.io.IOException {
        maxHeaderSize = 0;
        parseHeader(is);
    }

    /**
     * Returns list of header names in a comma separated list
     */
    public synchronized String getHeaderNamesInList() {
        StringJoiner joiner = new StringJoiner(",");
        for (int i=0; i<nkeys; i++) {
            joiner.add(keys[i]);
        }
        return joiner.toString();
    }

    /**
     * Reset a message header (all key/values removed)
     */
    public synchronized void reset() {
        keys = null;
        values = null;
        nkeys = 0;
        grow();
    }

    /**
     * Find the value that corresponds to this key.
     * It finds only the first occurrence of the key.
     * @param k the key to find.
     * @return null if not found.
     */
    public synchronized String findValue(String k) {
        if (k == null) {
            for (int i = nkeys; --i >= 0;)
                if (keys[i] == null)
                    return values[i];
        } else
            for (int i = nkeys; --i >= 0;) {
                if (k.equalsIgnoreCase(keys[i]))
                    return values[i];
            }
        return null;
    }

    // return the location of the key
    public synchronized int getKey(String k) {
        for (int i = nkeys; --i >= 0;)
            if ((keys[i] == k) ||
                (k != null && k.equalsIgnoreCase(keys[i])))
                return i;
        return -1;
    }

    public synchronized String getKey(int n) {
        if (n < 0 || n >= nkeys) return null;
        return keys[n];
    }

    public synchronized String getValue(int n) {
        if (n < 0 || n >= nkeys) return null;
        return values[n];
    }

    /** Deprecated: Use multiValueIterator() instead.
     *
     *  Find the next value that corresponds to this key.
     *  It finds the first value that follows v. To iterate
     *  over all the values of a key use:
     *  <pre>
     *          for(String v=h.findValue(k); v!=null; v=h.findNextValue(k, v)) {
     *              ...
     *          }
     *  </pre>
     */
    public synchronized String findNextValue(String k, String v) {
        boolean foundV = false;
        if (k == null) {
            for (int i = nkeys; --i >= 0;)
                if (keys[i] == null)
                    if (foundV)
                        return values[i];
                    else if (values[i] == v)
                        foundV = true;
        } else
            for (int i = nkeys; --i >= 0;)
                if (k.equalsIgnoreCase(keys[i]))
                    if (foundV)
                        return values[i];
                    else if (values[i] == v)
                        foundV = true;
        return null;
    }

    /**
     * Removes bare Negotiate and Kerberos headers when an "NTLM ..."
     * appears. All Performed on headers with key being k.
     * @return true if there is a change
     */
    public boolean filterNTLMResponses(String k) {
        boolean found = false;
        for (int i=0; i<nkeys; i++) {
            if (k.equalsIgnoreCase(keys[i])
                    && values[i] != null && values[i].length() > 5
                    && values[i].substring(0, 5).equalsIgnoreCase("NTLM ")) {
                found = true;
                break;
            }
        }
        if (found) {
            int j = 0;
            for (int i=0; i<nkeys; i++) {
                if (k.equalsIgnoreCase(keys[i]) && (
                        "Negotiate".equalsIgnoreCase(values[i]) ||
                        "Kerberos".equalsIgnoreCase(values[i]))) {
                    continue;
                }
                if (i != j) {
                    keys[j] = keys[i];
                    values[j] = values[i];
                }
                j++;
            }
            if (j != nkeys) {
                nkeys = j;
                return true;
            }
        }
        return false;
    }

    class HeaderIterator implements Iterator<String> {
        int index = 0;
        int next = -1;
        String key;
        boolean haveNext = false;
        Object lock;

        public HeaderIterator (String k, Object lock) {
            key = k;
            this.lock = lock;
        }
        public boolean hasNext () {
            synchronized (lock) {
                if (haveNext) {
                    return true;
                }
                while (index < nkeys) {
                    if (key.equalsIgnoreCase (keys[index])) {
                        haveNext = true;
                        next = index++;
                        return true;
                    }
                    index ++;
                }
                return false;
            }
        }
        public String next() {
            synchronized (lock) {
                if (haveNext) {
                    haveNext = false;
                    return values [next];
                }
                if (hasNext()) {
                    return next();
                } else {
                    throw new NoSuchElementException ("No more elements");
                }
            }
        }
        public void remove () {
            throw new UnsupportedOperationException ("remove not allowed");
        }
    }

    /**
     * return an Iterator that returns all values of a particular
     * key in sequence
     */
    public Iterator<String> multiValueIterator (String k) {
        return new HeaderIterator (k, this);
    }

    public synchronized Map<String, List<String>> getHeaders() {
        return getHeaders(null);
    }

    public synchronized Map<String, List<String>> getHeaders(String[] excludeList) {
        return filterAndAddHeaders(excludeList, null);
    }

    public synchronized Map<String, List<String>> filterAndAddHeaders(
            String[] excludeList, Map<String, List<String>>  include) {
        boolean skipIt = false;
        Map<String, List<String>> m = new HashMap<>();
        for (int i = 0; i < nkeys; i++) {
            if (excludeList != null) {
                // check if the key is in the excludeList.
                // if so, don't include it in the Map.
                for (int j = 0; j < excludeList.length; j++) {
                    if ((excludeList[j] != null) &&
                        (excludeList[j].equalsIgnoreCase(keys[i]))) {
                        skipIt = true;
                        break;
                    }
                }
            }
            if (!skipIt) {
                List<String> l = m.get(keys[i]);
                if (l == null) {
                    l = new ArrayList<>();
                    m.put(keys[i], l);
                }
                l.add(values[i]);
            } else {
                // reset the flag
                skipIt = false;
            }
        }

        if (include != null) {
                for (Map.Entry<String,List<String>> entry: include.entrySet()) {
                List<String> l = m.get(entry.getKey());
                if (l == null) {
                    l = new ArrayList<>();
                    m.put(entry.getKey(), l);
                }
                l.addAll(entry.getValue());
            }
        }

        for (String key : m.keySet()) {
            m.put(key, Collections.unmodifiableList(m.get(key)));
        }

        return Collections.unmodifiableMap(m);
    }

    /** Check if a line of message header looks like a request line.
     * This method does not perform a full validation but simply
     * returns false if the line does not end with 'HTTP/[1-9].[0-9]'
     * @param line the line to check.
     * @return true if the line might be a request line.
     */
    private static boolean isRequestline(String line) {
        String k = line.trim();
        int i = k.lastIndexOf(' ');
        if (i <= 0) return false;
        int len = k.length();
        if (len - i < 9) return false;

        char c1 = k.charAt(len-3);
        char c2 = k.charAt(len-2);
        char c3 = k.charAt(len-1);
        if (c1 < '1' || c1 > '9') return false;
        if (c2 != '.') return false;
        if (c3 < '0' || c3 > '9') return false;

        return (k.substring(i+1, len-3).equalsIgnoreCase("HTTP/"));
    }

    /** Prints the key-value pairs represented by this
        header. Also prints the RFC required blank line
        at the end. Omits pairs with a null key. Omits
        colon if key-value pair is the requestline. */
    public void print(PrintStream p) {
        // no synchronization: use cloned arrays instead.
        String[] k; String[] v; int n;
        synchronized (this) { n = nkeys; k = keys.clone(); v = values.clone(); }
        print(n, k, v, p);
    }


    /** Prints the key-value pairs represented by this
        header. Also prints the RFC required blank line
        at the end. Omits pairs with a null key. Omits
        colon if key-value pair is the requestline. */
    private  static void print(int nkeys, String[] keys, String[] values, PrintStream p) {
        for (int i = 0; i < nkeys; i++)
            if (keys[i] != null) {
                StringBuilder sb = new StringBuilder(keys[i]);
                if (values[i] != null) {
                    sb.append(": " + values[i]);
                } else if (i != 0 || !isRequestline(keys[i])) {
                    sb.append(":");
                }
                p.print(sb.append("\r\n"));
            }
        p.print("\r\n");
        p.flush();
    }

    /** Adds a key value pair to the end of the
        header.  Duplicates are allowed */
    public synchronized void add(String k, String v) {
        grow();
        keys[nkeys] = k;
        values[nkeys] = v;
        nkeys++;
    }

    /** Prepends a key value pair to the beginning of the
        header.  Duplicates are allowed */
    public synchronized void prepend(String k, String v) {
        grow();
        for (int i = nkeys; i > 0; i--) {
            keys[i] = keys[i-1];
            values[i] = values[i-1];
        }
        keys[0] = k;
        values[0] = v;
        nkeys++;
    }

    /** Overwrite the previous key/val pair at location 'i'
     * with the new k/v.  If the index didn't exist before
     * the key/val is simply tacked onto the end.
     */

    public synchronized void set(int i, String k, String v) {
        grow();
        if (i < 0) {
            return;
        } else if (i >= nkeys) {
            add(k, v);
        } else {
            keys[i] = k;
            values[i] = v;
        }
    }


    /** grow the key/value arrays as needed */

    private void grow() {
        if (keys == null || nkeys >= keys.length) {
            String[] nk = new String[nkeys + 4];
            String[] nv = new String[nkeys + 4];
            if (keys != null)
                System.arraycopy(keys, 0, nk, 0, nkeys);
            if (values != null)
                System.arraycopy(values, 0, nv, 0, nkeys);
            keys = nk;
            values = nv;
        }
    }

    /**
     * Remove the key from the header. If there are multiple values under
     * the same key, they are all removed.
     * Nothing is done if the key doesn't exist.
     * After a remove, the other pairs' order are not changed.
     * @param k the key to remove
     */
    public synchronized void remove(String k) {
        if(k == null) {
            for (int i = 0; i < nkeys; i++) {
                while (keys[i] == null && i < nkeys) {
                    for(int j=i; j<nkeys-1; j++) {
                        keys[j] = keys[j+1];
                        values[j] = values[j+1];
                    }
                    nkeys--;
                }
            }
        } else {
            for (int i = 0; i < nkeys; i++) {
                while (k.equalsIgnoreCase(keys[i]) && i < nkeys) {
                    for(int j=i; j<nkeys-1; j++) {
                        keys[j] = keys[j+1];
                        values[j] = values[j+1];
                    }
                    nkeys--;
                }
            }
        }
    }

    /** Sets the value of a key.  If the key already
        exists in the header, it's value will be
        changed.  Otherwise a new key/value pair will
        be added to the end of the header. */
    public synchronized void set(String k, String v) {
        for (int i = nkeys; --i >= 0;)
            if (k.equalsIgnoreCase(keys[i])) {
                values[i] = v;
                return;
            }
        add(k, v);
    }

    /** Set's the value of a key only if there is no
     *  key with that value already.
     */

    public synchronized void setIfNotSet(String k, String v) {
        if (findValue(k) == null) {
            add(k, v);
        }
    }

    /** Convert a message-id string to canonical form (strips off
        leading and trailing {@literal <>s}) */
    public static String canonicalID(String id) {
        if (id == null)
            return "";
        int st = 0;
        int len = id.length();
        boolean substr = false;
        int c;
        while (st < len && ((c = id.charAt(st)) == '<' ||
                            c <= ' ')) {
            st++;
            substr = true;
        }
        while (st < len && ((c = id.charAt(len - 1)) == '>' ||
                            c <= ' ')) {
            len--;
            substr = true;
        }
        return substr ? id.substring(st, len) : id;
    }

    /** Parse a MIME header from an input stream. */
    public void parseHeader(InputStream is) throws java.io.IOException {
        synchronized (this) {
            nkeys = 0;
            size = 0;
        }
        mergeHeader(is);
    }

    private void checkMaxHeaderSize(int sz) throws ProtocolException {
        if (maxHeaderSize > 0) checkNewSize(size, sz, 0);
    }

    private long checkNewSize(long size, int name, int value) throws ProtocolException {
        // See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
        long newSize = size + name + value + 32;
        if (maxHeaderSize > 0 && newSize > maxHeaderSize) {
            Arrays.fill(keys, 0, nkeys, null);
            Arrays.fill(values,0, nkeys, null);
            nkeys = 0;
            throw new ProtocolException(String.format("Header size too big: %s > %s",
                    newSize, maxHeaderSize));
        }
        return newSize;
    }

    /** Parse and merge a MIME header from an input stream. */
    @SuppressWarnings("fallthrough")
    public void mergeHeader(InputStream is) throws java.io.IOException {
        if (is == null)
            return;
        char s[] = new char[10];
        int firstc = is.read();
        while (firstc != '\n' && firstc != '\r' && firstc >= 0) {
            int len = 0;
            int keyend = -1;
            int c;
            boolean inKey = firstc > ' ';
            s[len++] = (char) firstc;
            checkMaxHeaderSize(len);
    parseloop:{
                // We start parsing for a new name value pair here.
                // The max header size includes an overhead of 32 bytes per
                // name value pair.
                // See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
                long maxRemaining = maxHeaderSize > 0
                        ? maxHeaderSize - size - 32
                        : Long.MAX_VALUE;
                while ((c = is.read()) >= 0) {
                    switch (c) {
                      case ':':
                        if (inKey && len > 0)
                            keyend = len;
                        inKey = false;
                        break;
                      case '\t':
                        c = ' ';
                      /*fall through*/
                      case ' ':
                        inKey = false;
                        break;
                      case '\r':
                      case '\n':
                        firstc = is.read();
                        if (c == '\r' && firstc == '\n') {
                            firstc = is.read();
                            if (firstc == '\r')
                                firstc = is.read();
                        }
                        if (firstc == '\n' || firstc == '\r' || firstc > ' ')
                            break parseloop;
                        /* continuation */
                        c = ' ';
                        break;
                    }
                    if (len >= s.length) {
                        char ns[] = new char[s.length * 2];
                        System.arraycopy(s, 0, ns, 0, len);
                        s = ns;
                    }
                    s[len++] = (char) c;
                    if (maxHeaderSize > 0 && len > maxRemaining) {
                        checkMaxHeaderSize(len);
                    }
                }
                firstc = -1;
            }
            while (len > 0 && s[len - 1] <= ' ')
                len--;
            String k;
            if (keyend <= 0) {
                k = null;
                keyend = 0;
            } else {
                k = String.copyValueOf(s, 0, keyend);
                if (keyend < len && s[keyend] == ':')
                    keyend++;
                while (keyend < len && s[keyend] <= ' ')
                    keyend++;
            }
            String v;
            if (keyend >= len)
                v = new String();
            else
                v = String.copyValueOf(s, keyend, len - keyend);
            int klen = k == null ? 0 : k.length();

            size = checkNewSize(size, klen, v.length());
            add(k, v);
        }
    }

    public synchronized String toString() {
        String result = super.toString() + " " + nkeys + " pairs: ";
        for (int i = 0; i < keys.length && i < nkeys; i++) {
            result += "{"+keys[i]+": "+values[i]+"}";
        }
        return result;
    }
}
