feat(tlc): add host tool for static TL generation
Transfer List Compiler is a command line tool that enables the static
generation of TL's compliant with version 0.9 of the firmware handoff
specification. The intent of this tool is to support information passing
via the firmware handoff framework to bootloaders that run without
preceding images (i.e. `RESET_TO_BL31`).
It currently allows for TL's to be statically generated from blobs of
data, and modified by removing/adding TE's. Future work will provide
support for TL generation from configuration file.
Change-Id: Iff670842e34c9ad18eac935248ee2aece43dc533
Signed-off-by: Harrison Mutai <harrison.mutai@arm.com>
Co-authored-by: Charlie Bareham <charlie.bareham@arm.com>
diff --git a/tools/tlc/tests/conftest.py b/tools/tlc/tests/conftest.py
new file mode 100644
index 0000000..6b28e43
--- /dev/null
+++ b/tools/tlc/tests/conftest.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+# type: ignore[attr-defined]
+
+#
+# Copyright (c) 2024, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+""" Common configurations and fixtures for test environment."""
+
+import pytest
+from click.testing import CliRunner
+
+from tlc.cli import cli
+
+
+@pytest.fixture
+def tmptlstr(tmpdir):
+ return tmpdir.join("tl.bin").strpath
+
+
+@pytest.fixture
+def tmpfdt(tmpdir):
+ fdt = tmpdir.join("fdt.dtb")
+ fdt.write_binary(b"\x00" * 100)
+ return fdt
+
+
+@pytest.fixture
+def tlcrunner(tmptlstr):
+ runner = CliRunner()
+ with runner.isolated_filesystem():
+ runner.invoke(cli, ["create", tmptlstr])
+ return runner
+
+
+@pytest.fixture
+def tlc_entries(tmpfdt):
+ return [(0, "/dev/null"), (1, tmpfdt.strpath), (0x102, tmpfdt.strpath)]
diff --git a/tools/tlc/tests/test_cli.py b/tools/tlc/tests/test_cli.py
new file mode 100644
index 0000000..d79773b
--- /dev/null
+++ b/tools/tlc/tests/test_cli.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python3
+# type: ignore[attr-defined]
+
+#
+# Copyright (c) 2024, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+"""Contains unit tests for the CLI functionality."""
+
+from pathlib import Path
+from unittest import mock
+
+import pytest
+from click.testing import CliRunner
+
+from tlc.cli import cli
+from tlc.te import TransferEntry
+from tlc.tl import TransferList
+
+
+def test_create_empty_tl(tmpdir):
+ runner = CliRunner()
+ test_file = tmpdir.join("tl.bin")
+
+ result = runner.invoke(cli, ["create", test_file.strpath])
+ assert result.exit_code == 0
+ assert TransferList.fromfile(test_file) is not None
+
+
+def test_create_with_fdt(tmpdir):
+ runner = CliRunner()
+ fdt = tmpdir.join("fdt.dtb")
+ fdt.write_binary(b"\x00" * 100)
+
+ result = runner.invoke(
+ cli,
+ [
+ "create",
+ "--fdt",
+ fdt.strpath,
+ "--size",
+ "1000",
+ tmpdir.join("tl.bin").strpath,
+ ],
+ )
+ assert result.exit_code == 0
+
+
+def test_add_single_entry(tlcrunner, tmptlstr):
+ tlcrunner.invoke(cli, ["add", "--entry", "0", "/dev/null", tmptlstr])
+
+ tl = TransferList.fromfile(tmptlstr)
+ assert tl is not None
+ assert len(tl.entries) == 1
+ assert tl.entries[0].id == 0
+
+
+def test_add_multiple_entries(tlcrunner, tlc_entries, tmptlstr):
+ for id, path in tlc_entries:
+ tlcrunner.invoke(cli, ["add", "--entry", id, path, tmptlstr])
+
+ tl = TransferList.fromfile(tmptlstr)
+ assert tl is not None
+ assert len(tl.entries) == len(tlc_entries)
+
+
+def test_info(tlcrunner, tmptlstr, tmpfdt):
+ tlcrunner.invoke(cli, ["add", "--entry", "0", "/dev/null", tmptlstr])
+ tlcrunner.invoke(cli, ["add", "--fdt", tmpfdt.strpath, tmptlstr])
+
+ result = tlcrunner.invoke(cli, ["info", tmptlstr])
+ assert result.exit_code == 0
+ assert "signature" in result.stdout
+ assert "id" in result.stdout
+
+ result = tlcrunner.invoke(cli, ["info", "--header", tmptlstr])
+ assert result.exit_code == 0
+ assert "signature" in result.stdout
+ assert "id" not in result.stdout
+
+ result = tlcrunner.invoke(cli, ["info", "--entries", tmptlstr])
+ assert result.exit_code == 0
+ assert "signature" not in result.stdout
+ assert "id" in result.stdout
+
+
+def test_raises_max_size_error(tmptlstr, tmpfdt):
+ tmpfdt.write_binary(bytes(6000))
+
+ runner = CliRunner()
+ result = runner.invoke(cli, ["create", "--fdt", tmpfdt, tmptlstr])
+
+ assert result.exception
+ assert isinstance(result.exception, MemoryError)
+ assert "TL max size exceeded, consider increasing with the option -s" in str(
+ result.exception
+ )
+ assert "TL size has exceeded the maximum allocation" in str(
+ result.exception.__cause__
+ )
+
+
+def test_info_get_fdt_offset(tmptlstr, tmpfdt):
+ runner = CliRunner()
+ with runner.isolated_filesystem():
+ runner.invoke(cli, ["create", "--size", "1000", tmptlstr])
+ runner.invoke(cli, ["add", "--entry", "1", tmpfdt.strpath, tmptlstr])
+ result = runner.invoke(cli, ["info", "--fdt-offset", tmptlstr])
+
+ assert result.exit_code == 0
+ assert result.output.strip("\n").isdigit()
+
+
+def test_remove_tag(tlcrunner, tmptlstr):
+ tlcrunner.invoke(cli, ["add", "--entry", "0", "/dev/null", tmptlstr])
+ result = tlcrunner.invoke(cli, ["info", tmptlstr])
+
+ assert result.exit_code == 0
+ assert "signature" in result.stdout
+
+ tlcrunner.invoke(cli, ["remove", "--tags", "0", tmptlstr])
+ tl = TransferList.fromfile(tmptlstr)
+
+ assert result.exit_code == 0
+ assert len(tl.entries) == 0
+
+
+def test_unpack_tl(tlcrunner, tmptlstr, tmpfdt, tmpdir):
+ with tlcrunner.isolated_filesystem(temp_dir=tmpdir):
+ tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
+ tlcrunner.invoke(cli, ["unpack", tmptlstr])
+ assert Path("te_0_1.bin").exists()
+
+
+def test_unpack_multiple_tes(tlcrunner, tlc_entries, tmptlstr, tmpdir):
+ with tlcrunner.isolated_filesystem(temp_dir=tmpdir):
+ for id, path in tlc_entries:
+ tlcrunner.invoke(cli, ["add", "--entry", id, path, tmptlstr])
+
+ assert all(
+ filter(
+ lambda te: (Path(tmpdir.strpath) / f"te_{te[0]}.bin").exists(), tlc_entries
+ )
+ )
+
+
+def test_unpack_into_dir(tlcrunner, tmpdir, tmptlstr, tmpfdt):
+ tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
+ tlcrunner.invoke(cli, ["unpack", "-C", tmpdir.strpath, tmptlstr])
+
+ assert (Path(tmpdir.strpath) / "te_0_1.bin").exists()
+
+
+def test_unpack_into_dir_with_conflicting_tags(tlcrunner, tmpdir, tmptlstr, tmpfdt):
+ tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
+ tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
+ tlcrunner.invoke(cli, ["unpack", "-C", tmpdir.strpath, tmptlstr])
+
+ assert (Path(tmpdir.strpath) / "te_0_1.bin").exists()
+ assert (Path(tmpdir.strpath) / "te_1_1.bin").exists()
+
+
+def test_validate_invalid_signature(tmptlstr, tlcrunner, monkeypatch):
+ tl = TransferList()
+ tl.signature = 0xDEADBEEF
+
+ mock_open = lambda tmptlstr, mode: mock.mock_open(read_data=tl.header_to_bytes())()
+ monkeypatch.setattr("builtins.open", mock_open)
+
+ result = tlcrunner.invoke(cli, ["validate", tmptlstr])
+ assert result.exit_code != 0
+
+
+def test_validate_misaligned_entries(tmptlstr, tlcrunner, monkeypatch):
+ """Base address of a TE must be 8-byte aligned."""
+ mock_open = lambda tmptlstr, mode: mock.mock_open(
+ read_data=TransferList().header_to_bytes()
+ + bytes(5)
+ + TransferEntry(0, 0, bytes(0)).header_to_bytes
+ )()
+ monkeypatch.setattr("builtins.open", mock_open)
+
+ result = tlcrunner.invoke(cli, ["validate", tmptlstr])
+
+ assert result.exit_code == 1
+
+
+@pytest.mark.parametrize(
+ "version", [0, TransferList.version, TransferList.version + 1, 1 << 8]
+)
+def test_validate_unsupported_version(version, tmptlstr, tlcrunner, monkeypatch):
+ tl = TransferList()
+ tl.version = version
+
+ mock_open = lambda tmptlstr, mode: mock.mock_open(read_data=tl.header_to_bytes())()
+ monkeypatch.setattr("builtins.open", mock_open)
+
+ result = tlcrunner.invoke(cli, ["validate", tmptlstr])
+
+ if version >= TransferList.version and version <= 0xFF:
+ assert result.exit_code == 0
+ else:
+ assert result.exit_code == 1
diff --git a/tools/tlc/tests/test_transfer_list.py b/tools/tlc/tests/test_transfer_list.py
new file mode 100644
index 0000000..e8c430e
--- /dev/null
+++ b/tools/tlc/tests/test_transfer_list.py
@@ -0,0 +1,234 @@
+#!/usr/bin/env python3
+
+#
+# Copyright (c) 2024, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+"""Contains unit tests for the types TransferEntry and TransferList."""
+
+import math
+
+import pytest
+
+from tlc.te import TransferEntry
+from tlc.tl import TransferList
+
+large_data = 0xDEADBEEF.to_bytes(4, "big")
+small_data = 0x1234.to_bytes(3, "big")
+test_entries = [
+ (0, b""),
+ (1, small_data),
+ (1, large_data),
+ (0xFFFFFF, small_data),
+ (0xFFFFFF, large_data),
+]
+
+
+@pytest.mark.parametrize(
+ "size,csum",
+ [
+ (-1, None),
+ (0x18, 0x9E),
+ (0x1000, 0xA6),
+ (0x2000, 0x96),
+ (0x4000, 0x76),
+ ],
+)
+def test_make_transfer_list(size, csum):
+ if size < 8:
+ with pytest.raises(AssertionError):
+ tl = TransferList(size)
+ else:
+ tl = TransferList(size)
+
+ assert tl.signature == 0x4A0FB10B
+ assert not tl.entries
+ assert tl.sum_of_bytes() == 0
+ assert tl.checksum == csum
+
+
+@pytest.mark.parametrize(("tag_id", "data"), test_entries)
+def test_add_transfer_entry(tag_id, data):
+ tl = TransferList(0x1000)
+ te = TransferEntry(tag_id, len(data), data)
+
+ tl.add_transfer_entry(tag_id, data)
+
+ assert te in tl.entries
+ assert tl.size == TransferList.hdr_size + te.size
+
+
+@pytest.mark.parametrize(
+ ("tag_id", "data"),
+ [
+ (-1, None), # tag out of range
+ (1, None), # no data provided
+ (1, bytes(8000)), # very large data > total size
+ (0x100000, b"0dd0edfe"), # tag out of range
+ ],
+)
+def test_add_out_of_range_transfer_entry(tag_id, data):
+ tl = TransferList()
+
+ with pytest.raises(Exception):
+ tl.add_transfer_entry(tag_id, data)
+
+
+@pytest.mark.parametrize(("tag_id", "data"), test_entries)
+def test_calculate_te_sum_of_bytes(tag_id, data):
+ te = TransferEntry(tag_id, len(data), data)
+ csum = (
+ sum(data)
+ + sum(len(data).to_bytes(4, "big"))
+ + te.hdr_size
+ + sum(tag_id.to_bytes(4, "big"))
+ ) % 256
+ assert te.sum_of_bytes == csum
+
+
+@pytest.mark.parametrize(("tag_id", "data"), test_entries)
+def test_calculate_tl_checksum(tag_id, data):
+ tl = TransferList(0x1000)
+
+ tl.add_transfer_entry(tag_id, data)
+ assert tl.sum_of_bytes() == 0
+
+
+def test_empty_transfer_list_blob(tmpdir):
+ """Check that we can correctly create a transfer list header."""
+ test_file = tmpdir.join("test_tl_blob.bin")
+ tl = TransferList()
+ tl.write_to_file(test_file)
+
+ with open(test_file, "rb") as f:
+ assert f.read(tl.hdr_size) == tl.header_to_bytes()
+
+
+@pytest.mark.parametrize(("tag_id", "data"), test_entries)
+def test_single_te_transfer_list(tag_id, data, tmpdir):
+ """Check that we can create a complete TL with a single TE."""
+ test_file = tmpdir.join("test_tl_blob.bin")
+ tl = TransferList(0x1000)
+
+ tl.add_transfer_entry(tag_id, data)
+ tl.write_to_file(test_file)
+
+ te = tl.entries[0]
+
+ with open(test_file, "rb") as f:
+ assert f.read(tl.hdr_size) == tl.header_to_bytes()
+ assert int.from_bytes(f.read(3), "little") == te.id
+ assert int.from_bytes(f.read(1), "little") == te.hdr_size
+ assert int.from_bytes(f.read(4), "little") == te.data_size
+ assert f.read(te.data_size) == te.data
+
+
+def test_multiple_te_transfer_list(tmpdir):
+ """Check that we can create a TL with multiple TE's."""
+ test_file = tmpdir.join("test_tl_blob.bin")
+ tl = TransferList(0x1000)
+
+ for tag_id, data in test_entries:
+ tl.add_transfer_entry(tag_id, data)
+
+ tl.write_to_file(test_file)
+
+ with open(test_file, "rb") as f:
+ assert f.read(tl.hdr_size) == tl.header_to_bytes()
+ # Ensure that TE's have the correct alignment
+ for tag_id, data in test_entries:
+ f.seek(int(math.ceil(f.tell() / 2**tl.alignment) * 2**tl.alignment))
+ print(f.tell())
+ assert int.from_bytes(f.read(3), "little") == tag_id
+ assert int.from_bytes(f.read(1), "little") == TransferEntry.hdr_size
+ # Make sure the data in the TE matches the data in the original case
+ data_size = int.from_bytes(f.read(4), "little")
+ assert f.read(data_size) == data
+
+
+def test_read_empty_transfer_list_from_file(tmpdir):
+ test_file = tmpdir.join("test_tl_blob.bin")
+ original_tl = TransferList(0x1000)
+ original_tl.write_to_file(test_file)
+
+ # Read the contents of the file we just wrote
+ tl = TransferList.fromfile(test_file)
+ assert tl.header_to_bytes() == original_tl.header_to_bytes()
+ assert tl.sum_of_bytes() == 0
+
+
+def test_read_single_transfer_list_from_file(tmpdir):
+ test_file = tmpdir.join("test_tl_blob.bin")
+ original_tl = TransferList(0x1000)
+
+ original_tl.add_transfer_entry(test_entries[0][0], test_entries[0][1])
+ original_tl.write_to_file(test_file)
+
+ # Read the contents of the file we just wrote
+ tl = TransferList.fromfile(test_file)
+ assert tl.entries
+
+ te = tl.entries[0]
+ assert te.id == test_entries[0][0]
+ assert te.data == test_entries[0][1]
+ assert tl.sum_of_bytes() == 0
+
+
+def test_read_multiple_transfer_list_from_file(tmpdir):
+ test_file = tmpdir.join("test_tl_blob.bin")
+ original_tl = TransferList(0x1000)
+
+ for tag_id, data in test_entries:
+ original_tl.add_transfer_entry(tag_id, data)
+
+ original_tl.write_to_file(test_file)
+
+ # Read the contents of the file we just wrote
+ tl = TransferList.fromfile(test_file)
+
+ # The TE we derive from the file might have a an associated offset, compare
+ # the TE's based on the header in bytes, which doesn't account for this.
+ for te0, te1 in zip(tl.entries, original_tl.entries):
+ assert te0.header_to_bytes() == te1.header_to_bytes()
+
+ assert tl.sum_of_bytes() == 0
+
+
+@pytest.mark.parametrize("tag", [tag for tag, _ in test_entries])
+def test_remove_tag_from_file(tag):
+ tl = TransferList(0x1000)
+
+ for tag_id, data in test_entries:
+ tl.add_transfer_entry(tag_id, data)
+
+ removed_entries = list(filter(lambda te: te.id == tag, tl.entries))
+ original_size = tl.size
+ tl.remove_tag(tag)
+
+ assert not any(tag == te.id for te in tl.entries)
+ assert tl.size == original_size - sum(map(lambda te: te.size, removed_entries))
+
+
+def test_get_fdt_offset(tmpdir):
+ tl = TransferList(0x1000)
+ tl.add_transfer_entry(1, 0xEDFE0DD0.to_bytes(4, "big"))
+ f = tmpdir.join("blob.bin")
+
+ tl.write_to_file(f)
+
+ blob_tl = TransferList.fromfile(f)
+
+ assert blob_tl.hdr_size + TransferEntry.hdr_size == blob_tl.get_entry_data_offset(1)
+
+
+def test_get_missing_fdt_offset(tmpdir):
+ tl = TransferList(0x1000)
+ f = tmpdir.join("blob.bin")
+
+ tl.write_to_file(f)
+ blob_tl = TransferList.fromfile(f)
+
+ with pytest.raises(ValueError):
+ blob_tl.get_entry_data_offset(1)