open Common

let test_unmarshal_with_options () =
  let datagram = Cstruct.create 40 in
  Cstruct.blit_from_string ("\x46\xc0\x00\x28\x00\x00\x40\x00\x01\x02" ^
                            "\x42\x49\xc0\xa8\x01\x08\xe0\x00\x00\x16\x94\x04\x00\x00\x22" ^
                            "\x00\xfa\x02\x00\x00\x00\x01\x03\x00\x00\x00\xe0\x00\x00\xfb") 0 datagram 0 40;
  match Ipv4_packet.Unmarshal.of_cstruct datagram with
  | Ok ({Ipv4_packet.options ; _}, payload) ->
      Alcotest.(check int) "options" (Cstruct.length options) 4;
      Alcotest.(check int) "payload" (Cstruct.length payload) 16;
      Lwt.return_unit
  | _ ->
      Alcotest.fail "Fail to parse ip packet with options"


let test_unmarshal_without_options () =
  let datagram = Cstruct.create 40 in
  Cstruct.blit_from_string ("\x45\x00\x00\x28\x19\x29\x40\x00\x34\x06\x98\x75\x36\xb7" ^
                            "\x9c\xca\xc0\xa8\x01\x08\x00\x50\xca\xa6\x6f\x19\xf4\x76" ^
                            "\x00\x00\x00\x00\x50\x04\x00\x00\xec\x27\x00\x00") 0 datagram 0 40;
  match Ipv4_packet.Unmarshal.of_cstruct datagram with
  | Ok ({Ipv4_packet.options ; _}, payload) ->
      Alcotest.(check int) "options" (Cstruct.length options) 0;
      Alcotest.(check int) "payload" (Cstruct.length payload) 20;
      Lwt.return_unit
  | _ ->
      Alcotest.fail "Fail to parse ip packet with options"

let test_unmarshal_regression () =
  let p = Cstruct.of_string "\x49\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" in
  Alcotest.(check (result reject pass))
    "correctly return error for bad packet"
    (Error "any") (Ipv4_packet.Unmarshal.of_cstruct p);
  Lwt.return_unit

let test_size () =
  let src = Ipaddr.V4.of_string_exn "127.0.0.1" in
  let dst = Ipaddr.V4.of_string_exn "127.0.0.2" in
  let ttl = 64 in
  let ip = { Ipv4_packet.src; dst; proto = 17; ttl; id = 0 ; off = 0 ; options = (Cstruct.of_string "aaaa") } in
  let payload = Cstruct.of_string "abcdefgh" in
  let tmp = Ipv4_packet.Marshal.make_cstruct ~payload_len:(Cstruct.length payload) ip in
  let tmp = Cstruct.concat [tmp; payload] in
  Ipv4_packet.Unmarshal.of_cstruct tmp
  |> Alcotest.(check (result (pair ipv4_packet cstruct) string)) "Loading an IP packet with IP options" (Ok (ip, payload));
  Lwt.return_unit

let test_packet =
  let src = Ipaddr.V4.of_string_exn "127.0.0.1" in
  let dst = Ipaddr.V4.of_string_exn "127.0.0.2" in
  let ttl = 64 in
  { Ipv4_packet.src; dst; proto = 17; ttl; id = 0 ; off = 0 ; options = (Cstruct.of_string "aaaa") }

let mf = 0x2000

let white = Cstruct.create 16
let black =
  let buf = Cstruct.create 16 in
  Cstruct.memset buf 0xFF ;
  buf
let gray =
  let buf = Cstruct.create 16 in
  Cstruct.memset buf 0x55 ;
  buf

let empty_cache = Fragments.Cache.empty 1000

let basic_fragments payload () =
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__
              (Some (test_packet, payload))
              (snd @@ Fragments.process empty_cache 0L test_packet payload)) ;
  let off_packet = { test_packet with off = 1 } in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__
              None
              (snd @@ Fragments.process empty_cache 0L off_packet payload)) ;
  Lwt.return_unit

let basic_reassembly () =
  let more_frags = { test_packet with off = mf } in
  let cache, res = Fragments.process empty_cache 0L more_frags black in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  let off_packet = { test_packet with off = 2 } in
  Alcotest.(check (option (pair ipv4_packet cstruct)) "reassembly of two segments works"
              (Some (test_packet, Cstruct.append black white))
              (snd @@ Fragments.process cache 0L off_packet white)) ;
  Lwt.return_unit

let basic_reassembly_timeout () =
  let more_frags = { test_packet with off = mf } in
  let cache, res = Fragments.process empty_cache 0L more_frags black in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  let off_packet = { test_packet with off = 2 } in
  let below_max = Int64.sub Fragments.max_duration 1L in
  Alcotest.(check (option (pair ipv4_packet cstruct)) "even after just before max duration"
              (Some (test_packet, Cstruct.append black white))
              (snd @@ Fragments.process cache below_max off_packet white)) ;
  Alcotest.(check (option (pair ipv4_packet cstruct)) "none after max duration"
              None
              (snd @@ Fragments.process cache Fragments.max_duration off_packet white)) ;
  let more_off_packet = { test_packet with off = mf lor 2 } in
  let cache, res = Fragments.process cache below_max more_off_packet gray in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  let final_packet = { test_packet with off = 4 } in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__
              (Some (test_packet, Cstruct.concat [ black; gray; white]))
              (snd @@ Fragments.process cache below_max final_packet white)) ;
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__
              None
              (snd @@ Fragments.process cache Fragments.max_duration off_packet white)) ;
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  Lwt.return_unit

let reassembly_out_of_order () =
  let more_frags = { test_packet with off = mf } in
  let off_packet = { test_packet with off = 2 } in
  let cache, res = Fragments.process empty_cache 0L off_packet gray in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  Alcotest.(check (option (pair ipv4_packet cstruct)) "reassembly of two segments works"
              (Some (test_packet, Cstruct.append black gray))
              (snd @@ Fragments.process cache 0L more_frags black)) ;
  Lwt.return_unit

let reassembly_multiple_out_of_order packets final_payload () =
  let _, res = List.fold_left (fun (cache, res) (off, payload) ->
      Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
      let packet = { test_packet with off } in
      Fragments.process cache 0L packet payload)
      (empty_cache, None) packets
  in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__
              (Some (test_packet, final_payload))
              res) ;
  Lwt.return_unit

let basic_overlaps () =
  let more_frags = { test_packet with off = mf } in
  let off_packet = { test_packet with off = 1 } in
  let cache, res = Fragments.process empty_cache 0L off_packet black in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None
              (snd @@ Fragments.process cache 0L more_frags white)) ;
  Lwt.return_unit

let basic_other_ip_flow () =
  let more_frags = { test_packet with off = mf } in
  let cache, res = Fragments.process empty_cache 0L more_frags black in
  let off_packet = { test_packet with off = 2 ; src = Ipaddr.V4.of_string_exn "127.0.0.2" } in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None
              (snd @@ Fragments.process cache 0L off_packet white)) ;
  let off_packet' = { test_packet with off = 2 ; proto = 25 } in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None
              (snd @@ Fragments.process cache 0L off_packet' white)) ;
  Lwt.return_unit

let max_fragment () =
  let all_16 = [ white; gray; black; white;
                 white; gray; black; white;
                 white; gray; black; white;
                 white; gray; black ; gray ]
  in
  let (cache, res), off =
    List.fold_left (fun ((cache, res), off) payload ->
        Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
        let r = Fragments.process cache 0L { test_packet with off = off lor mf } payload in
        (r, Cstruct.length payload / 8 + off))
      ((empty_cache, None), 0)
      all_16
  in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__
              (Some (test_packet, Cstruct.concat (all_16 @ [white ])))
              (snd @@ Fragments.process cache 0L { test_packet with off } white)) ;
  let cache, res = Fragments.process cache 0L { test_packet with off = off lor mf } white in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__
              None
              (snd @@ Fragments.process cache 0L { test_packet with off = off + 2 } black)) ;
  Lwt.return_unit

let none_returned packets () =
  let _, res = List.fold_left (fun (cache, res) (off, payload) ->
      Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
      let packet = { test_packet with off } in
      Fragments.process cache 0L packet payload)
      (empty_cache, None) packets
  in
  Alcotest.(check (option (pair ipv4_packet cstruct)) __LOC__ None res) ;
  Lwt.return_unit

let ins_all_positions x l =
  let rec aux prev acc = function
    | [] -> List.rev ((prev @ [x]) :: acc)
    | hd::tl as l -> aux (prev @ [hd]) ((prev @ [x] @ l) :: acc) tl
  in
  aux [] [] l

let rec permutations = function
  | [] -> []
  | [x] -> [[x]]
  | x::xs -> List.fold_left (fun acc p -> acc @ ins_all_positions x p ) []
               (permutations xs)

let fragment_simple () =
  let hdr =
    { Ipv4_packet.src = Ipaddr.V4.localhost ; dst = Ipaddr.V4.localhost ;
      id = 0x42 ; off = 0 ; ttl = 10 ; proto = 10 ; options = Cstruct.empty }
  in
  let payload = Cstruct.create 1030 in
  let fs = Fragments.fragment ~mtu:36 hdr payload in
  (* 16 byte per packet -> 64 fragments (a 16 byte) + 1 (6 byte) *)
  Alcotest.(check int __LOC__ 65 (List.length fs));
  let second, last = List.hd fs, List.(hd (rev fs)) in
  Alcotest.(check int __LOC__ 26 (Cstruct.length last));
  match
    Ipv4_packet.Unmarshal.of_cstruct second,
    Ipv4_packet.Unmarshal.of_cstruct last
  with
  | Error e, _ -> Alcotest.fail ("failed to decode second fragment " ^ e)
  | _, Error e -> Alcotest.fail ("failed to decode last fragment " ^ e)
  | Ok (hdr, _payload), Ok (hdr', _payload') ->
    Alcotest.(check int __LOC__ (0x2000 lor 2) hdr.Ipv4_packet.off);
    Alcotest.(check int __LOC__ 0x42 hdr.Ipv4_packet.id);
    Alcotest.(check int __LOC__ 130 hdr'.Ipv4_packet.off);
    Alcotest.(check int __LOC__ 0x42 hdr'.Ipv4_packet.id);
    let fs' = Fragments.fragment ~mtu:36 hdr (Cstruct.sub payload 0 1024) in
    (* 16 byte per packet -> 64 fragments (a 16 byte) *)
    Alcotest.(check int __LOC__ 64 (List.length fs'));
    let second', last' = List.hd fs', List.(hd (rev fs')) in
    Alcotest.(check int __LOC__ 36 (Cstruct.length last'));
    match
      Ipv4_packet.Unmarshal.of_cstruct second',
      Ipv4_packet.Unmarshal.of_cstruct last'
    with
    | Error e, _ -> Alcotest.fail ("failed to decode second fragment' " ^ e)
    | _, Error e -> Alcotest.fail ("failed to decode last fragment' " ^ e)
    | Ok (hdr'', _payload''), Ok (hdr''', _payload''') ->
      Alcotest.(check int __LOC__ (0x2000 lor 2) hdr''.Ipv4_packet.off);
      Alcotest.(check int __LOC__ 0x42 hdr''.Ipv4_packet.id);
      Alcotest.(check int __LOC__ 128 hdr'''.Ipv4_packet.off);
      Alcotest.(check int __LOC__ 0x42 hdr'''.Ipv4_packet.id)

let suite = [
  "unmarshal ip datagram with options", `Quick, test_unmarshal_with_options;
  "unmarshal ip datagram without options", `Quick, test_unmarshal_without_options;
  "unmarshal ip datagram with no payload & hlen > 5", `Quick, test_unmarshal_regression;
  "size", `Quick, test_size ] @
  List.mapi (fun i size ->
      Printf.sprintf "basic fragment %d: payload %d" i size, `Quick, basic_fragments (Cstruct.create size))
    [ 0 ; 1 ; 2 ; 10 ; 100 ; 1000 ; 5000 ; 10000 ] @ [
    "basic reassembly", `Quick, basic_reassembly;
    "basic reassembly timeout", `Quick, basic_reassembly_timeout;
    "reassembly out of order", `Quick, reassembly_out_of_order ;
    "other ip flow", `Quick, basic_other_ip_flow ;
    "maximum amount of fragments", `Quick, max_fragment ] @
    List.mapi (fun i (packets, final) ->
      Printf.sprintf "reassembly multiple %d" i, `Quick,
      reassembly_multiple_out_of_order packets final)
    ([
      ([ (mf, white); (2, black) ], Cstruct.concat [white;black]);
      ([ (mf, black); (2, white) ], Cstruct.concat [black;white]);
      ([ (2, black); (mf, white) ], Cstruct.concat [white;black]);
      ([ (2, white); (mf, black) ], Cstruct.concat [black;white]);
      ([ (mf, Cstruct.create 984); (123, black)], Cstruct.concat [Cstruct.create 984;black]);
      ([ (mf, Cstruct.create 984); (123 lor mf, black); (125, gray)],
       Cstruct.concat [Cstruct.create 984;black;gray]);
      ([ (mf, Cstruct.create 1000); (125, (Cstruct.concat [black;black;black]))],
       Cstruct.concat [Cstruct.create 1000;black;black;black]);
    ]@
      List.map (fun x -> (x, Cstruct.concat [gray;white;black]))
        (permutations [ (mf, gray); (2 lor mf, white); (4, black)]) @
      List.map (fun x -> (x, Cstruct.concat [gray;white;black;Cstruct.create 10]))
        (permutations [ (mf, gray); (2 lor mf, white); (4 lor mf, black); (6, Cstruct.create 10)]) @
      List.map (fun x -> (x, Cstruct.concat [black;gray;white;black;gray]))
        (permutations [ (mf, black); (2 lor mf, gray); (4 lor mf, white); (6 lor mf, black); (8, gray)])
    ) @
  [ "nothing returned", `Quick, basic_overlaps ] @
  List.mapi (fun i packets ->
      Printf.sprintf "nothing returned %d" i, `Quick,
      none_returned packets)
    ([
      [ (mf, white); (1, black) ];
      [ (mf, black); (3, white) ];
      [ (mf, Cstruct.create 992); (124 lor mf, black);(126, gray)];
      [ (mf, Cstruct.create 1024); (128, black)];
    ] @
      permutations [ (mf, gray); (2 lor mf, white); (3, black)] @
      permutations [ (mf, gray); (2 lor mf, white); (5, black)] @
      permutations [ (mf, gray); (3 lor mf, white); (4, black)] @
      permutations [ (mf, gray); (3 lor mf, white); (5, black)] @
      permutations [ (mf, gray); (1 lor mf, white); (3, black)] @
      permutations [ (mf, gray); (1 lor mf, white); (4, black)] @
      permutations [ (mf, (Cstruct.append gray gray)); (3 lor mf, white)] @
      permutations [ (mf, (Cstruct.append gray gray)); (2 lor mf, white)] @
      permutations [ (mf, gray); (2 lor mf, white); (4 lor mf, black); (6 lor mf, gray)] @
      permutations [ (mf, gray); (2 lor mf, white); (4 lor mf, black); (5, gray)] @
      permutations [ (mf, gray); (4 lor mf, white); (4 lor mf, black); (6, gray)] @
      permutations [ (mf, gray); (1 lor mf, white); (3 lor mf, black); (5, gray)] @
      permutations [ (mf, gray); (2 lor mf, white); (4 lor mf, black); (7, gray)]
    ) @ [
    "simple fragment", `Quick, (fun () -> Lwt.return (fragment_simple ()))
  ]
