feat(tlc): add command gen-header

Introduce the gen-header command to the tool, enabling developers to
create language bindings. Currently, it supports generating C headers
from a transfer list.

Change-Id: Ibec75639c38577802d5abe55c7bc718740aad2b8
Signed-off-by: Harrison Mutai <harrison.mutai@arm.com>
diff --git a/tools/tlc/poetry.lock b/tools/tlc/poetry.lock
index 60402f1..decec59 100644
--- a/tools/tlc/poetry.lock
+++ b/tools/tlc/poetry.lock
@@ -1431,4 +1431,4 @@
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "9ad739a282f180a9c47cf8cb8f1f53a167e1739b1c5bf572932384b45749df26"
+content-hash = "aac9123f3fa544b8c3e9b085f41f5a1c6c4ed2d59ce3236dcda6ea2aef5a694c"
diff --git a/tools/tlc/pyproject.toml b/tools/tlc/pyproject.toml
index ea842e5..b606238 100644
--- a/tools/tlc/pyproject.toml
+++ b/tools/tlc/pyproject.toml
@@ -38,6 +38,7 @@
 rich = "^10.14.0"
 click = "^8.1.7"
 pyyaml = "^6.0.1"
+tox = "^4.18.0"
 jinja2 = "^3.1.4"
 
 [tool.poetry.group.dev]
diff --git a/tools/tlc/tests/test_cli.py b/tools/tlc/tests/test_cli.py
index 4fad349..a5ef30e 100644
--- a/tools/tlc/tests/test_cli.py
+++ b/tools/tlc/tests/test_cli.py
@@ -11,6 +11,7 @@
 
 from math import ceil, log2
 from pathlib import Path
+from re import findall, search
 from unittest import mock
 
 import pytest
@@ -383,6 +384,69 @@
     assert actual == expected
 
 
+@pytest.mark.parametrize("option", ["-O", "--output"])
+def test_gen_tl_header_with_output_name(tlcrunner, tmptlstr, option, filename="test.h"):
+    with tlcrunner.isolated_filesystem():
+        result = tlcrunner.invoke(
+            cli,
+            [
+                "gen-header",
+                option,
+                filename,
+                tmptlstr,
+            ],
+        )
+
+        assert result.exit_code == 0
+        assert Path(filename).exists()
+
+
+def test_gen_tl_with_fdt_header(tmptlstr, tmpfdt):
+    tlcrunner = CliRunner()
+
+    with tlcrunner.isolated_filesystem():
+        tlcrunner.invoke(cli, ["create", "--size", 1000, "--fdt", tmpfdt, tmptlstr])
+
+        result = tlcrunner.invoke(
+            cli,
+            [
+                "gen-header",
+                tmptlstr,
+            ],
+        )
+
+        assert result.exit_code == 0
+        assert Path("header.h").exists()
+
+        with open("header.h", "r") as f:
+            dtb_match = search(r"DTB_OFFSET\s+(\d+)", "".join(f.readlines()))
+            assert dtb_match and dtb_match[1].isnumeric()
+
+
+def test_gen_empty_tl_c_header(tlcrunner, tmptlstr):
+    with tlcrunner.isolated_filesystem():
+        result = tlcrunner.invoke(
+            cli,
+            [
+                "gen-header",
+                tmptlstr,
+            ],
+        )
+
+        assert result.exit_code == 0
+        assert Path("header.h").exists()
+
+        with open("header.h", "r") as f:
+            lines = "".join(f.readlines())
+
+            assert TransferList.hdr_size == int(
+                findall(r"SIZE\s+(0x[0-9a-fA-F]+|\d+)", lines)[0], 16
+            )
+            assert TransferList.version == int(
+                findall(r"VERSION.+(0x[0-9a-fA-F]+|\d+)", lines)[0]
+            )
+
+
 def bytes_to_hex(data: bytes) -> str:
     """Convert bytes to a hex string in the same format as the debugger in
     ArmDS
diff --git a/tools/tlc/tlc/cli.py b/tools/tlc/tlc/cli.py
index 1d4949d..3d60938 100644
--- a/tools/tlc/tlc/cli.py
+++ b/tools/tlc/tlc/cli.py
@@ -12,6 +12,7 @@
 from pathlib import Path
 
 import click
+import jinja2
 import yaml
 
 from tlc.tl import *
@@ -166,6 +167,34 @@
 
 @cli.command()
 @click.argument("filename", type=click.Path(exists=True, dir_okay=False))
+@click.option(
+    "--output",
+    "-O",
+    type=click.Path(exists=False),
+    help="Output filename for the header",
+    default=Path("header.h"),
+)
+def gen_header(filename, output):
+    """Generate a header with common definitions."""
+    tl = TransferList.fromfile(filename)
+    tmp_keys = tl.__dict__
+    tmp_keys["header_guard"] = Path(output).name.replace(".", "_").upper()
+
+    dtb_te = tl.get_entry(1)
+
+    if dtb_te:
+        tmp_keys["dtb_offset"] = dtb_te.offset + dtb_te.hdr_size
+
+    env = jinja2.Environment(
+        loader=jinja2.PackageLoader("tlc", "templates"),
+    )
+    template = env.get_template("header.h.j2")
+    with open(output, "w") as f:
+        f.write(template.render(tmp_keys))
+
+
+@cli.command()
+@click.argument("filename", type=click.Path(exists=True, dir_okay=False))
 def validate(filename):
     """Validate the contents of an existing Transfer List."""
     TransferList.fromfile(filename)
diff --git a/tools/tlc/tlc/templates/header.h.j2 b/tools/tlc/tlc/templates/header.h.j2
new file mode 100644
index 0000000..87707ce
--- /dev/null
+++ b/tools/tlc/tlc/templates/header.h.j2
@@ -0,0 +1,16 @@
+/*
+ * Auto-generated by TLC, this file includes declarations and macros
+ * derived from a Transfer List input.
+ */
+
+#ifndef {{ header_guard }}
+#define {{ header_guard }}
+
+{% if dtb_offset -%}
+#define TRANSFER_LIST_DTB_OFFSET	{{ "0x%x" % dtb_offset }}
+{%- endif %}
+#define TRANSFER_LIST_CONVENTION_VERSION	{{ version }}
+#define TRANSFER_LIST_HEADER_SIZE	{{ "0x%x" % hdr_size }}
+#define TRANSFER_LIST_SIZE		{{ "0x%x" % size }}
+
+#endif /* {{ header_guard }} */
diff --git a/tools/tlc/tlc/tl.py b/tools/tlc/tlc/tl.py
index b8bb399..98d2205 100644
--- a/tools/tlc/tlc/tl.py
+++ b/tools/tlc/tlc/tl.py
@@ -8,7 +8,7 @@
 
 """Module containing definitions pertaining to the 'Transfer List' (TL) type."""
 
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
 
 import math
 import struct
@@ -93,7 +93,7 @@
         self.size = self.hdr_size
         self.total_size = max_size
         self.flags = flags
-        self.entries: List["TransferEntry"] = []
+        self.entries: List[TransferEntry] = []
         self.update_checksum()
 
     def __str__(self) -> str:
@@ -197,15 +197,23 @@
             sum(self.header_to_bytes()) + sum(te.sum_of_bytes for te in self.entries)
         ) % 256
 
-    def get_entry_data_offset(self, tag_id: int) -> int:
-        """Returns offset of data of a TE from the base of the TL."""
+    def get_entry(self, tag_id: int) -> Optional[TransferEntry]:
         for te in self.entries:
             if te.id == tag_id:
-                return te.offset + te.hdr_size
+                return te
+
+        return None
+
+    def get_entry_data_offset(self, tag_id: int) -> int:
+        """Returns offset of data of a TE from the base of the TL."""
+        te = self.get_entry(tag_id)
+
+        if not te:
+            raise ValueError(f"Tag {tag_id} not found in TL!")
 
-        raise ValueError(f"Tag {tag_id} not found in TL!")
+        return te.offset + te.hdr_size
 
-    def add_transfer_entry(self, tag_id: int, data: bytes) -> "TransferEntry":
+    def add_transfer_entry(self, tag_id: int, data: bytes) -> TransferEntry:
         """Appends a TransferEntry into the internal list of TE's."""
         if not (self.total_size >= self.size + TransferEntry.hdr_size + len(data)):
             raise MemoryError(
@@ -220,14 +228,14 @@
 
     def add_transfer_entry_from_struct_format(
         self, tag_id: int, struct_format: str, *args: Any
-    ) -> "TransferEntry":
+    ) -> TransferEntry:
         struct_format = "<" + struct_format
         data = struct.pack(struct_format, *args)
         return self.add_transfer_entry(tag_id, data)
 
     def add_entry_point_info_transfer_entry(
         self, entry: Dict[str, Any]
-    ) -> "TransferEntry":
+    ) -> TransferEntry:
         """Add entry_point_info transfer entry
 
         :param entry: Dictionary of the transfer entry, in the same format as
@@ -285,7 +293,7 @@
     def add_transfer_entry_from_dict(
         self,
         entry: Dict[str, Any],
-    ) -> "TransferEntry":
+    ) -> TransferEntry:
         """Add a transfer entry from data in a dictionary
 
         The dictionary should have the same format as the entries in the yaml
@@ -320,7 +328,7 @@
         else:
             raise ValueError(f"Invalid transfer entry {entry}.")
 
-    def add_transfer_entry_from_file(self, tag_id: int, path: Path) -> "TransferEntry":
+    def add_transfer_entry_from_file(self, tag_id: int, path: Path) -> TransferEntry:
         with open(path, "rb") as f:
             return self.add_transfer_entry(tag_id, f.read())