/*- * Copyright (c) 2000, 2001 Citrus Project, * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * */ #include #include #include #include "i18n.h" #include "mo.h" #include "kernel/event_loop.h" #include "kernel/pbl_malloc.h" #include "resource/resource.h" #include "services/normal/filesystem/pfs.h" #include "shell/normal/language_ui.h" #include "shell/prefs.h" #include "system/logging.h" #include "system/passert.h" #include "util/list.h" ////////////////////////////////////////////////////// // See mo.h for a description of the MO file format // ////////////////////////////////////////////////////// typedef struct { uint32_t hash; const char *string; const void *owner; } StringLookupInfo; static struct DomainBinding { uint32_t resource_id; ResourceCallbackHandle watch_handle; bool need_reload; ResourceVersion version; MoHandle mohandle; I18nString *strings_list; char iso_locale[ISO_LOCALE_LENGTH]; char lang_name[LOCALE_NAME_LENGTH]; uint16_t lang_version; } s_system_domain; static void prv_list_flush(void); /////////////////////////////////////////////////// // MO File Hash Table uint32_t prv_gettext_hash(const char *str) { const uint8_t *p; uint32_t hash = 0, tmp; for (p = (const uint8_t *)str; *p; p++) { hash <<= 4; hash += *p; tmp = hash & 0xF0000000; if (tmp != 0) { hash ^= tmp; hash ^= tmp >> 24; } } return hash; } static uint32_t prv_collision_step(uint32_t hashval, uint32_t hashsize) { return (hashval % (hashsize - 2)) + 1; } static uint32_t prv_next_index(uint32_t curidx, uint32_t hashsize, uint32_t step) { return curidx + step - (curidx >= hashsize - step ? hashsize : 0); } //! Lookup a translated string. //! @param rlen[out] Can be NULL. If non-null will be populated with the length of the translated //! string. //! @param rstring[out] Can be NULL. If non-null this buffer will be populated with the translated //! string. This buffer will be null-terminated. //! @param rstring_len The length of the rstring buffer. static void prv_lookup(const char *msgid, struct DomainBinding *db, size_t *rlen, char *rstring, size_t rstring_len) { MoHandle *mohandle = &db->mohandle; *rlen = 0; if (mohandle->mo.hdr.mo_hsize <= 2 || mohandle->mo.mo_htable == NULL) { return; } uint32_t hashval = prv_gettext_hash(msgid); uint32_t step = prv_collision_step(hashval, mohandle->mo.hdr.mo_hsize); uint32_t idx = hashval % mohandle->mo.hdr.mo_hsize; size_t len = strlen(msgid); while (1) { uint32_t strno = mohandle->mo.mo_htable[idx]; if (strno-- == 0) { /* unexpected miss */ return; } MoEntry oentry; if (resource_load_byte_range_system(0, db->resource_id, mohandle->mo.hdr.mo_otable + sizeof(MoEntry) * strno, (uint8_t *)&oentry, sizeof(MoEntry)) != sizeof(MoEntry)) { return; } if (len == oentry.len) { // Length of original matches, compare the contents char key[oentry.len + 1]; if (resource_load_byte_range_system(0, db->resource_id, oentry.off, (uint8_t *)key, oentry.len) != oentry.len) { return; } key[oentry.len] = '\0'; if (!strcmp(msgid, key)) { // Contents of original string matches, get the translated string MoEntry tentry; if (resource_load_byte_range_system(0, db->resource_id, mohandle->mo.hdr.mo_ttable + sizeof(MoEntry) * strno, (uint8_t *)&tentry, sizeof(MoEntry)) != sizeof(MoEntry)) { return; } if (rstring) { // If we want the translated string, copy it out. // Make sure we don't read out more than the length of the buffer we're reading into. // Leave space for the null-terminator as well. const size_t read_length = MIN(tentry.len, rstring_len - 1); if (resource_load_byte_range_system(0, db->resource_id, tentry.off, (uint8_t *)rstring, read_length) != read_length) { return; } rstring[read_length] = '\0'; } if (rlen) { // If we want the translated string length, copy it out. *rlen = tentry.len; } return; } } idx = prv_next_index(idx, mohandle->mo.hdr.mo_hsize, step); } } /////////////////////////////////////////////////// // MO File Mapping & Lookup static bool prv_get_property(const char *header, const char *name, char *buffer, size_t size) { // Isolate the language name char *str = strstr(header, name); if (str == NULL) { // strstr failed return false; } str += strlen(name); char *end = strchr(str, '\n'); unsigned int length = end - str; if (end == NULL || length > size) { // strchr failed return false; } memcpy(buffer, str, length); buffer[length] = '\0'; return true; } static bool prv_get_metadata(struct DomainBinding *db) { const size_t HEADER_BUFFER_SIZE = 400; // malloc a comfortable amount of RAM to save the header in char *header = kernel_malloc_check(HEADER_BUFFER_SIZE); size_t header_len = 0; bool success = false; // all metadata is in the "" header entry prv_lookup("", db, &header_len, header, HEADER_BUFFER_SIZE); if (!header_len) { PBL_LOG(LOG_LEVEL_WARNING, "Could not find header in language pack"); goto cleanup; } // Isolate the language substring if (!prv_get_property(header, "Language: ", db->iso_locale, ISO_LOCALE_LENGTH)) { PBL_LOG(LOG_LEVEL_WARNING, "Could not parse a language from language pack"); goto cleanup; } // Isolate the language name if (!prv_get_property(header, "Name: ", db->lang_name, LOCALE_NAME_LENGTH)) { strcpy(db->lang_name, "Unknown"); } // Isolate the version value char version_str[10] = {0}; if (!prv_get_property(header, "Project-Id-Version: ", version_str, 10)) { PBL_LOG(LOG_LEVEL_WARNING, "Could not parse a version from language pack"); goto cleanup; } char *version_end; db->lang_version = strtol(version_str, &version_end, 0); if (version_end == version_str) { PBL_LOG(LOG_LEVEL_WARNING, "Could not parse a version from language pack"); goto cleanup; } success = true; PBL_LOG(LOG_LEVEL_INFO, "language: %s, version %d", db->iso_locale, db->lang_version); cleanup: kernel_free(header); return (success); } static int prv_unmapit(struct DomainBinding *db) { MoHandle *mohandle = &db->mohandle; kernel_free(mohandle->mo.mo_htable); mohandle->mo.mo_htable = NULL; mohandle->mo = (Mo){}; strcpy(db->iso_locale, "en_US"); strcpy(db->lang_name, "English"); db->lang_version = 1; return 0; } static bool prv_mapit(const uint32_t resource_id, struct DomainBinding *db) { // If the resource is changed on disk, our resource_watch callback will set need_reload if (!db->need_reload) { return (db->version.crc != 0); } PBL_LOG(LOG_LEVEL_DEBUG, "New language detected!"); db->need_reload = false; /* save version */ db->version = resource_get_version(SYSTEM_APP, resource_id); prv_list_flush(); prv_unmapit(db); unsigned int size; if ((size = resource_size(SYSTEM_APP, resource_id)) < sizeof(MoHeader)) { goto fail; } if (!resource_is_valid(SYSTEM_APP, resource_id)) { goto fail; } MoHandle *mohandle = &db->mohandle; if (resource_load_byte_range_system(SYSTEM_APP, resource_id, 0, (uint8_t *)&mohandle->mo.hdr, sizeof(MoHeader)) == 0) { goto fail; } if (mohandle->mo.hdr.mo_magic != MO_MAGIC) { goto fail; } mohandle->len = size; /* validate htable */ if (mohandle->mo.hdr.mo_hsize < 2) { goto fail; } size_t htable_size = sizeof(uint32_t) * mohandle->mo.hdr.mo_hsize; uint32_t *htable = kernel_malloc_check(htable_size); mohandle->mo.mo_htable = htable; if (resource_load_byte_range_system(SYSTEM_APP, resource_id, mohandle->mo.hdr.mo_hoffset, (uint8_t *)htable, htable_size) == 0) { prv_unmapit(db); goto fail; } for (unsigned int i = 0; i < mohandle->mo.hdr.mo_hsize; ++i) { if (htable[i] > mohandle->mo.hdr.mo_nstring) { /* illegal string number */ prv_unmapit(db); goto fail; } } if (!prv_get_metadata(db)) { prv_unmapit(db); goto fail; } return true; fail: return false; } /////////////////////////////////////////////////// // Strings List Manipulation void prv_list_flush(void) { ListNode *cur = (ListNode *)s_system_domain.strings_list; while (cur) { ListNode *next = list_get_next(cur); kernel_free(cur); cur = next; } s_system_domain.strings_list = NULL; } static bool prv_list_string_filter_callback(ListNode *found_node, void *data) { I18nString *i18n_string = (I18nString *)found_node; StringLookupInfo *lookup_info = data; if (i18n_string->original_hash == lookup_info->hash && lookup_info->owner == i18n_string->owner && strcmp(i18n_string->original_string, lookup_info->string) == 0) { return true; } else { return false; } } static bool prv_list_owner_filter_callback(ListNode *found_node, void *owner) { I18nString *i18n_string = (I18nString *)found_node; if (i18n_string->owner == owner) { return true; } else { return false; } } // Not static because we call this from unit test code I18nString *prv_list_find_string(const char *string, const void *owner) { StringLookupInfo lookup_info = { .string = string, .hash = prv_gettext_hash(string), .owner = owner }; return (I18nString *)list_find((ListNode *)s_system_domain.strings_list, prv_list_string_filter_callback, (void *)&lookup_info); } static const char *prv_list_add_string(const char *original_string, const char *translated_string, const void *owner) { uint32_t translated_len = strlen(translated_string); // Allocate enough space to hold the original and translated strings. The translated string // is stored at i18n_string->translated and the original string immediately after that. I18nString *i18n_string = kernel_malloc_check(sizeof(I18nString) + translated_len + 1 + strlen(original_string) + 1); list_init(&i18n_string->node); i18n_string->owner = owner; strcpy(i18n_string->translated_string, translated_string); i18n_string->original_hash = prv_gettext_hash(original_string); // Store the original string immediately after the translated one in memory. i18n_string->original_string = &i18n_string->translated_string[translated_len + 1]; strcpy(i18n_string->original_string, original_string); I18nString **strings_list = &s_system_domain.strings_list; *strings_list = (I18nString *)list_prepend((ListNode *)*strings_list, &i18n_string->node); if (translated_len > 0) { return (i18n_string->translated_string); } else { return original_string; } } static void prv_list_remove_string(I18nString *i18n_string) { list_remove(&i18n_string->node, (ListNode **)&s_system_domain.strings_list, NULL); kernel_free(i18n_string); } static bool prv_check_domain(struct DomainBinding *db) { return (prv_mapit(s_system_domain.resource_id, db)); } static const char *prv_message_from_msgid(const char *msgid) { // If a string wasn't found, we want to return the original string. // However, if we have a context, this string needs to not show the context. // So we just find EOT and if it's present return the next character. const char *message = strchr(msgid, '\4'); if (message == NULL) { // No context, the whole string is the message. return msgid; } // strchr gets the address of that character. We want to skip the EOT, so +1. return message + 1; } /////////////////////////////////////////////////// // i18n API // NOTE: Currently, we don't do reference counting, so bad things will happen if the caller // calls i18n_get() on the same string more than once and assumes that any of those return // pointers will still be valid after i18n_free() is called on one of them. const char *i18n_get(const char *msgid, const void *owner) { PBL_ASSERTN(owner); if (msgid == NULL || msgid[0] == 0) { goto fail; } struct DomainBinding *db = &s_system_domain; if (!prv_check_domain(db)) { goto fail; } // See if this original has been cached. I18nString *i18n_string = prv_list_find_string(msgid, owner); if (i18n_string) { if (i18n_string->translated_string[0]) { return i18n_string->translated_string; } else { // No translation exists for this string, return original goto fail; } } // Lookup the translation from the language pack and add it to our cache char translated[200]; size_t len = 0; prv_lookup(msgid, db, &len, translated, sizeof(translated)); if (len >= sizeof(translated)) { PBL_LOG(LOG_LEVEL_WARNING, "Truncated string: <%s>", msgid); } if (len) { return prv_list_add_string(msgid, translated, owner); } else { // Add to cache as an untranslatable string so we don't waste time looking for it again. prv_list_add_string(msgid, (const char *)"", owner); } fail: // String not found or an error occurred. return prv_message_from_msgid(msgid); } void i18n_get_with_buffer(const char *msgid, char *buffer, size_t length) { if (msgid == NULL || msgid[0] == 0) { goto fail; } struct DomainBinding *db = &s_system_domain; if (!prv_check_domain(db)) { goto fail; } size_t len = 0; prv_lookup(msgid, db, &len, buffer, length); if (len >= length) { PBL_LOG(LOG_LEVEL_WARNING, "Truncated string: <%s>", msgid); } if (len) { // buffer has been written, return return; } fail: msgid = prv_message_from_msgid(msgid); strncpy(buffer, msgid, length); buffer[length - 1] = '\0'; } size_t i18n_get_length(const char *msgid) { if (msgid == NULL || msgid[0] == 0) { return 0; } struct DomainBinding *db = &s_system_domain; if (!prv_check_domain(db)) { goto fail; } size_t len = 0; prv_lookup(msgid, db, &len, NULL, 0); if (len) { // String was found return len; } fail: // String not found, or error occurred msgid = prv_message_from_msgid(msgid); return strlen(msgid); } void i18n_free(const char *original, const void *owner) { PBL_ASSERTN(owner); I18nString *i18n_string = prv_list_find_string(original, owner); if (i18n_string) { prv_list_remove_string(i18n_string); } } void i18n_free_all(const void *owner) { I18nString *cur_string = (I18nString *)list_find((ListNode *)s_system_domain.strings_list, prv_list_owner_filter_callback, (void*)owner); while (cur_string) { I18nString *next_string = (I18nString *)list_find_next(&cur_string->node, prv_list_owner_filter_callback, false, (void*)owner); prv_list_remove_string(cur_string); cur_string = next_string; } } static void prv_resource_changed_handler(void *data) { struct DomainBinding *db = (struct DomainBinding *)data; // Mark as invalid PBL_LOG(LOG_LEVEL_DEBUG, "lang resource file reloading"); shell_prefs_set_language_english(false); db->need_reload = true; if (resource_is_valid(SYSTEM_APP, db->resource_id)) { language_ui_display_changed(db->lang_name); } } static void prv_resource_changed_callback(void *data) { // We want to not actually handle the reload here, because the PFS lock is still held here. // So instead we throw in the reload as an event callback. PBL_LOG(LOG_LEVEL_DEBUG, "lang resource file was modified"); launcher_task_add_callback(prv_resource_changed_handler, data); } static void prv_unset(void) { s_system_domain.need_reload = false; prv_list_flush(); prv_unmapit(&s_system_domain); } void i18n_set_resource(uint32_t resource_id) { // Remove prior watch, if any // Warning: you better be sure we're not calling from the resource changed callback. if (s_system_domain.watch_handle) { resource_unwatch(s_system_domain.watch_handle); } s_system_domain.resource_id = resource_id; s_system_domain.watch_handle = resource_watch(SYSTEM_APP, resource_id, prv_resource_changed_callback, &s_system_domain); if (shell_prefs_get_language_english()) { prv_unset(); return; } s_system_domain.need_reload = true; // try mapping it right away prv_mapit(resource_id, &s_system_domain); } char *i18n_get_locale(void) { return (s_system_domain.iso_locale); } uint16_t i18n_get_version(void) { return (s_system_domain.lang_version); } char *i18n_get_lang_name(void) { return (s_system_domain.lang_name); } void i18n_enable(bool enable) { if (enable) { s_system_domain.need_reload = true; prv_mapit(s_system_domain.resource_id, &s_system_domain); } else { prv_unset(); } } void command_i18n_resource(const char *arg) { uint32_t resource_id = atoi(arg); i18n_set_resource(resource_id); }