open Common

let check = Alcotest.(check @@ result (list options) string)

let errors ?(check_msg = false) exp = function
  | Ok opt -> failf "Ok %a when Error %s expected" Tcp.Options.pps opt exp
  | Error p -> if check_msg then
      Alcotest.(check string)
        "Error didn't give the expected error message" exp p
    else ()

let test_unmarshal_bad_mss () =
  let odd_sized_mss = Cstruct.create 3 in
  Cstruct.set_uint8 odd_sized_mss 0 2;
  Cstruct.set_uint8 odd_sized_mss 1 3;
  Cstruct.set_uint8 odd_sized_mss 2 255;
  errors "MSS size is unreasonable" (Tcp.Options.unmarshal odd_sized_mss)

let test_unmarshal_bogus_length () =
  let bogus = Cstruct.create (4*8-1) in
  Cstruct.memset bogus 0;
  Cstruct.blit_from_string "\x6e\x73\x73\x68\x2e\x63\x6f\x6d" 0 bogus 0 8;
  (* some unknown option (0x6e) with claimed length 0x73, longer than
     the buffer. This invalidates later results, but previous ones are
     still valid, if any *)
  check "length" (Ok []) (Tcp.Options.unmarshal bogus)

let test_unmarshal_zero_length () =
  let bogus = Cstruct.create 10 in
  Cstruct.memset bogus 1; (* noops *)
  Cstruct.set_uint8 bogus 0 64; (* arbitrary unknown option-kind *)
  Cstruct.set_uint8 bogus 1 0;
  (* this invalidates later results, but previous ones are still
     valid, if any *)
  check "zero" (Ok []) (Tcp.Options.unmarshal bogus)

let test_unmarshal_simple_options () =
  (* empty buffer should give empty list *)
  check "simple" (Ok []) (Tcp.Options.unmarshal (Cstruct.create 0));

  (* buffer with just eof should give empty list *)
  let just_eof = Cstruct.create 1 in
  Cstruct.set_uint8 just_eof 0 0;
  check "eof" (Ok []) (Tcp.Options.unmarshal just_eof);

  (* buffer with single noop should give a list with 1 noop *)
  let just_noop = Cstruct.create 1 in
  Cstruct.set_uint8 just_noop 0 1;
  check "noop" (Ok [ Tcp.Options.Noop ]) (Tcp.Options.unmarshal just_noop);

  (* buffer with valid, but unknown, option should be correctly communicated *)
  let unknown = Cstruct.create 10 in
  let data = "hi mom!!" in
  let kind = 18 in (* TODO: more canonically unknown option-kind *)
  Cstruct.blit_from_string data 0 unknown 2 (String.length data);
  Cstruct.set_uint8 unknown 0 kind;
  Cstruct.set_uint8 unknown 1 (Cstruct.length unknown);
  check "more"
    (Ok [Tcp.Options.Unknown (kind, data)])
    (Tcp.Options.unmarshal unknown)

let test_unmarshal_stops_at_eof () =
  let buf = Cstruct.create 14 in
  let ts1 = 0xabad1deal in
  let ts2 = 0xc0ffee33l in
  Cstruct.memset buf 0;
  Cstruct.set_uint8 buf 0 4; (* sack_ok *)
  Cstruct.set_uint8 buf 1 2; (* length of two *)
  Cstruct.set_uint8 buf 2 1; (* noop *)
  Cstruct.set_uint8 buf 3 0; (* eof *)
  Cstruct.set_uint8 buf 4 8; (* timestamp *)
  Cstruct.set_uint8 buf 5 10; (* timestamps are 2 4-byte times *)
  Cstruct.BE.set_uint32 buf 6 ts1;
  Cstruct.BE.set_uint32 buf 10 ts2;
  (* correct parsing will ignore options from after eof, so we shouldn't see
     timestamp or noop *)
  match Tcp.Options.unmarshal buf with
  | Error s -> Alcotest.fail s
  | Ok result ->
    Alcotest.(check bool) "SACK_ok missing"
      true (List.mem Tcp.Options.SACK_ok result);
    Alcotest.(check bool) "noop missing"
      true (List.mem Tcp.Options.Noop result);
    Alcotest.(check bool) "timestamp present"
      false (List.mem (Tcp.Options.Timestamp (ts1, ts2)) result)

let test_unmarshal_ok_options () =
  let buf = Cstruct.create 8 in
  Cstruct.memset buf 0;
  let opts = [ Tcp.Options.MSS 536; Tcp.Options.SACK_ok; Tcp.Options.Noop;
               Tcp.Options.Noop ] in
  let marshalled = Tcp.Options.marshal buf opts in
  Alcotest.(check int) "marshalled" marshalled 8;
  (* order is reversed by the unmarshaller, which is fine but we need to
     account for that when making equality assertions *)
  match Tcp.Options.unmarshal buf with
  | Error s -> Alcotest.fail s
  | Ok l    -> Alcotest.(check @@ list options) "l" l opts

let test_unmarshal_random_data () =
  let random = Cstruct.create 64 in
  let iterations = 100 in
  Random.self_init ();
  let set_random pos =
    let num = Random.int32 Int32.max_int in
    Cstruct.BE.set_uint32 random pos num;
  in
  let rec check = function
    | n when n <= 0 -> ()
    | n ->
      List.iter set_random [0;4;8;12;16;20;24;28;32;36;40;44;48;52;56;60];
      Cstruct.hexdump random;
      (* acceptable outcomes: some list of options or the expected exception *)
      match Tcp.Options.unmarshal random with
      | Error _ -> (* Errors are OK, just finish *) ()
      | Ok l ->
        Tcp.Options.pps Format.std_formatter l;
        (* a really basic truth: the longest list we can have is 64 noops *)
        Alcotest.(check bool) "random" true (List.length l < 65);
        check (n - 1)
  in
  check iterations

let test_marshal_unknown () =
  let buf = Cstruct.create 10 in
  Cstruct.memset buf 255;
  let unknown = [ Tcp.Options.Unknown (64, "  ") ] in (* overall, length 4 *)
  Alcotest.(check int) "4 bytes"
    4 (Tcp.Options.marshal buf unknown); (* should have written 4 bytes *)
  Cstruct.hexdump buf;
  (* option-kind *)
  Alcotest.(check int) "option kind" 64 (Cstruct.get_uint8 buf 0);
  (* option-length *)
  Alcotest.(check int)"option length" 4 (Cstruct.get_uint8 buf 1);
  (* data *)
  Alcotest.(check int) "data 1" 0x20 (Cstruct.get_uint8 buf 2);
  (* moar data *)
  Alcotest.(check int) "data 2" 0x20 (Cstruct.get_uint8 buf 3);
   (* unwritten region *)
  Alcotest.(check int) "canary" 255 (Cstruct.get_uint8 buf 4)

let test_options_marshal_padding () =
  let buf = Cstruct.create 8 in
  Cstruct.memset buf 255;
  let extract = Cstruct.get_uint8 buf in
  let needs_padding = [ Tcp.Options.SACK_ok ] in
  Alcotest.(check int) "padding"   4 (Tcp.Options.marshal buf needs_padding);
  Alcotest.(check int) "extract 0" 4 (extract 0);
  Alcotest.(check int) "extract 1" 2 (extract 1);
  (* should pad out the rest of the buffer with 0 *)
  Alcotest.(check int) "extract 2" 0 (extract 2);
  Alcotest.(check int) "extract 3" 0 (extract 3);
  (* but not keep padding into random memory *)
  Alcotest.(check int) "extract 4" 255 (extract 4)

let test_marshal_empty () =
  let buf = Cstruct.create 4 in
  Cstruct.memset buf 255;
  Alcotest.(check int) "0"   0 (Tcp.Options.marshal buf []);
  Alcotest.(check int) "255" 255 (Cstruct.get_uint8 buf 0)

let test_marshal_into_cstruct () =
  let options = [
    Tcp.Options.MSS 1460;
    Tcp.Options.SACK_ok;
    Tcp.Options.Window_size_shift 2
  ] in
  (* MSS is 4 bytes, SACK_OK is 4 bytes, window_size_shift is 3, plus
     1 for padding *)
  let options_size = 12 in
  let buf = Cstruct.create (Tcp.Tcp_wire.sizeof_tcp + options_size) in
  Cstruct.memset buf 255;
  let src = Ipaddr.V4.of_string_exn "127.0.0.1" in
  let dst = Ipaddr.V4.of_string_exn "127.0.0.1" in
  let ipv4_header =
    {Ipv4_packet.src; dst; proto = 6; ttl = 64; id = 0 ; off = 0 ; options = Cstruct.create 0}
  in
  let payload = Cstruct.of_string "ab" in
  let pseudoheader =
    Ipv4_packet.Marshal.pseudoheader ~src ~dst ~proto:`TCP
      (Tcp.Tcp_wire.sizeof_tcp + options_size + Cstruct.length payload)
  in
  let packet =
    Tcp.Tcp_packet.{
      urg = false;
      ack = true;
      psh = false;
      rst = false;
      syn = true;
      fin = false;
      window = 0;
      options;
      sequence = Tcp.Sequence.of_int 255;
      ack_number = Tcp.Sequence.of_int 1024;
      src_port = 3000;
      dst_port = 6667;
    }
  in
  Tcp.Tcp_packet.Marshal.into_cstruct ~pseudoheader ~payload packet buf
  |> Alcotest.(check (result int string)) "correct size written"
    (Ok (Cstruct.length buf));
  let raw =Cstruct.concat [buf; payload]  in
  Ipv4_packet.Unmarshal.verify_transport_checksum ~proto:`TCP ~ipv4_header
    ~transport_packet:raw
  |> Alcotest.(check bool) "Checksum correct" true;
  Tcp.Tcp_packet.Unmarshal.of_cstruct raw
  |> Alcotest.(check (result (pair tcp_packet cstruct) string))
    "reload TCP packet" (Ok (packet, payload));
  let just_options = Cstruct.create options_size in
  let generated_options = Cstruct.shift buf Tcp.Tcp_wire.sizeof_tcp in
  Alcotest.(check int) "size of options buf" options_size @@
  Tcp.Options.marshal just_options options;
  (* expecting the result of Options.Marshal to be here *)
  Alcotest.check cstruct "marshalled options are as expected"
    just_options generated_options;
  (* Now try with make_cstruct *)
  let headers =
    Tcp.Tcp_packet.Marshal.make_cstruct ~pseudoheader ~payload packet
  in
  let raw =Cstruct.concat [headers; payload]  in
  Ipv4_packet.Unmarshal.verify_transport_checksum ~proto:`TCP ~ipv4_header
    ~transport_packet:raw
  |> Alcotest.(check bool) "Checksum correct" true

let test_marshal_without_padding () =
  let options = [ Tcp.Options.MSS 1460 ] in
  let options_size = 4 in (* MSS is 4 bytes *)
  let buf = Cstruct.create (Tcp.Tcp_wire.sizeof_tcp + options_size) in
  Cstruct.memset buf 255;
  let src = Ipaddr.V4.of_string_exn "127.0.0.1" in
  let dst = Ipaddr.V4.of_string_exn "127.0.0.1" in
  let ipv4_header =
    {Ipv4_packet.src; dst; proto = 6; ttl = 64; id = 0 ; off = 0 ; options = Cstruct.create 0}
  in
  let payload = Cstruct.of_string "\x02\x04\x05\xb4" in
  let pseudoheader =
    Ipv4_packet.Marshal.pseudoheader ~src ~dst ~proto:`TCP
      (Tcp.Tcp_wire.sizeof_tcp + options_size + Cstruct.length payload)
  in
  let packet =
    Tcp.Tcp_packet.{
      urg = false;
      ack = true;
      psh = false;
      rst = false;
      syn = true;
      fin = false;
      window = 0;
      options;
      sequence = Tcp.Sequence.of_int 255;
      ack_number = Tcp.Sequence.of_int 1024;
      src_port = 3000;
      dst_port = 6667;
    }
  in
  Tcp.Tcp_packet.Marshal.into_cstruct ~pseudoheader ~payload packet buf
  |> Alcotest.(check (result int string)) "correct size written"
    (Ok (Cstruct.length buf));
  let raw =Cstruct.concat [buf; payload]  in
  Ipv4_packet.Unmarshal.verify_transport_checksum ~proto:`TCP ~ipv4_header
    ~transport_packet:raw
  |> Alcotest.(check bool) "Checksum correct" true;
  Tcp.Tcp_packet.Unmarshal.of_cstruct raw
  |> Alcotest.(check (result (pair tcp_packet cstruct) string))
    "reload TCP packet" (Ok (packet, payload))

let suite = [
  "unmarshal broken mss", `Quick, test_unmarshal_bad_mss;
  "unmarshal option with bogus length", `Quick, test_unmarshal_bogus_length;
  "unmarshal option with zero length", `Quick, test_unmarshal_zero_length;
  "unmarshal simple cases", `Quick, test_unmarshal_simple_options;
  "unmarshal stops at eof", `Quick, test_unmarshal_stops_at_eof;
  "unmarshal non-broken tcp options", `Quick, test_unmarshal_ok_options;
  "unmarshalling random data returns", `Quick, test_unmarshal_random_data;
  "test marshalling into a cstruct", `Quick, test_marshal_into_cstruct;
  "test marshalling without padding", `Quick, test_marshal_without_padding;
  "test marshalling an unknown value", `Quick, test_marshal_unknown;
  "test options marshalling when padding is needed", `Quick,
  test_options_marshal_padding;
  "test marshalling the empty list", `Quick, test_marshal_empty;
]

let suite =
  List.map (fun (n, s, f) -> n, s, (fun () -> Lwt.return (f ()))) suite
