lib/baobab/entry.ex

defmodule Baobab.Entry do
  alias Baobab.Entry.Validator
  alias Baobab.{Identity, Persistence}

  @moduledoc """
  A struct representing a Baobab entry
  """
  defstruct tag: <<0>>,
            author:
              <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                0, 0, 0, 0, 0>>,
            log_id: 0,
            seqnum: 0,
            lipmaalink: nil,
            backlink: nil,
            size: 0,
            payload_hash:
              <<0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>,
            sig:
              <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                0, 0, 0, 0, 0, 0, 0, 0>>,
            payload: ""

  @doc false
  def create(payload, clump_id, identity, log_id) do
    author = Identity.key(identity, :public)
    signer = Identity.key(identity, :signing)
    prev = Baobab.max_seqnum(author, log_id: log_id, clump_id: clump_id)
    seq = prev + 1
    head = <<0>> <> author <> Varu64.encode(log_id) <> Varu64.encode(seq)

    ll =
      case Lipmaa.linkseq(seq) do
        ^prev -> <<>>
        n -> Persistence.content(:entry, :hash, {author, log_id, n}, clump_id)
      end

    bl =
      case prev do
        0 -> <<>>
        n -> Persistence.content(:entry, :hash, {author, log_id, n}, clump_id)
      end

    tail = Varu64.encode(byte_size(payload)) <> YAMFhash.create(payload, 0)

    meat = head <> ll <> bl <> tail
    sig = :enacl.sign_detached(meat, signer)
    entry = meat <> sig

    Persistence.content(:both, :write, {author, log_id, seq}, clump_id, {entry, payload})

    {final, ""} = (entry <> payload) |> from_binary({false, clump_id})
    final
  end

  @doc false
  def store(entry, clump_id, replace)

  def store(
        %Baobab.Entry{
          author: author,
          log_id: log_id,
          seqnum: seq
        } = entry,
        clump_id,
        false
      ) do
    case Persistence.content(:entry, :exists, {author, log_id, seq}, clump_id) do
      false -> store(entry, clump_id, true)
      true -> entry
    end
  end

  def store(%Baobab.Entry{author: author, log_id: log_id} = entry, clump_id, true) do
    case Baobab.ClumpMeta.blocked?({author, log_id, 1}, clump_id) do
      true ->
        {:error, "Refusing to store for blocked author"}

      false ->
        case Validator.validate(clump_id, entry) do
          %Baobab.Entry{
            tag: tag,
            log_id: log_id,
            seqnum: seq,
            lipmaalink: ll,
            backlink: bl,
            payload: payload,
            payload_hash: ph,
            sig: sig,
            size: size
          } ->
            contents =
              tag <>
                author <>
                Varu64.encode(log_id) <>
                Varu64.encode(seq) <> option(ll) <> option(bl) <> Varu64.encode(size) <> ph <> sig

            Persistence.content(
              :both,
              :write,
              {author, log_id, seq},
              clump_id,
              {contents, payload}
            )

            entry

          error ->
            error
        end
    end
  end

  def store(_, _, _), do: {:error, "Attempt to store non-Baobab.Entry"}

  defp option(val) when is_nil(val), do: <<>>
  defp option(val), do: val

  @doc false
  def delete(author, seq, log_id, clump_id) do
    entry_id = {author, log_id, seq}
    Persistence.content(:entry, :delete, entry_id, clump_id)
  end

  @doc false
  def from_binaries(stuff, validate, clump_id, acc \\ [])
  def from_binaries(:error, _, _, _), do: [{:error, :missing}]
  def from_binaries("", _, _, acc), do: Enum.reverse(acc)

  def from_binaries(bin, validate, clump_id, acc) do
    case {from_binary(bin, clump_id), validate} do
      {{%Baobab.Entry{} = entry, rest}, true} ->
        Validator.validate(clump_id, entry)
        from_binaries(rest, true, clump_id, [entry | acc])

      {{%Baobab.Entry{} = entry, rest}, false} ->
        from_binaries(rest, true, clump_id, [entry | acc])

      _ ->
        [{:error, "Could not reify fully"}]
    end
  end

  defp from_binary(<<tag::binary-size(1), author::binary-size(32), rest::binary>>, clump_id) do
    add_logid(%Baobab.Entry{tag: tag, author: author}, rest, clump_id)
  end

  defp add_logid(map, bin, clump_id) do
    {logid, rest} = Varu64.decode(bin)
    add_sequence_num(Map.put(map, :log_id, logid), rest, clump_id)
  end

  defp add_sequence_num(map, bin, clump_id) do
    {seqnum, rest} = Varu64.decode(bin)
    add_lipmaa(Map.put(map, :seqnum, seqnum), rest, clump_id)
  end

  defp add_lipmaa(%Baobab.Entry{seqnum: 1} = map, bin, clump_id), do: add_size(map, bin, clump_id)

  defp add_lipmaa(
         %Baobab.Entry{seqnum: seq} = map,
         full = <<yamfh::binary-size(66), rest::binary>>,
         clump_id
       ) do
    ll = Lipmaa.linkseq(seq)

    case ll == seq - 1 do
      true -> add_backlink(map, full, clump_id)
      false -> add_backlink(Map.put(map, :lipmaalink, yamfh), rest, clump_id)
    end
  end

  defp add_backlink(map, <<yamfh::binary-size(66), rest::binary>>, clump_id) do
    add_size(Map.put(map, :backlink, yamfh), rest, clump_id)
  end

  defp add_size(map, bin, clump_id) do
    {size, rest} = Varu64.decode(bin)
    add_payload_hash(Map.put(map, :size, size), rest, clump_id)
  end

  defp add_payload_hash(map, <<yamfh::binary-size(66), rest::binary>>, clump_id) do
    add_sig(Map.put(map, :payload_hash, yamfh), rest, clump_id)
  end

  defp add_sig(map, <<sig::binary-size(64), rest::binary>>, clump_id) do
    add_payload(Map.put(map, :sig, sig), rest, clump_id)
  end

  # If we only got the `entry` portion, assume we might have it on disk
  # The `:error` in the struct can act at a signal that we don't
  defp add_payload(
         %Baobab.Entry{author: author, log_id: log_id, seqnum: seqnum} = map,
         "",
         clump_id
       ) do
    {Map.put(
       map,
       :payload,
       Persistence.content(:payload, :contents, {author, log_id, seqnum}, clump_id)
     ), ""}
  end

  defp add_payload(%Baobab.Entry{size: pbytes} = map, full, _) do
    <<payload::binary-size(pbytes), rest::binary>> = full
    {Map.put(map, :payload, payload), rest}
  end
end