/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 * SPDX-License-Identifier: curl
 *
 ***************************************************************************/

#include "unitcheck.h"

#include "bufq.h"
#include "capsule.h"

#if defined(USE_PROXY_HTTP3) && defined(USE_NGTCP2) && \
  !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
static void queue_bytes(struct bufq *q, const unsigned char *src, size_t len)
{
  size_t nwritten = 0;
  CURLcode result = Curl_bufq_write(q, src, len, &nwritten);
  fail_unless(result == CURLE_OK, "queue failed");
  fail_unless(nwritten == len, "queue short write");
}
#endif

#if defined(USE_PROXY_HTTP3) && \
  !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
static void check_capsule_hdr(size_t payload_len,
                              const unsigned char *expected,
                              size_t expected_len)
{
  unsigned char hdr[HTTP_CAPSULE_HEADER_MAX_SIZE];
  size_t hdr_len;

  memset(hdr, 0xA5, sizeof(hdr));
  hdr_len = capsule_encap_udp_hdr(hdr, sizeof(hdr), payload_len);
  fail_unless(hdr_len == expected_len, "capsule header length mismatch");
  fail_unless(!memcmp(hdr, expected, expected_len),
              "capsule header bytes mismatch");
}

static void test_capsule_encap_udp_hdr_boundaries(void)
{
  const unsigned char p0[] = { 0x00, 0x01, 0x00 };
  const unsigned char p62[] = { 0x00, 0x3F, 0x00 };
  const unsigned char p63[] = { 0x00, 0x40, 0x40, 0x00 };
  const unsigned char p64[] = { 0x00, 0x40, 0x41, 0x00 };
  const unsigned char p16382[] = { 0x00, 0x7F, 0xFF, 0x00 };
  const unsigned char p16383[] = { 0x00, 0x80, 0x00, 0x40, 0x00, 0x00 };
  const unsigned char p16384[] = { 0x00, 0x80, 0x00, 0x40, 0x01, 0x00 };

  check_capsule_hdr(0, p0, sizeof(p0));
  check_capsule_hdr(62, p62, sizeof(p62));
  check_capsule_hdr(63, p63, sizeof(p63));
  check_capsule_hdr(64, p64, sizeof(p64));
  check_capsule_hdr(16382, p16382, sizeof(p16382));
  check_capsule_hdr(16383, p16383, sizeof(p16383));
  check_capsule_hdr(16384, p16384, sizeof(p16384));
}

#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */

#if defined(USE_PROXY_HTTP3) && defined(USE_NGTCP2) && \
  !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
static void check_capsule_result(struct bufq *q,
                                 const unsigned char *capsule, size_t capslen,
                                 size_t outlen, CURLcode expect_err,
                                 size_t expect_nread)
{
  unsigned char out[32];
  CURLcode err = CURLE_OK;
  size_t nread;

  memset(out, 0, sizeof(out));
  Curl_bufq_reset(q);
  if(capsule && capslen)
    queue_bytes(q, capsule, capslen);

  nread = Curl_capsule_process_udp_raw(NULL, NULL, q, out, outlen, &err);
  fail_unless(err == expect_err, "unexpected capsule error");
  fail_unless(nread == expect_nread, "unexpected capsule read size");
}

static void test_capsule_encode_decode_roundtrip(void)
{
  struct dynbuf dyn;
  struct bufq q;
  unsigned char payload[128];
  unsigned char out[128];
  CURLcode result, err;
  size_t payload_len;
  size_t i, nread;

  for(i = 0; i < sizeof(payload); ++i)
    payload[i] = (unsigned char)i;

  for(i = 0; i < 2; ++i) {
    payload_len = i ? 64 : 7;
    memset(out, 0, sizeof(out));

    result = Curl_capsule_encap_udp_datagram(&dyn, payload, payload_len);
    fail_unless(result == CURLE_OK, "failed to encapsulate UDP datagram");

    Curl_bufq_init2(&q, 32, 8, BUFQ_OPT_NONE);
    queue_bytes(&q, (const unsigned char *)curlx_dyn_ptr(&dyn),
                curlx_dyn_len(&dyn));

    err = CURLE_OK;
    nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out),
                                         &err);
    fail_unless(err == CURLE_OK, "failed to decode UDP datagram");
    fail_unless(nread == payload_len, "decoded payload length mismatch");
    fail_unless(!memcmp(out, payload, payload_len),
                "decoded payload bytes mismatch");
    fail_unless(Curl_bufq_is_empty(&q), "decoded capsule must be consumed");

    Curl_bufq_free(&q);
    curlx_dyn_free(&dyn);
  }
}

static void test_capsule_sequential_decode(void)
{
  /* Verify that multiple back-to-back capsules in the same bufq are
     each decoded in turn and the buffer is fully consumed. */
  struct bufq q;
  unsigned char out[8];
  CURLcode err;
  size_t nread;
  /* Two back-to-back 3-byte UDP capsules */
  const unsigned char two_caps[] = {
    0x00, 0x04, 0x00, 0x11, 0x22, 0x33,   /* capsule 1: [0x11,0x22,0x33] */
    0x00, 0x04, 0x00, 0xAA, 0xBB, 0xCC    /* capsule 2: [0xAA,0xBB,0xCC] */
  };

  Curl_bufq_init2(&q, 32, 4, BUFQ_OPT_NONE);

  queue_bytes(&q, two_caps, sizeof(two_caps));

  /* First capsule */
  memset(out, 0, sizeof(out));
  err = CURLE_OK;
  nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
  fail_unless(err == CURLE_OK, "sequential: first capsule decode failed");
  fail_unless(nread == 3, "sequential: first capsule size mismatch");
  fail_unless(out[0] == 0x11 && out[1] == 0x22 && out[2] == 0x33,
              "sequential: first capsule bytes mismatch");

  /* Second capsule */
  memset(out, 0, sizeof(out));
  err = CURLE_OK;
  nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
  fail_unless(err == CURLE_OK, "sequential: second capsule decode failed");
  fail_unless(nread == 3, "sequential: second capsule size mismatch");
  fail_unless(out[0] == 0xAA && out[1] == 0xBB && out[2] == 0xCC,
              "sequential: second capsule bytes mismatch");

  /* Buffer must be empty after both capsules */
  fail_unless(Curl_bufq_is_empty(&q),
              "sequential: buffer must be empty after two capsules");

  /* No more data - CURLE_AGAIN expected */
  err = CURLE_OK;
  nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
  fail_unless(err == CURLE_AGAIN,
              "sequential: empty queue should return AGAIN");
  fail_unless(nread == 0, "sequential: empty queue should read zero bytes");

  Curl_bufq_free(&q);
}

static void test_capsule_decode_paths(void)
{
  struct bufq q;
  unsigned char out[8];
  CURLcode err = CURLE_OK;
  size_t nread;
  const unsigned char invalid_type[] = { 0x01 };
  const unsigned char partial_len[] = { 0x00, 0x40 };
  const unsigned char invalid_context[] = { 0x00, 0x01, 0x01 };
  const unsigned char invalid_caps_len[] = { 0x00, 0x00, 0x00 };
  const unsigned char partial_payload[] = { 0x00, 0x04, 0x00, 0x11, 0x22 };
  const unsigned char payload_3b[] = { 0x00, 0x04, 0x00, 0x11, 0x22, 0x33 };
  const unsigned char payload_empty[] = { 0x00, 0x01, 0x00 };

  Curl_bufq_init2(&q, 32, 4, BUFQ_OPT_NONE);

  check_capsule_result(&q, NULL, 0, 0, CURLE_BAD_FUNCTION_ARGUMENT, 0);
  check_capsule_result(&q, NULL, 0, sizeof(out), CURLE_AGAIN, 0);
  check_capsule_result(&q, invalid_type, sizeof(invalid_type), sizeof(out),
                       CURLE_RECV_ERROR, 0);
  check_capsule_result(&q, partial_len, sizeof(partial_len), sizeof(out),
                       CURLE_AGAIN, 0);
  check_capsule_result(&q, invalid_context, sizeof(invalid_context),
                       sizeof(out), CURLE_RECV_ERROR, 0);
  check_capsule_result(&q, invalid_caps_len, sizeof(invalid_caps_len),
                       sizeof(out), CURLE_RECV_ERROR, 0);
  check_capsule_result(&q, partial_payload, sizeof(partial_payload),
                       sizeof(out), CURLE_AGAIN, 0);

  /* oversized payload is rejected and discarded */
  Curl_bufq_reset(&q);
  queue_bytes(&q, payload_3b, sizeof(payload_3b));
  nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, 2, &err);
  fail_unless(err == CURLE_RECV_ERROR,
              "expected RECV_ERROR for short output buffer");
  fail_unless(nread == 0, "expected zero read on short output buffer");
  fail_unless(Curl_bufq_is_empty(&q), "oversized capsule must be discarded");

  /* zero-length UDP payload is accepted and consumed */
  Curl_bufq_reset(&q);
  queue_bytes(&q, payload_empty, sizeof(payload_empty));
  nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
  fail_unless(err == CURLE_OK, "zero-length UDP payload should succeed");
  fail_unless(nread == 0, "zero-length UDP payload should read zero");
  fail_unless(Curl_bufq_is_empty(&q), "zero-length capsule must be consumed");

  /* normal payload decode */
  Curl_bufq_reset(&q);
  queue_bytes(&q, payload_3b, sizeof(payload_3b));
  memset(out, 0, sizeof(out));
  nread = Curl_capsule_process_udp_raw(NULL, NULL, &q, out, sizeof(out), &err);
  fail_unless(err == CURLE_OK, "payload decode should succeed");
  fail_unless(nread == 3, "payload decode size mismatch");
  fail_unless(out[0] == 0x11 && out[1] == 0x22 && out[2] == 0x33,
              "payload decode bytes mismatch");
  fail_unless(Curl_bufq_is_empty(&q), "payload capsule must be consumed");

  Curl_bufq_free(&q);
}
#endif /* USE_NGTCP2 && !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */

static CURLcode test_unit3400(const char *arg)
{
  UNITTEST_BEGIN_SIMPLE

#if defined(USE_PROXY_HTTP3) && \
  !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
  test_capsule_encap_udp_hdr_boundaries();
#endif

#if defined(USE_PROXY_HTTP3) && defined(USE_NGTCP2) && \
  !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
  test_capsule_encode_decode_roundtrip();
  test_capsule_decode_paths();
  test_capsule_sequential_decode();
#endif

  UNITTEST_END_SIMPLE
}
