Skip to content

Commit 2cbada2

Browse files
Load app metadata from companion app_<target>.json file for Rust binaries
1 parent f490198 commit 2cbada2

3 files changed

Lines changed: 254 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.15.0] - 2025-12-23
9+
10+
### Added
11+
12+
- Load app metadata from companion app_<target>.json file for rust binaries
13+
814
## [0.14.0] - 2025-12-03
915

1016
### Added

src/ledgered/binary.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
from argparse import ArgumentParser
34
from dataclasses import asdict, dataclass
@@ -46,10 +47,60 @@ def __init__(self, binary_path: Union[str, Path]):
4647
}
4748
self._sections = Sections(**sections)
4849

50+
# Rust apps store app_name/app_version/app_flags in companion JSON file
51+
if self.is_rust_app:
52+
self._load_metadata_from_json()
53+
54+
def _load_metadata_from_json(self) -> None:
55+
"""Load app metadata from companion app_<target>.json file.
56+
57+
Rust applications don't embed app_name, app_version, and app_flags in the ELF.
58+
Instead, these are stored in a JSON file named app_<target>.json in the same directory.
59+
This method is called only for Rust applications.
60+
"""
61+
target = self._sections.target
62+
if not target:
63+
logging.warning(
64+
"Rust app detected but no target found, cannot locate companion JSON file"
65+
)
66+
return
67+
68+
json_path = self._path.parent / f"app_{target}.json"
69+
if not json_path.exists():
70+
logging.warning("Rust app detected but companion JSON file not found: %s", json_path)
71+
return
72+
73+
try:
74+
logging.info("Loading Rust app metadata from %s", json_path)
75+
with json_path.open("r") as f:
76+
data = json.load(f)
77+
78+
if "name" in data:
79+
self._sections.app_name = data["name"]
80+
logging.debug("Loaded app_name: %s", self._sections.app_name)
81+
82+
if "version" in data:
83+
self._sections.app_version = data["version"]
84+
logging.debug("Loaded app_version: %s", self._sections.app_version)
85+
86+
if "flags" in data:
87+
self._sections.app_flags = data["flags"]
88+
logging.debug("Loaded app_flags: %s", self._sections.app_flags)
89+
90+
except (json.JSONDecodeError, OSError) as e:
91+
logging.error("Failed to load companion JSON file %s: %s", json_path, e)
92+
4993
@property
5094
def sections(self) -> Sections:
5195
return self._sections
5296

97+
@property
98+
def is_rust_app(self) -> bool:
99+
"""Returns True if this is a Rust application."""
100+
return (
101+
self._sections.rust_sdk_name is not None or self._sections.rust_sdk_version is not None
102+
)
103+
53104

54105
def set_parser() -> ArgumentParser:
55106
parser = ArgumentParser(

tests/unit/test_binary.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
import tempfile
13
from dataclasses import dataclass
24
from unittest import TestCase
35
from unittest.mock import patch
@@ -83,3 +85,198 @@ def test___init__from_str(self):
8385
path = "/dev/urandom"
8486
with patch("ledgered.binary.ELFFile"):
8587
B.LedgerBinaryApp(path)
88+
89+
def test_c_app_detection(self):
90+
"""Test that C apps are correctly identified (no rust SDK fields)."""
91+
path = Path("/dev/urandom")
92+
with patch("ledgered.binary.ELFFile") as elfmock:
93+
elfmock().iter_sections.return_value = [
94+
Section("ledger.app_name", b"Test App"),
95+
Section("ledger.app_version", b"1.0.0"),
96+
Section("ledger.target", b"stax"),
97+
]
98+
bin = B.LedgerBinaryApp(path)
99+
100+
self.assertFalse(bin.is_rust_app)
101+
self.assertEqual(bin.sections.app_name, "Test App")
102+
self.assertEqual(bin.sections.app_version, "1.0.0")
103+
104+
def test_rust_app_detection(self):
105+
"""Test that Rust apps are correctly identified by rust_sdk_name."""
106+
path = Path("/dev/urandom")
107+
with patch("ledgered.binary.ELFFile") as elfmock:
108+
elfmock().iter_sections.return_value = [
109+
Section("ledger.rust_sdk_name", b"ledger_secure_sdk_sys"),
110+
Section("ledger.target", b"flex"),
111+
]
112+
bin = B.LedgerBinaryApp(path)
113+
114+
self.assertTrue(bin.is_rust_app)
115+
116+
def test_rust_app_detection_by_version(self):
117+
"""Test that Rust apps are correctly identified by rust_sdk_version."""
118+
path = Path("/dev/urandom")
119+
with patch("ledgered.binary.ELFFile") as elfmock:
120+
elfmock().iter_sections.return_value = [
121+
Section("ledger.rust_sdk_version", b"1.12.0"),
122+
Section("ledger.target", b"stax"),
123+
]
124+
bin = B.LedgerBinaryApp(path)
125+
126+
self.assertTrue(bin.is_rust_app)
127+
128+
def test_rust_app_loads_metadata_from_json(self):
129+
"""Test that Rust apps load app_name, app_version, and app_flags from JSON."""
130+
with tempfile.TemporaryDirectory() as tmpdir:
131+
tmpdir_path = Path(tmpdir)
132+
binary_path = tmpdir_path / "app-boilerplate-rust"
133+
binary_path.touch()
134+
135+
# Create companion JSON file
136+
json_data = {
137+
"name": "Rust Boilerplate",
138+
"version": "1.7.7",
139+
"flags": "0x200",
140+
"apiLevel": "25",
141+
"targetId": "0x33300004",
142+
}
143+
json_path = tmpdir_path / "app_flex.json"
144+
with json_path.open("w") as f:
145+
json.dump(json_data, f)
146+
147+
with patch("ledgered.binary.ELFFile") as elfmock:
148+
elfmock().iter_sections.return_value = [
149+
Section("ledger.rust_sdk_name", b"ledger_secure_sdk_sys"),
150+
Section("ledger.rust_sdk_version", b"1.12.0"),
151+
Section("ledger.target", b"flex"),
152+
Section("ledger.api_level", b"25"),
153+
]
154+
bin = B.LedgerBinaryApp(binary_path)
155+
156+
self.assertTrue(bin.is_rust_app)
157+
self.assertEqual(bin.sections.app_name, "Rust Boilerplate")
158+
self.assertEqual(bin.sections.app_version, "1.7.7")
159+
self.assertEqual(bin.sections.app_flags, "0x200")
160+
self.assertEqual(bin.sections.api_level, "25")
161+
162+
def test_rust_app_missing_json_file(self):
163+
"""Test that Rust apps handle missing JSON file gracefully."""
164+
with tempfile.TemporaryDirectory() as tmpdir:
165+
tmpdir_path = Path(tmpdir)
166+
binary_path = tmpdir_path / "app-boilerplate-rust"
167+
binary_path.touch()
168+
169+
# No JSON file created
170+
with patch("ledgered.binary.ELFFile") as elfmock:
171+
elfmock().iter_sections.return_value = [
172+
Section("ledger.rust_sdk_name", b"ledger_secure_sdk_sys"),
173+
Section("ledger.target", b"flex"),
174+
]
175+
with patch("ledgered.binary.logging") as log_mock:
176+
bin = B.LedgerBinaryApp(binary_path)
177+
178+
# Should warn about missing JSON
179+
log_mock.warning.assert_called()
180+
181+
self.assertTrue(bin.is_rust_app)
182+
self.assertIsNone(bin.sections.app_name)
183+
self.assertIsNone(bin.sections.app_version)
184+
185+
def test_rust_app_no_target(self):
186+
"""Test that Rust apps without target field handle JSON loading gracefully."""
187+
path = Path("/dev/urandom")
188+
with patch("ledgered.binary.ELFFile") as elfmock:
189+
elfmock().iter_sections.return_value = [
190+
Section("ledger.rust_sdk_name", b"ledger_secure_sdk_sys"),
191+
]
192+
with patch("ledgered.binary.logging") as log_mock:
193+
bin = B.LedgerBinaryApp(path)
194+
195+
# Should warn about missing target
196+
log_mock.warning.assert_called()
197+
198+
self.assertTrue(bin.is_rust_app)
199+
self.assertIsNone(bin.sections.app_name)
200+
201+
def test_rust_app_malformed_json(self):
202+
"""Test that Rust apps handle malformed JSON gracefully."""
203+
with tempfile.TemporaryDirectory() as tmpdir:
204+
tmpdir_path = Path(tmpdir)
205+
binary_path = tmpdir_path / "app-boilerplate-rust"
206+
binary_path.touch()
207+
208+
# Create malformed JSON file
209+
json_path = tmpdir_path / "app_flex.json"
210+
with json_path.open("w") as f:
211+
f.write("{ this is not valid json }")
212+
213+
with patch("ledgered.binary.ELFFile") as elfmock:
214+
elfmock().iter_sections.return_value = [
215+
Section("ledger.rust_sdk_name", b"ledger_secure_sdk_sys"),
216+
Section("ledger.target", b"flex"),
217+
]
218+
with patch("ledgered.binary.logging") as log_mock:
219+
bin = B.LedgerBinaryApp(binary_path)
220+
221+
# Should log error
222+
log_mock.error.assert_called()
223+
224+
self.assertTrue(bin.is_rust_app)
225+
self.assertIsNone(bin.sections.app_name)
226+
227+
def test_rust_app_partial_json_data(self):
228+
"""Test that Rust apps handle JSON with missing fields."""
229+
with tempfile.TemporaryDirectory() as tmpdir:
230+
tmpdir_path = Path(tmpdir)
231+
binary_path = tmpdir_path / "app-boilerplate-rust"
232+
binary_path.touch()
233+
234+
# Create JSON file with only some fields
235+
json_data = {
236+
"name": "Rust App",
237+
# Missing version and flags
238+
}
239+
json_path = tmpdir_path / "app_stax.json"
240+
with json_path.open("w") as f:
241+
json.dump(json_data, f)
242+
243+
with patch("ledgered.binary.ELFFile") as elfmock:
244+
elfmock().iter_sections.return_value = [
245+
Section("ledger.rust_sdk_name", b"ledger_secure_sdk_sys"),
246+
Section("ledger.target", b"stax"),
247+
]
248+
bin = B.LedgerBinaryApp(binary_path)
249+
250+
self.assertTrue(bin.is_rust_app)
251+
self.assertEqual(bin.sections.app_name, "Rust App")
252+
self.assertIsNone(bin.sections.app_version)
253+
self.assertIsNone(bin.sections.app_flags)
254+
255+
def test_c_app_does_not_load_json(self):
256+
"""Test that C apps do not attempt to load JSON file even if present."""
257+
with tempfile.TemporaryDirectory() as tmpdir:
258+
tmpdir_path = Path(tmpdir)
259+
binary_path = tmpdir_path / "app.elf"
260+
binary_path.touch()
261+
262+
# Create JSON file that should be ignored
263+
json_data = {
264+
"name": "Should Be Ignored",
265+
"version": "9.9.9",
266+
}
267+
json_path = tmpdir_path / "app_stax.json"
268+
with json_path.open("w") as f:
269+
json.dump(json_data, f)
270+
271+
with patch("ledgered.binary.ELFFile") as elfmock:
272+
elfmock().iter_sections.return_value = [
273+
Section("ledger.app_name", b"C App"),
274+
Section("ledger.app_version", b"1.0.0"),
275+
Section("ledger.target", b"stax"),
276+
]
277+
bin = B.LedgerBinaryApp(binary_path)
278+
279+
# Should use ELF data, not JSON
280+
self.assertFalse(bin.is_rust_app)
281+
self.assertEqual(bin.sections.app_name, "C App")
282+
self.assertEqual(bin.sections.app_version, "1.0.0")

0 commit comments

Comments
 (0)