WIP - cleaner translations interface

pull/3491/head
matejcik 5 months ago
parent 43ef1f29d8
commit 9b86d9a829

@ -0,0 +1,56 @@
use crate::{error::Error, trezorhal::translations};
use super::Translations;
static mut TRANSLATIONS_ON_FLASH: Option<Translations> = None;
pub fn erase() -> Result<(), Error> {
// SAFETY: Looking is safe (in a single threaded environment).
if unsafe { TRANSLATIONS_ON_FLASH.is_some() } {
return Err(value_error!("Translations blob already set"));
}
// SAFETY: The blob is not set, so there are no references to it.
unsafe { translations::erase() };
Ok(())
}
pub fn write(data: &[u8], offset: usize) -> Result<(), Error> {
// SAFETY: Looking is safe (in a single threaded environment).
if unsafe { TRANSLATIONS_ON_FLASH.is_some() } {
return Err(value_error!("Translations blob already set"));
}
// SAFETY: The blob is not set, so there are no references to it.
unsafe { translations::write(data, offset) };
Ok(())
}
/// Load translations from flash, validate, and cache references to lookup
/// tables.
pub fn init() -> Result<(), Error> {
if unsafe { TRANSLATIONS_ON_FLASH.is_some() } {
return Ok(());
}
let flash_data = unsafe { translations::get_blob() };
todo!();
let blob = Translations::new(flash_data);
// SAFETY: TODO
unsafe { TRANSLATIONS_ON_FLASH = blob.ok() };
Ok(())
}
// SAFETY: Invalidates all references coming from the flash-based blob.
// In other words, none should exist when this function is called.
pub unsafe fn deinit() {
// SAFETY: Given the above, we can safely clear the cached object.
unsafe { TRANSLATIONS_ON_FLASH = None };
}
// SAFETY: Gives out a reference to a TranslationsBlob which can be invalidated
// by calling `erase()`. The caller must not store this reference, nor any that
// come from it, beyond the lifetime of the current function.
pub unsafe fn get<'a>() -> Option<&'a Translations<'a>> {
// SAFETY: We are in a single-threaded environment.
unsafe { TRANSLATIONS_ON_FLASH.as_ref() }
}

@ -0,0 +1,62 @@
//! generated from enum.out.mako
//! (by running `make templates` in `core`)
//! do not edit manually!
<%
import json
from pathlib import Path
THIS = Path(local.filename).resolve()
SRCDIR = THIS.parent.parent
order_file = SRCDIR / "order.json"
order_index_name = json.loads(order_file.read_text())
order = {int(k): v for k, v in order_index_name.items()}
en_file = SRCDIR / "en.json"
en_data = json.loads(en_file.read_text())["translations"]
def get_en_strings(data: dict) -> dict[str, str]:
res = {}
for section_name, section in data.items():
for k, v in section.items():
key = f"{section_name}__{k}"
res[key] = json.dumps(v)
return res
en_strings = get_en_strings(en_data)
%>\
#[cfg(feature = "micropython")]
use crate::micropython::qstr::Qstr;
#[derive(Debug, Copy, Clone, FromPrimitive)]
#[repr(u16)]
#[rustfmt::skip]
#[allow(non_camel_case_types)]
pub enum TranslatedString {
% for idx, name in order.items():
${name} = ${idx},
% endfor
}
impl TranslatedString {
pub fn untranslated(self) -> &'static str {
match self {
% for name in order.values():
Self::${name} => ${en_strings.get(name, '""')},
% endfor
}
}
#[cfg(feature = "micropython")]
pub fn from_qstr(qstr: Qstr) -> Option<Self> {
match qstr {
% for name in order.values():
Qstr::MP_QSTR_${name} => Some(Self::${name}),
% endfor
_ => None,
}
}
}

@ -1,7 +1,11 @@
use crate::{
error::Error,
micropython::{
buffer::get_buffer,
ffi,
gc::Gc,
map::Map,
module::Module,
obj::{Obj, ObjBase},
qstr::Qstr,
typ::Type,
@ -9,42 +13,45 @@ use crate::{
},
};
use super::{get_language_name, tr};
extern "C" fn translate_attr_fn(_self_in: Obj, attr: ffi::qstr, dest: *mut Obj) {
let block = || {
let arg = unsafe { dest.read() };
if !arg.is_null() {
// Null destination would mean a `setattr`.
return Err(Error::TypeError);
}
let attr = Qstr::from_u16(attr as u16);
unsafe { dest.write(TR_OBJ.getattr(attr)?) };
Ok(())
};
unsafe { util::try_or_raise(block) }
}
use super::TranslatedString;
#[repr(C)]
pub struct TrObj {
base: ObjBase,
}
static TR_TYPE: Type = obj_type! {
name: Qstr::MP_QSTR_TR,
attr_fn: translate_attr_fn,
};
// SAFETY: We are in a single-threaded environment.
unsafe impl Sync for TrObj {}
fn translate(translation: TranslatedString) -> Result<Obj, Error> {
// SAFETY: TryFrom<&str> for Obj allocates a copy of the passed in string.
// The reference to flash data is discarded at the end of this function.
let stored_translations = unsafe { super::flash::get() };
translation.translate(stored_translations).try_into()
}
impl TrObj {
fn obj_type() -> &'static Type {
const fn obj_type() -> &'static Type {
static TR_TYPE: Type = obj_type! {
name: Qstr::MP_QSTR_TR,
attr_fn: TrObj::attr_fn,
};
&TR_TYPE
}
pub const fn singleton() -> &'static Self {
static TR_OBJ: TrObj = TrObj {
base: TrObj::obj_type().as_base(),
};
&TR_OBJ
}
fn getattr(&self, attr: Qstr) -> Result<Obj, Error> {
tr(attr.as_str()).try_into()
if let Some(translation) = TranslatedString::from_qstr(attr) {
Ok(translate(translation)?)
} else {
Err(Error::AttributeError(attr))
}
}
/// Convert TrObj to a MicroPython object
@ -57,19 +64,209 @@ impl TrObj {
}
}
/// Translations object callable from micropython.
pub static TR_OBJ: TrObj = TrObj {
base: TR_TYPE.as_base(),
};
// MicroPython interface
// SAFETY: Caller is supposed to be MicroPython, or copy MicroPython contracts
// about the meaning of arguments.
impl TrObj {
unsafe extern "C" fn attr_fn(_self_in: Obj, attr: ffi::qstr, dest: *mut Obj) {
let block = || {
let arg = unsafe { dest.read() };
if !arg.is_null() {
// Null destination would mean a `setattr`.
return Err(Error::TypeError);
}
let attr = Qstr::from_u16(attr as u16);
unsafe { dest.write(TrObj::singleton().getattr(attr)?) };
Ok(())
};
unsafe { util::try_or_raise(block) }
}
}
#[derive(Copy, Clone)]
enum HeaderSource {
Flash,
Gc(Obj),
}
impl HeaderSource {
pub fn from_gc(obj: Obj) -> Result<Self, Error> {
if !obj.is_bytes() {
return Err(Error::TypeError);
}
Ok(Self::Gc(obj))
}
// SAFETY: This is a convenience wrapper around TranslationsHeader::from_flash()
// and get_buffer() on the GC object. Safety parameters of those apply here,
// namely, we only guarantee validity of the returned data for a short
// timeframe (more precisely, until either MicroPython is allowed to touch
// the Gc variant, or until flash contents are updated with a new
// translations blob). In the Gc case, we also do not guarantee
// immutability. The caller should discard the result immediately after use.
pub unsafe fn header<'a>(self) -> Result<super::TranslationsHeader<'a>, Error> {
match self {
HeaderSource::Flash => unsafe { super::flash::get() }
.map(|blob| Ok(blob.header))
.unwrap_or(Err(value_error!("Translations blob not set"))),
HeaderSource::Gc(obj) => {
// get_buffer will succeed because we only accept bytes-like objects when
// constructing
super::TranslationsHeader::parse(unwrap!(unsafe { get_buffer(obj) }))
}
}
}
}
#[repr(C)]
pub struct TranslationsHeader {
base: ObjBase,
source: HeaderSource,
}
// SAFETY: We are in a single-threaded environment.
unsafe impl Sync for TranslationsHeader {}
/// Language name getter callable from micropython.
pub extern "C" fn language_name_obj() -> Obj {
let block = || {
if let Some(lang) = get_language_name() {
lang.try_into()
impl TranslationsHeader {
const fn obj_type() -> &'static Type {
static TYPE: Type = obj_type! {
name: Qstr::MP_QSTR_TranslationHeader,
make_new_fn: TranslationsHeader::make_new,
attr_fn: TranslationsHeader::attr_fn,
};
&TYPE
}
fn from_gc(obj: Obj) -> Result<Self, Error> {
Ok(Self {
base: Self::obj_type().as_base(),
source: HeaderSource::from_gc(obj)?,
})
}
const fn from_flash() -> &'static Self {
static OBJ: TranslationsHeader = TranslationsHeader {
base: TranslationsHeader::obj_type().as_base(),
source: HeaderSource::Flash,
};
&OBJ
}
pub const extern "C" fn obj_from_flash(_cls_in: Obj) -> Obj {
// SAFETY:
// - Self::from_flash returns a reference to an object in ROM which can't be
// mutated.
// - That object is a struct with a base and a type.
unsafe { Obj::from_ptr(Self::from_flash() as *const _ as *mut _) }
}
pub fn getattr(&self, attr: Qstr) -> Result<Obj, Error> {
// SAFETY: All data from the header is copied before returning.
let header = unsafe { self.source.header()? };
match attr {
Qstr::MP_QSTR_load_from_flash => Ok(obj_fn_1!(Self::obj_from_flash).as_obj()),
Qstr::MP_QSTR_language_name => header.language.try_into(),
Qstr::MP_QSTR_data_length => Ok(header.data_length.into()),
Qstr::MP_QSTR_change_language_title => header.change_language_title.try_into(),
Qstr::MP_QSTR_change_language_prompt => header.change_language_prompt.try_into(),
Qstr::MP_QSTR_version => {
let version = [header.version];
todo!()
}
_ => Err(Error::AttributeError(attr)),
}
}
}
// MicroPython interface
// SAFETY: Caller is supposed to be MicroPython, or uphold MicroPython contracts
// about the meaning of arguments.
impl TranslationsHeader {
pub unsafe extern "C" fn make_new(
typ: *const Type,
n_args: usize,
n_kw: usize,
args: *const Obj,
) -> Obj {
if n_args != 1 || n_kw != 0 || typ != Self::obj_type() {
return Error::TypeError.into_obj();
}
let block = |args: &[Obj], kwargs: &Map| {
assert!(args.len() == 1);
Gc::new(Self::from_gc(args[0])?).map(Into::into)
};
unsafe { util::try_with_args_and_kwargs_inline(n_args, n_kw, args, block) }
}
pub unsafe extern "C" fn attr_fn(_self_in: Obj, attr: ffi::qstr, dest: *mut Obj) {
let block = || {
let arg = unsafe { dest.read() };
if !arg.is_null() {
// Null destination would mean a `setattr`.
return Err(Error::TypeError);
}
let this = Gc::<Self>::try_from(_self_in)?;
let attr = Qstr::from_u16(attr as u16);
unsafe { dest.write(this.getattr(attr)?) };
Ok(())
};
unsafe { util::try_or_raise(block) }
}
}
impl From<Gc<TranslationsHeader>> for Obj {
fn from(value: Gc<TranslationsHeader>) -> Self {
// SAFETY:
// - `value` is an object struct with a base and a type.
// - `value` is GC-allocated.
unsafe { Obj::from_ptr(Gc::into_raw(value).cast()) }
}
}
impl TryFrom<Obj> for Gc<TranslationsHeader> {
type Error = Error;
fn try_from(value: Obj) -> Result<Self, Self::Error> {
if TranslationsHeader::obj_type().is_type_of(value) {
// SAFETY: We assume that if `value` is an object pointer with the correct type,
// it is managed by MicroPython GC (see `Gc::from_raw` for details).
let this = unsafe { Gc::from_raw(value.as_ptr().cast()) };
Ok(this)
} else {
Ok(Obj::const_none())
Err(Error::TypeError)
}
};
unsafe { util::try_or_raise(block) }
}
}
#[no_mangle]
#[rustfmt::skip]
pub static mp_module_trezortranslate: Module = obj_module! {
// TODO: add function to get all the translations keys in order
// - so that client can validate it is sending correct keys in correct order
/// class TranslationsHeader:
/// """Metadata about the translations blob."""
///
/// language_name: str
/// version: tuple[int, int, int, int]
/// change_language_title: str
/// change_language_prompt: str
/// header_length: int
/// data_length: int
///
/// def __init__(self, header_bytes: bytes) -> None:
/// """Parse header from bytes.
/// The header has variable length.
/// """
///
/// @staticmethod
/// def load_from_flash() -> TranslationsHeader | None:
/// """Load translations from flash."""
Qstr::MP_QSTR_TranslationsHeader => TranslationsHeader::obj_type().as_obj(),
/// from trezortranslate_keys import TR # noqa: F401
/// """Translation object with attributes."""
Qstr::MP_QSTR_TR => TrObj::singleton().as_obj(),
};

@ -1,50 +1,160 @@
mod en;
#[cfg(feature = "micropython")]
mod export;
mod general;
mod flash;
mod generated;
#[cfg(feature = "micropython")]
mod micropython;
mod translated_string;
use en::EN_TRANSLATIONS;
pub use translated_string::TranslatedString;
use crate::trezorhal::translations::{get_pointer_with_offset, get_translations_blob, PointerData};
use core::str;
use crate::{error::Error, io::InputStream};
use core::{ptr::null, str};
const TERMINATE_BYTE: u8 = 0xFF;
const ALIGNMENT_BYTE: u8 = 0x00;
const HEADER_LEN: usize = 256;
/// Translation function for Rust.
pub fn tr(key: &str) -> &'static str {
translate(key).unwrap_or_default()
}
// TODO: somehow make it a singleton object, so it is loaded just once
// TODO: split it into header.rs, translations.rs and font.rs?
// TODO: --- put the {en,cs,fr}.* files to `languages` dir?
// TODO: not generate fr/cs.rs files not to confuse, rather do .txt?
// TODO: add some tests?
struct TranslationsHeader {
#[repr(packed)]
struct OffsetEntry {
pub id: u16,
pub offset: u16,
}
struct Table<'a> {
offsets: &'a [OffsetEntry],
data: &'a [u8],
}
impl<'a> Table<'a> {
pub fn new(data: &'a [u8]) -> Result<Self, Error> {
let mut reader = crate::io::InputStream::new(data);
let count = reader.read_u16_le()?;
// The offsets table is (count + 1) entries long, the last entry is a sentinel.
let offsets_data =
reader.read((count + 1) as usize * core::mem::size_of::<OffsetEntry>())?;
// SAFETY: OffsetEntry is repr(packed) of two u16 values, so any four bytes are
// a valid OffsetEntry value.
let (_prefix, offsets, _suffix) = unsafe { offsets_data.align_to::<OffsetEntry>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(value_error!("Invalid offsets table"));
}
Ok(Self {
offsets,
data: reader.rest(),
})
}
pub fn get(self: &'a Self, id: u16) -> Option<&'a [u8]> {
self.offsets
.iter()
.position(|it| it.id == id)
.and_then(|idx| {
let start = self.offsets[idx].offset as usize;
let end = self.offsets[idx + 1].offset as usize;
self.data.get(start..end)
})
}
}
struct Translations<'a> {
pub header: TranslationsHeader<'a>,
translations: &'a [u8],
translation_offsets: &'a [u16],
fonts: Table<'a>,
}
impl<'a> Translations<'a> {
pub fn new(blob: &'a [u8]) -> Result<Self, Error> {
let header = TranslationsHeader::parse(&blob[..HEADER_LEN])?;
let data_len = header.data_length as usize;
if blob.len() < HEADER_LEN + data_len {
return Err(Error::EOFError);
}
let data = &blob[HEADER_LEN..HEADER_LEN + data_len];
if header.translations_length > data.len() as u16 {
return Err(value_error!("Invalid translations length field"));
}
let translations = &data[..header.translations_length as usize];
// length of offsets table is (count + 1) entries, the last one is a sentinel
let table_len = (header.translations_count + 1) as usize * core::mem::size_of::<u16>();
if table_len > translations.len() {
return Err(value_error!("Invalid translations table"));
}
// SAFETY: any bytes are valid u16 values, so casting any data to
// a sequence of u16 values is safe.
let (_prefix, translation_offsets, _suffix) =
unsafe { data[..table_len].align_to::<u16>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(value_error!("Invalid translations table"));
}
let fonts_data = &data[header.translations_length as usize..];
Ok(Self {
header,
translations,
translation_offsets,
fonts: Table::new(fonts_data)?,
})
}
/// Returns the translation at the given index.
pub fn translation(self, index: usize) -> Option<&'a str> {
if index >= self.translation_offsets.len() + 1 {
// The index is out of bounds.
// May happen when new firmware is using older translations and the string
// is not defined yet.
// Fallback to english.
return None;
}
let start_offset = self.translation_offsets[index] as usize;
let end_offset = self.translation_offsets[index + 1] as usize;
// Construct the relevant slice
let mut string = &self.translations[start_offset..end_offset];
if string.is_empty() {
// The string is not defined in the blob.
// May happen when old firmware is using newer translations and the string
// was deleted in the newer version.
// Fallback to english.
return None;
}
str::from_utf8(string).ok()
}
pub fn font(self, index: u16) -> Option<Table<'a>> {
self.fonts.get(index).and_then(|data| Table::new(data).ok())
}
}
struct TranslationsHeader<'a> {
/// Human readable language identifier (e.g. "cs" of "fr")
pub language: &'static str,
pub language: &'a str,
/// Version in format {major}.{minor}.{patch}
pub version: &'static str,
pub version: &'a str,
/// Overall length of the data blob (excluding the header)
pub data_length: u16,
/// Length of the translation data
pub translations_length: u16,
/// Number of translation items
pub translations_num: u16,
pub translations_count: u16,
/// Hash of the data blob (excluding the header)
pub data_hash: [u8; 32],
/// Title to show user before changing the language
pub change_language_title: &'static str,
pub change_language_title: &'a str,
/// Text to show user before changing the language
pub change_language_prompt: &'static str,
pub change_language_prompt: &'a str,
}
impl TranslationsHeader {
impl<'a> TranslationsHeader<'a> {
const MAGIC: [u8; 4] = [84, 82, 84, 82]; // b"TRTR"
const VERSION_LEN: usize = 16;
const LANG_LEN: usize = 32;
@ -53,366 +163,75 @@ impl TranslationsHeader {
const CHANGE_LANG_PROMPT_LEN: usize = 40;
const SIGNATURE_LEN: usize = 65;
pub fn from_flash() -> Result<Self, &'static str> {
let header_data = &get_translations_blob()[..HEADER_LEN];
Self::from_bytes(header_data)
fn read_fixedsize_str(reader: &'a InputStream<'a>, len: usize) -> Result<&'a str, Error> {
let bytes = reader.read(len)?;
let string = core::str::from_utf8(bytes)
.map_err(|_| value_error!("Invalid string"))?
.trim_end_matches(0 as char);
Ok(string)
}
pub fn from_bytes(data: &'static [u8]) -> Result<Self, &'static str> {
pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
if data.len() != HEADER_LEN {
return Err("Invalid header length");
return Err(value_error!("Invalid header length"));
}
let (magic, rest) = data.split_at(4);
let mut reader = crate::io::InputStream::new(data);
let magic = reader.read(4)?;
if magic != Self::MAGIC {
return Err("Invalid header magic");
return Err(value_error!("Invalid header magic"));
}
let (version, rest) = rest.split_at(Self::VERSION_LEN);
let version = core::str::from_utf8(version)
.or(Err("Invalid version string"))?
.trim_end_matches(0 as char);
let (language, rest) = rest.split_at(Self::LANG_LEN);
let language = core::str::from_utf8(language)
.or(Err("Invalid language string"))?
.trim_end_matches(0 as char);
let (data_length_bytes, rest) = rest.split_at(2);
let data_length = u16::from_le_bytes(
data_length_bytes
.try_into()
.or(Err("Invalid data length bytes"))?,
);
let (translations_length_bytes, rest) = rest.split_at(2);
let translations_length = u16::from_le_bytes(
translations_length_bytes
.try_into()
.or(Err("Invalid translations length bytes"))?,
);
let (translations_num_bytes, rest) = rest.split_at(2);
let translations_num = u16::from_le_bytes(
translations_num_bytes
.try_into()
.or(Err("Invalid translations num bytes"))?,
);
let (data_hash, rest) = rest.split_at(Self::DATA_HASH_LEN);
let data_hash: [u8; 32] = data_hash.try_into().or(Err("Invalid data hash length"))?;
let (change_language_title, rest) = rest.split_at(Self::CHANGE_LANG_TITLE_LEN);
let change_language_title = core::str::from_utf8(change_language_title)
.or(Err("Invalid change_language_title string"))?
.trim_end_matches(0 as char);
let (change_language_prompt, rest) = rest.split_at(Self::CHANGE_LANG_PROMPT_LEN);
let change_language_prompt = core::str::from_utf8(change_language_prompt)
.or(Err("Invalid change_language_prompt string"))?
.trim_end_matches(0 as char);
let non_sig_len = rest.len() as i32 - Self::SIGNATURE_LEN as i32;
if non_sig_len < 0 {
return Err("Not enough space for signature");
let version = Self::read_fixedsize_str(&mut reader, Self::VERSION_LEN)?;
let language = Self::read_fixedsize_str(&mut reader, Self::LANG_LEN)?;
let data_length = reader.read_u16_le()?;
let translations_length = reader.read_u16_le()?;
let translations_count = reader.read_u16_le()?;
let data_hash = unwrap!(reader.read(32)?.try_into());
let change_language_title =
Self::read_fixedsize_str(&mut reader, Self::CHANGE_LANG_TITLE_LEN)?;
let change_language_prompt =
Self::read_fixedsize_str(&mut reader, Self::CHANGE_LANG_PROMPT_LEN)?;
if translations_length > data_length {
return Err(value_error!("Invalid translations length field"));
}
let (non_sig_rest, _sig) = rest.split_at(non_sig_len as usize);
// Rest must be empty bytes
for byte in non_sig_rest {
if *byte != 0 {
return Err("Invalid header data");
}
let trailing_data = reader.rest();
if trailing_data.len() < Self::SIGNATURE_LEN {
return Err(value_error!("Invalid header signature"));
}
// Signature was already checked in micropython
// TODO check signature
Ok(Self {
language,
version,
data_length,
translations_length,
translations_num,
translations_count,
data_hash,
change_language_title,
change_language_prompt,
})
}
/// Starting offset for the font data.
pub fn font_start_offset(&self) -> u16 {
HEADER_LEN as u16 + self.translations_length
}
/// Offset for the last byte of the blob.
pub fn blob_end_offset(&self) -> u16 {
HEADER_LEN as u16 + self.data_length
}
/// Get the part of the blob dedicated to the translations.
pub fn get_all_translations_data(&self) -> &'static [u8] {
let start = HEADER_LEN;
let end = start + self.translations_length as usize;
&get_translations_blob()[start..end]
}
/// Get the offset table for the translations data.
pub fn get_translations_table(&self) -> OffsetTableData {
// Each entry has 2 bytes of offset
// Connection to a specific translation is carried by the index in the table
const ENTRY_SIZE: usize = 2;
let table_len = self.translations_num as usize * ENTRY_SIZE;
let start = HEADER_LEN;
let end = start + table_len;
let table_data = &get_translations_blob()[start..end];
OffsetTableData {
offset_table_data: table_data,
overall_len: self.translations_length,
}
}
/// Get the part of the blob dedicated to the fonts.
pub fn get_all_font_data(&self) -> &'static [u8] {
let start = self.font_start_offset() as usize;
let end = self.blob_end_offset() as usize;
&get_translations_blob()[start..end]
}
pub fn get_font_table(&self) -> OffsetTableData {
let all_font_data = self.get_all_font_data();
let data_size = all_font_data.len() as u16;
let (font_amount_bytes, data) = all_font_data.split_at(2);
let font_amount = u16::from_le_bytes(unwrap!(font_amount_bytes.try_into()));
// Each entry has 2 bytes of font-ID and 2 bytes of offset
const FONT_ENTRY_SIZE: usize = 4;
let pairs_len = font_amount as usize * FONT_ENTRY_SIZE;
let pairs_data = &data[..pairs_len];
OffsetTableData {
offset_table_data: pairs_data,
overall_len: data_size,
}
}
pub fn get_font_data_offset(&self, font_offset: u16, len: u16) -> &'static [u8] {
let start = font_offset as usize;
let end = start + len as usize;
&self.get_all_font_data()[start..end]
}
}
pub struct OffsetTableData {
/// Offset table raw data
pub offset_table_data: &'static [u8],
/// Overall length of the data (used for getting the length of the last
/// element in the table)
pub overall_len: u16,
}
/// Get the language name.
fn get_language_name() -> Option<&'static str> {
TranslationsHeader::from_flash()
.ok()
.map(|header| header.language)
}
/// Try to find the translation in flash (for a non-english language).
/// If not found, fallback to english.
fn translate(key: &str) -> Option<&'static str> {
let mut translation: Option<&'static str> = None;
if are_there_translations() {
if let Some(index) = EN_TRANSLATIONS.get_position(key) {
translation = get_translation_by_index(index);
}
}
if translation.is_none() {
translation = EN_TRANSLATIONS.get_text(key);
}
translation
}
/// Quickly checks whether there are some valid translations data
fn are_there_translations() -> bool {
get_translations_blob()[0] != TERMINATE_BYTE
}
/// Returns the translation at the given index.
fn get_translation_by_index(index: usize) -> Option<&'static str> {
// TODO: do not use unwrap, probably return None in case of Err?
// --- not to introduce panic in case of corrupted data in flash
let header = unwrap!(TranslationsHeader::from_flash());
let translations_table = header.get_translations_table();
let offsets = read_u16_list(translations_table.offset_table_data);
// The index is out of bounds.
// May happen when new firmware is using older translations and the string
// is not defined yet.
// Fallback to english.
if index > offsets.len() {
return None;
}
// Identifying where the string starts.
let start_offset = offsets[index];
// In case the string is last, we may need to strip the alignment bytes
let mut strip_alignment_bytes = false;
// Getting the data length - difference between the current offset and the next
// one.
let len = if index == offsets.len() - 1 {
// The last element in the table
strip_alignment_bytes = true;
translations_table.overall_len - start_offset
} else {
offsets[index + 1] - start_offset
};
// The string is not defined in the blob.
// May happen when old firmware is using newer translations and the string
// was deleted in the newer version.
// Fallback to english.
if len == 0 {
return None;
}
let start = start_offset as usize;
let mut end = start + len as usize;
let translations_data = header.get_all_translations_data();
if strip_alignment_bytes {
// Strip possible alignment bytes at the end
while translations_data[end - 1] == ALIGNMENT_BYTE {
end -= 1;
}
}
let data = &translations_data[start..end];
str::from_utf8(data).ok()
}
/// Given blob data returns a list of (u16, u16) values.
fn read_u16_pairs_list(bytes: &'static [u8]) -> &'static [(u16, u16)] {
let (prefix, data, suffix) = unsafe { bytes.align_to::<(u16, u16)>() };
if !prefix.is_empty() || !suffix.is_empty() {
return &[];
}
data
}
/// Given blob data returns a list of u16 values.
fn read_u16_list(bytes: &'static [u8]) -> &'static [u16] {
let (prefix, data, suffix) = unsafe { bytes.align_to::<u16>() };
if !prefix.is_empty() || !suffix.is_empty() {
return &[];
}
data
}
pub struct OffsetLen {
pub offset: u16,
pub len: u16,
}
/// Information about specific font - where it starts and how long it is
fn get_font_offset_from_id(font_id: u16) -> Option<OffsetLen> {
// TODO: do not use unwrap?
let font_table = unwrap!(TranslationsHeader::from_flash()).get_font_table();
match_first_u16_pair(
font_id,
font_table.offset_table_data,
font_table.overall_len,
)
}
/// Returns the offset and length of the u16 pair whose first element matches
/// `code_id`.
fn match_first_u16_pair(
code_id: u16,
pairs_data: &'static [u8],
overall_len: u16,
) -> Option<OffsetLen> {
// To get the length of the font, need to read the offset of the next font
let mut font_offset: Option<u16> = None;
let mut next_offset: Option<u16> = None;
for &(code, offset) in read_u16_pairs_list(pairs_data) {
if code == code_id {
font_offset = Some(offset);
} else if font_offset.is_some() {
next_offset = Some(offset);
break;
}
}
if let Some(offset) = font_offset {
// When our font was last, there is no next offset - use the data size
let next = next_offset.unwrap_or(overall_len);
let len = next - offset;
return Some(OffsetLen { offset, len });
}
None
}
// Get information about a given glyph with a given font-offset
fn get_glyph_data(char_code: u16, font_offset: u16, font_len: u16) -> Option<OffsetLen> {
// TODO: do not use unwrap?
let font_data =
unwrap!(TranslationsHeader::from_flash()).get_font_data_offset(font_offset, font_len);
let data_size = font_data.len() as u16;
let (glyph_amount_bytes, data) = font_data.split_at(2);
let glyph_amount = u16::from_le_bytes(unwrap!(glyph_amount_bytes.try_into()));
// Each entry has 2 bytes of glyph-code and 2 bytes of offset
const GLYPH_ENTRY_SIZE: usize = 4;
let pairs_len = glyph_amount as usize * GLYPH_ENTRY_SIZE;
let pairs_data = &data[..pairs_len];
match_first_u16_pair(char_code, pairs_data, data_size)
}
/// Get information about the glyph in the given font.
/// First finding out the offset of the font within the translations data.
/// Then finding out the offset of the glyph within the font.
/// Returning the absolute offset of the glyph within the translations data -
/// together with its length.
fn get_glyph_font_data(char_code: u16, font_id: u16) -> Option<OffsetLen> {
if !are_there_translations() {
return None;
}
if let Some(font_offset) = get_font_offset_from_id(font_id) {
if let Some(glyph_data) = get_glyph_data(char_code, font_offset.offset, font_offset.len) {
// TODO: do not use unwrap?
let font_start_offset = unwrap!(TranslationsHeader::from_flash()).font_start_offset();
let final_offset = font_start_offset + font_offset.offset + glyph_data.offset;
return Some(OffsetLen {
offset: final_offset,
len: glyph_data.len,
});
}
}
None
}
#[no_mangle]
pub extern "C" fn get_utf8_glyph(char_code: cty::uint16_t, font: cty::c_int) -> PointerData {
pub unsafe extern "C" fn get_utf8_glyph(codepoint: cty::uint16_t, font: cty::c_int) -> *const u8 {
// C will send a negative number
let font_abs = font.unsigned_abs() as u16;
if let Some(glyph_data) = get_glyph_font_data(char_code, font_abs) {
return get_pointer_with_offset(glyph_data.offset, glyph_data.len);
}
PointerData {
ptr: core::ptr::null(),
len: 0,
// SAFETY:
// We rely on the fact that the caller (`display_text_render` and friends) uses and
// discards this data immediately.
let Some(tr) = (unsafe { flash::get() }) else {
return null();
};
if let Some(glyph) = tr.font(font_abs).and_then(|t| t.get(codepoint)) {
glyph.as_ptr()
} else {
null()
}
}

@ -0,0 +1,10 @@
pub use super::generated::translated_string::TranslatedString;
use super::Translations;
impl TranslatedString {
pub fn translate<'a>(self, source: Option<&'a Translations>) -> &'a str {
source
.and_then(|s| s.translation(self as _))
.unwrap_or(self.untranslated())
}
}
Loading…
Cancel
Save