|
|
|
@ -16,9 +16,17 @@ from ..tools import EnumAdapter, TupleAdapter
|
|
|
|
|
|
|
|
|
|
# All sections need to be aligned to 2 bytes for the offset tables using u16 to work properly
|
|
|
|
|
ALIGNMENT = 2
|
|
|
|
|
# "align end of struct" subcon. The builtin c.Aligned does not do the right thing,
|
|
|
|
|
# because it assumes that the alignment is relative to the start of the subcon, not the
|
|
|
|
|
# start of the whole struct.
|
|
|
|
|
# TODO this spelling may or may not align in context of the stream as a whole (as
|
|
|
|
|
# opposed to the containing struct). This is prooobably not a problem -- we want the
|
|
|
|
|
# top-level alignment to always be ALIGNMENT anyway. But if someone were to use some
|
|
|
|
|
# of the structs separately, they might get a surprise. Maybe. Didn't test this.
|
|
|
|
|
ALIGN_SUBCON = c.Padding(
|
|
|
|
|
lambda ctx: (ALIGNMENT - (ctx._io.tell() % ALIGNMENT)) % ALIGNMENT
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
JsonTranslationData = t.Dict[str, t.Dict[str, str]]
|
|
|
|
|
TranslatedStrings = t.Dict[str, str]
|
|
|
|
|
JsonFontInfo = t.Dict[str, str]
|
|
|
|
|
Order = t.Dict[int, str]
|
|
|
|
|
|
|
|
|
@ -32,7 +40,7 @@ class JsonHeader(TypedDict):
|
|
|
|
|
|
|
|
|
|
class JsonDef(TypedDict):
|
|
|
|
|
header: JsonHeader
|
|
|
|
|
translations: JsonTranslationData
|
|
|
|
|
translations: dict[str, str]
|
|
|
|
|
fonts: dict[str, JsonFontInfo]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -54,7 +62,7 @@ def _version_to_tuple(version: str) -> tuple[int, int, int, int]:
|
|
|
|
|
return (*items, 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TranslationsHeader(Struct):
|
|
|
|
|
class Header(Struct):
|
|
|
|
|
language: str
|
|
|
|
|
model: Model
|
|
|
|
|
firmware_version: tuple[int, int, int, int]
|
|
|
|
@ -73,13 +81,13 @@ class TranslationsHeader(Struct):
|
|
|
|
|
"data_hash" / c.Bytes(32),
|
|
|
|
|
"change_language_title" / c.PascalString(c.Int8ul, "utf8"),
|
|
|
|
|
"change_language_prompt" / c.PascalString(c.Int8ul, "utf8"),
|
|
|
|
|
c.Aligned(ALIGNMENT, c.Pass),
|
|
|
|
|
ALIGN_SUBCON,
|
|
|
|
|
c.Terminated,
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TranslationsProof(Struct):
|
|
|
|
|
class Proof(Struct):
|
|
|
|
|
merkle_proof: list[bytes]
|
|
|
|
|
sigmask: int
|
|
|
|
|
signature: bytes
|
|
|
|
@ -89,6 +97,7 @@ class TranslationsProof(Struct):
|
|
|
|
|
"merkle_proof" / c.PrefixedArray(c.Int8ul, c.Bytes(32)),
|
|
|
|
|
"sigmask" / c.Byte,
|
|
|
|
|
"signature" / c.Bytes(64),
|
|
|
|
|
ALIGN_SUBCON,
|
|
|
|
|
c.Terminated,
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
@ -105,7 +114,7 @@ class BlobTable(Struct):
|
|
|
|
|
"_length" / c.Rebuild(c.Int16ul, c.len_(c.this.offsets) - 1),
|
|
|
|
|
"offsets" / c.Array(c.this._length + 1, TupleAdapter(c.Int16ul, c.Int16ul)),
|
|
|
|
|
"data" / c.GreedyBytes,
|
|
|
|
|
c.Aligned(ALIGNMENT, c.Pass),
|
|
|
|
|
ALIGN_SUBCON,
|
|
|
|
|
c.Terminated,
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
@ -135,7 +144,7 @@ class BlobTable(Struct):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TranslationsData(Struct):
|
|
|
|
|
class TranslatedStrings(Struct):
|
|
|
|
|
offsets: list[int]
|
|
|
|
|
strings: bytes
|
|
|
|
|
|
|
|
|
@ -143,7 +152,8 @@ class TranslationsData(Struct):
|
|
|
|
|
SUBCON = c.Struct(
|
|
|
|
|
"_length" / c.Rebuild(c.Int16ul, c.len_(c.this.offsets) - 1),
|
|
|
|
|
"offsets" / c.Array(c.this._length + 1, c.Int16ul),
|
|
|
|
|
"strings" / c.Aligned(ALIGNMENT, c.GreedyBytes),
|
|
|
|
|
"strings" / c.GreedyBytes,
|
|
|
|
|
ALIGN_SUBCON,
|
|
|
|
|
c.Terminated,
|
|
|
|
|
)
|
|
|
|
|
# fmt: on
|
|
|
|
@ -214,7 +224,7 @@ class FontsTable(BlobTable):
|
|
|
|
|
# =========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TranslationsPayload(Struct):
|
|
|
|
|
class Payload(Struct):
|
|
|
|
|
translations_bytes: bytes
|
|
|
|
|
fonts_bytes: bytes
|
|
|
|
|
|
|
|
|
@ -230,7 +240,7 @@ class TranslationsPayload(Struct):
|
|
|
|
|
class TranslationsBlob(Struct):
|
|
|
|
|
header_bytes: bytes
|
|
|
|
|
proof_bytes: bytes
|
|
|
|
|
payload: TranslationsPayload = subcon(TranslationsPayload)
|
|
|
|
|
payload: Payload = subcon(Payload)
|
|
|
|
|
|
|
|
|
|
# fmt: off
|
|
|
|
|
SUBCON = c.Struct(
|
|
|
|
@ -248,7 +258,7 @@ class TranslationsBlob(Struct):
|
|
|
|
|
"_start_offset" / c.Tell,
|
|
|
|
|
"header_bytes" / c.Prefixed(c.Int16ul, c.GreedyBytes),
|
|
|
|
|
"proof_bytes" / c.Prefixed(c.Int16ul, c.GreedyBytes),
|
|
|
|
|
"payload" / TranslationsPayload.SUBCON,
|
|
|
|
|
"payload" / Payload.SUBCON,
|
|
|
|
|
"_end_offset" / c.Tell,
|
|
|
|
|
c.Terminated,
|
|
|
|
|
|
|
|
|
@ -258,60 +268,59 @@ class TranslationsBlob(Struct):
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def header(self):
|
|
|
|
|
return TranslationsHeader.parse(self.header_bytes)
|
|
|
|
|
return Header.parse(self.header_bytes)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def proof(self):
|
|
|
|
|
return TranslationsProof.parse(self.proof_bytes)
|
|
|
|
|
return Proof.parse(self.proof_bytes)
|
|
|
|
|
|
|
|
|
|
@proof.setter
|
|
|
|
|
def proof(self, proof: TranslationsProof):
|
|
|
|
|
def proof(self, proof: Proof):
|
|
|
|
|
self.proof_bytes = proof.build()
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def translations(self):
|
|
|
|
|
return TranslationsData.parse(self.payload.translations_bytes)
|
|
|
|
|
return TranslatedStrings.parse(self.payload.translations_bytes)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def fonts(self):
|
|
|
|
|
return FontsTable.parse(self.payload.fonts_bytes)
|
|
|
|
|
|
|
|
|
|
def verify(self) -> None:
|
|
|
|
|
header = self.header
|
|
|
|
|
data = self.payload.build()
|
|
|
|
|
|
|
|
|
|
assert header.data_len == len(data)
|
|
|
|
|
assert header.data_hash == sha256(data).digest()
|
|
|
|
|
def build(self) -> bytes:
|
|
|
|
|
assert len(self.header_bytes) % ALIGNMENT == 0
|
|
|
|
|
assert len(self.proof_bytes) % ALIGNMENT == 0
|
|
|
|
|
assert len(self.payload.translations_bytes) % ALIGNMENT == 0
|
|
|
|
|
assert len(self.payload.fonts_bytes) % ALIGNMENT == 0
|
|
|
|
|
return super().build()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_blob(dir: Path, lang: str, model: TrezorModel) -> TranslationsBlob:
|
|
|
|
|
lang_file = dir / f"{lang}.json"
|
|
|
|
|
fonts_dir = dir / "fonts"
|
|
|
|
|
def order_from_json(json_order: dict[str, str]) -> Order:
|
|
|
|
|
return {int(k): v for k, v in json_order.items()}
|
|
|
|
|
|
|
|
|
|
lang_data: JsonDef = json.loads(lang_file.read_text())
|
|
|
|
|
|
|
|
|
|
def blob_from_defs(
|
|
|
|
|
lang_data: JsonDef,
|
|
|
|
|
order: Order,
|
|
|
|
|
model: TrezorModel,
|
|
|
|
|
fonts_dir: Path,
|
|
|
|
|
) -> TranslationsBlob:
|
|
|
|
|
json_header: JsonHeader = lang_data["header"]
|
|
|
|
|
|
|
|
|
|
json_order = json.loads((dir / "order.json").read_text())
|
|
|
|
|
order: Order = {int(k): v for k, v in json_order.items()}
|
|
|
|
|
|
|
|
|
|
# flatten translations
|
|
|
|
|
translations_flattened = {
|
|
|
|
|
f"{section}__{key}": value
|
|
|
|
|
for section, section_data in lang_data["translations"].items()
|
|
|
|
|
for key, value in section_data.items()
|
|
|
|
|
}
|
|
|
|
|
# order translations -- python dicts keep insertion order
|
|
|
|
|
translations_ordered = [
|
|
|
|
|
translations_flattened.get(key, "") for _, key in sorted(order.items())
|
|
|
|
|
lang_data["translations"].get(key, "") for _, key in sorted(order.items())
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
translations = TranslationsData.from_items(translations_ordered)
|
|
|
|
|
translations = TranslatedStrings.from_items(translations_ordered)
|
|
|
|
|
|
|
|
|
|
if model.internal_name not in lang_data["fonts"]:
|
|
|
|
|
raise ValueError(f"Model {model.internal_name} not found in {lang_file}")
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Model {model.internal_name} not found in header for {json_header['language']} v{json_header['version']}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
model_fonts = lang_data["fonts"][model.internal_name]
|
|
|
|
|
fonts = FontsTable.from_dir(model_fonts, fonts_dir)
|
|
|
|
|
|
|
|
|
@ -320,13 +329,13 @@ def make_blob(dir: Path, lang: str, model: TrezorModel) -> TranslationsBlob:
|
|
|
|
|
fonts_bytes = fonts.build()
|
|
|
|
|
assert len(fonts_bytes) % ALIGNMENT == 0
|
|
|
|
|
|
|
|
|
|
payload = TranslationsPayload(
|
|
|
|
|
payload = Payload(
|
|
|
|
|
translations_bytes=translations_bytes,
|
|
|
|
|
fonts_bytes=fonts_bytes,
|
|
|
|
|
)
|
|
|
|
|
data = payload.build()
|
|
|
|
|
|
|
|
|
|
header = TranslationsHeader(
|
|
|
|
|
header = Header(
|
|
|
|
|
language=json_header["language"],
|
|
|
|
|
model=Model.from_trezor_model(model),
|
|
|
|
|
firmware_version=_version_to_tuple(json_header["version"]),
|
|
|
|
@ -341,3 +350,12 @@ def make_blob(dir: Path, lang: str, model: TrezorModel) -> TranslationsBlob:
|
|
|
|
|
proof_bytes=b"",
|
|
|
|
|
payload=payload,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def blob_from_dir(dir: Path, lang: str, model: TrezorModel) -> TranslationsBlob:
|
|
|
|
|
lang_file = dir / f"{lang}.json"
|
|
|
|
|
fonts_dir = dir / "fonts"
|
|
|
|
|
json_order = json.loads((dir / "order.json").read_text())
|
|
|
|
|
lang_data = json.loads(lang_file.read_text())
|
|
|
|
|
order = order_from_json(json_order)
|
|
|
|
|
return blob_from_defs(lang_data, order, model, fonts_dir)
|
|
|
|
|