/* diff.pk - Differences and patching utilities.  */

/* Copyright (C) 2021, 2022, 2023, 2024 Jose E. Marchesi.  */

/* This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/* This pickle contains utilities related to diffing and patching.  */

load "pk-table.pk";

/* The Diff_Thunk type is used to implement utilities related to the
   Structured Binary Diff format (sdiff) as documented in
   https://binary-tools.net/bindiff.pdf.  */

var DIFF_THUNK_ADD = 1;
var DIFF_THUNK_REM = 2;

type Diff_Thunk =
  struct
  {
    type Diff_Thunk_Line =
      struct
      {
        int<32> kind : kind in [DIFF_THUNK_ADD, DIFF_THUNK_REM];
        string data;
        string name;
        string value;
      };

    int<32> emit_values_p;
    offset<int<32>,B> group_by = 1#B;

    var A = 0, B = 1;
    offset<uint<64>,B>[2] offset;
    offset<uint<64>,B>[2] size;
    Diff_Thunk_Line[] lines;

    method reset = (offset<uint<64>,B> aoff = 0#B,
                    offset<uint<64>,B> asiz = 0#B,
                    offset<uint<64>,B> boff = 0#B,
                    offset<uint<64>,B> bsiz = 0#B) void:
    {
      offset[A] = aoff;
      offset[B] = boff;
      size[A] = asiz;
      size[B] = bsiz;
      lines = Diff_Thunk_Line[]();
    }

    method append_line = (int<32> kind,
                          uint<8>[] bytes,
                          string name = "",
                          string value = "") void:
    {
      fun format_bytes = (uint<8>[] bytes) string:
      {
        var x = 1#B;
        var s = "";

        for (b in bytes)
        {
          s += format ("%u8x", b);
          if (x++ % group_by == 0#B)
            s += " ";
        }
        return s;
      }

      lines += [Diff_Thunk_Line { kind = kind,
                                  data = format_bytes (bytes),
                                  name = name,
                                  value = value }];
    }

    method print_thunk = void:
    {
      if (lines'length == 0)
        return;

      var a_region
        = size[A] > 0#B ? format ("0x%u8x+%u64d", offset[A]/#B, size[A]/#B) : "";
      var b_region
        = size[B] > 0#B ? format ("0x%u8x+%u64d", offset[B]/#B, size[B]/#B) : "";

      term_begin_class ("diff-thunk-header");
      printf ("@@ %s,%s @@\n", a_region, b_region);
      term_end_class ("diff-thunk-header");
      var table = Pk_Table { num_columns = emit_values_p ? 3 : 2 };
      for (line in lines)
      {
        table.row (line.kind == DIFF_THUNK_ADD ? "diff-plus" : "diff-minus");
        table.column ((line.kind == DIFF_THUNK_ADD ? "+" : "-") + line.data);
        table.column (line.name);
        if (emit_values_p)
          table.column (line.value);
      }
      table.print_table;
    }

    method change = (offset<uint<64>,B> aoff, offset<uint<64>,B> asiz,
                     offset<uint<64>,B> boff, offset<uint<64>,B> bsiz,
                     uint<8>[] abytes, uint<8>[] bbytes,
                     string aname = "", string bname = "",
                     string avalue = "", string bvalue = "") void:
    {
      if (aoff == offset[A] + size[A]
          && boff == offset[B] + size[B])
      {
        size[A] += asiz;
        size[B] += bsiz;
      }
      else
      {
        print_thunk;
        reset (aoff, asiz, boff, bsiz);
      }

      append_line (DIFF_THUNK_REM, abytes, aname, avalue);
      append_line (DIFF_THUNK_ADD, bbytes, bname, bvalue);
    }

    method addrem = (int<32> what,
                     offset<uint<64>,B> off, offset<uint<64>,B> siz,
                     uint<8>[] bytes, string name = "", string value = "") void:
    {
      var to = what == DIFF_THUNK_ADD ? B : A;

      if (off == offset[to] + size[to])
        size[to] += siz;
      else
      {
        print_thunk;
        if (what == DIFF_THUNK_ADD)
          reset :boff off :bsiz siz;
        else
          reset :aoff off :asiz siz;
      }

      append_line (what, bytes, name, value);
    }
  };

/* Print a structured binary diff expressing the differences of two
   given mapped values.  Note that for the output to make sense both
   values must be of the same type.

   A is the first mapped value whose bytes are compared.

   B is the second mapped value whose bytes are compared.

   VALUES is a boolean that determines whether to include interpreted
   values in the output.  Defaults to 1.

   GROUP_BY is an offset that determines how many bytes are grouped
   together in the output.  Defaults to one byte.

   This function raises E_map if any of the values to compare are not
   mapped.

   This function raises E_constraint if the size of either A or B is
   not a multiple of bytes.  Note however that the constituent data
   structures are not required to be a whole number of bytes.  */

fun diff_structured = (any a, any b,
                       string prefix_a = "a", string prefix_b = "b",
                       int<32> values = 1, offset<int<32>,B> group_by = 1#B) void:
{
  var thunk = Diff_Thunk { emit_values_p = values,
                           group_by = group_by };

  fun format_ename = (string ename) string:
  {
    if (ename'length > 0 && ename[0] == '[')
      return ename;
    else
      return "." + ename;
  }

  fun get_bytes = (any v) uint<8>[]:
  {
    try return uint<8>[v'size] @ v'ios : v'offset;
    catch if E_constraint { raise E_inval; }
  }

  fun get_elem_bytes = (any v, uint<64> idx) uint<8>[]:
  {
    try return uint<8>[v'esize (idx)] @ v'ios : v'eoffset (idx);
    catch if E_constraint { raise E_inval; }
  }

  fun elem_simple_p = (any elem, uint<64> idx) int<32>:
  {
    /* To see if IDX-th element is "simple", we try to see if it is
       mapped or not.  If it's not mapped, then it's "simple".  */
    try { return !elem'elem (idx)'mapped; }
    catch (Exception e)
    {
      if (e.code == EC_out_of_bounds)
        return 0;
      if (e.code != EC_inval)
       raise e;
    }
    return 1;
  }

  fun sdiff_addrem = (int<32> what, any val,
                      string prefix, uint<64> idx) void:
  {
    for (; idx < val'length; ++idx)
    {
      if (!(val'elem (idx) ?! E_inval))
      {
        /* Note that recursing into the element may result in E_inval
           in case a non-byte element is found.  In that case, try to
           emit the thunk anyway.  */
        if (elem_simple_p (val, idx)
            || { sdiff_addrem (what, val'elem (idx),
                               prefix + format_ename (val'ename (idx)),
                               0); } ?! E_inval)
          thunk.addrem :what what :off val'eoffset (idx) :siz val'esize (idx)
                       :bytes get_elem_bytes (val, idx)
                       :name prefix + format_ename (val'ename (idx))
                       :value format ("%v", val'elem (idx));
      }
    }
  }

  fun sdiff_change = (any a, any b,
                      string prefix_a, string prefix_b) void:
  {
    /* Proceed element by element.  */
    var idx = 0UL;
    for (; idx < a'length; ++idx)
    {
      var a_is_present = !(a'elem (idx) ?! E_inval);

      if (idx >= b'length)
      {
        sdiff_addrem (DIFF_THUNK_REM, a, prefix_a, idx);
        break;
      }
      else
      {
        var b_is_present = !(b'elem (idx) ?! E_inval);

        if (a_is_present && b_is_present)
        {
          var a_full_name = prefix_a + format_ename (a'ename (idx));
          var b_full_name = prefix_b + format_ename (b'ename (idx));

          /* If the element is simple, add a change to the current
             thunk; otherwise recurse to handle its elements.  Note that
             recursing into the element may result in E_inval in case a
             non-byte element is found.  In that case, try to emit the
             thunk anyway.  */
          if (elem_simple_p (a, idx)
              || { sdiff_change (a'elem (idx), b'elem (idx),
                                 a_full_name, b_full_name); } ?! E_inval)
          {
            var a_bytes = get_elem_bytes (a, idx);
            var b_bytes = get_elem_bytes (b, idx);

            if (a_bytes != b_bytes)
              thunk.change :aoff a'eoffset (idx) :asiz a'esize (idx)
                           :boff b'eoffset (idx) :bsiz b'esize (idx)
                           :abytes a_bytes :bbytes b_bytes
                           :aname a_full_name :bname b_full_name
                           :avalue format ("%v", a'elem (idx))
                           :bvalue format ("%v", b'elem (idx));
          }
        }
        else if (b_is_present)
          sdiff_addrem (DIFF_THUNK_ADD, b'elem (idx), prefix_b, 0);
        else if (a_is_present)
          sdiff_addrem (DIFF_THUNK_REM, a'elem (idx), prefix_a, 0);
      }
    }

    /* Now generate ADD thunks for extra elements in `b'.  */
    if (idx < b'length)
      sdiff_addrem (DIFF_THUNK_ADD, b, prefix_b, idx);
  }

  /* First of all, make sure both values are mapped.  */
  if (!a'mapped || !b'mapped)
    raise E_map;

  try
  {
    if ({ sdiff_change (a, b, prefix_a, prefix_b); } ?! E_inval)
    {
      var a_bytes = get_bytes (a);
      var b_bytes = get_bytes (b);

      if (a_bytes != b_bytes)
        thunk.change :aoff a'offset :asiz a'size
                     :boff b'offset :bsiz b'size
                     :abytes a_bytes :bbytes b_bytes
                     :aname prefix_a :bname prefix_b
                     :avalue format ("%v", a)
                     :bvalue format ("%v", b);
    }
  }
  catch (Exception e)
  {
    if (e.code == EC_inval)
      e.msg = "size of compared values is not a multiple of bytes";
    raise e;
  }

  /* There may be a thunk still to be emitted.  */
  thunk.print_thunk;
}
