mirror of
https://github.com/google/pebble.git
synced 2025-11-24 16:22:25 -05:00
266 lines
9.5 KiB
C
266 lines
9.5 KiB
C
/*
|
|
* Copyright 2024 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "timezone_database.h"
|
|
|
|
#include "resource/resource.h"
|
|
#include "resource/resource_ids.auto.h"
|
|
#include "services/common/clock.h"
|
|
#include "system/passert.h"
|
|
|
|
#include <util/attributes.h>
|
|
#include <util/size.h>
|
|
|
|
#include <string.h>
|
|
|
|
// The format of the database is as follows
|
|
// Header
|
|
// 2 bytes - Region count
|
|
// 2 bytes - DST Rule count
|
|
// 2 bytes - Link count
|
|
// Regions
|
|
// For each region (24 bytes):
|
|
// 1 byte - Continent index, @see CONTINENT_NAMES
|
|
// 15 bytes - City name
|
|
// 2 bytes - GMT offset in minutes as a int16_t
|
|
// 5 bytes - Timezone name abbreviation (aka tz_abbr)
|
|
// 1 byte - DST Rule ID
|
|
// DST Rules
|
|
// For each DST ID (16 bytes)
|
|
// For each rule in the pair, first the start rule followed by the end rule (8 bytes)
|
|
// @see TimezoneDSTRule for the structure
|
|
// Links
|
|
// For each link (35 bytes)
|
|
// 2 bytes - The region id this link maps to
|
|
// 33 bytes - The name of the link that should be treated as an alias to the linked region
|
|
|
|
typedef struct PACKED {
|
|
uint16_t region_count;
|
|
uint16_t dst_rule_count;
|
|
uint16_t link_count;
|
|
} TimezoneDatabaseFlashHeader;
|
|
#define TZDATA_HEADER_BYTES (sizeof(TimezoneDatabaseFlashHeader))
|
|
|
|
#define TIMEZONE_CITY_LENGTH 15 // maximum length of the city name in timezone database
|
|
#define REGION_BYTES (1 + TIMEZONE_CITY_LENGTH + 2 + 5 + 1)
|
|
|
|
#define DST_RULE_BYTES (sizeof(TimezoneDSTRule))
|
|
#define DST_RULE_PAIR_BYTES (DST_RULE_BYTES * 2)
|
|
|
|
#define LINK_REGION_LENGTH 2
|
|
#define LINK_NAME_LENGTH 33
|
|
#define LINK_BYTES (LINK_REGION_LENGTH + LINK_NAME_LENGTH)
|
|
|
|
|
|
//! Names for all the continents we support. The timezone database stores continents as indexes
|
|
//! into this constant array.
|
|
const char * const CONTINENT_NAMES[] = { "Africa",
|
|
"America",
|
|
"Antarctica",
|
|
"Asia",
|
|
"Atlantic",
|
|
"Australia",
|
|
"Europe",
|
|
"Indian",
|
|
"Pacific",
|
|
"Etc"};
|
|
|
|
|
|
//! Helper function to curry out some common arguments to the resource reads in this file.
|
|
static bool prv_database_read(uint32_t offset, void *data, size_t num_bytes) {
|
|
return resource_load_byte_range_system(SYSTEM_APP, RESOURCE_ID_TIMEZONE_DATABASE,
|
|
offset, data, num_bytes) == num_bytes;
|
|
}
|
|
|
|
//! Note! This count includes rule 0 which isn't actually stored in the database.
|
|
static int prv_get_dst_rule_count(void) {
|
|
uint16_t dst_rule_count;
|
|
prv_database_read(offsetof(TimezoneDatabaseFlashHeader, dst_rule_count),
|
|
&dst_rule_count, sizeof(dst_rule_count));
|
|
return dst_rule_count;
|
|
}
|
|
|
|
static int prv_get_link_count(void) {
|
|
uint16_t link_count;
|
|
prv_database_read(offsetof(TimezoneDatabaseFlashHeader, link_count),
|
|
&link_count, sizeof(link_count));
|
|
return link_count;
|
|
}
|
|
|
|
int timezone_database_get_region_count(void) {
|
|
uint16_t region_count;
|
|
prv_database_read(offsetof(TimezoneDatabaseFlashHeader, region_count),
|
|
®ion_count, sizeof(region_count));
|
|
return region_count;
|
|
}
|
|
|
|
bool timezone_database_load_region_info(uint16_t region_id, TimezoneInfo *tz_info) {
|
|
const int region_offset =
|
|
// Skip over the region count
|
|
TZDATA_HEADER_BYTES +
|
|
// Skip over the regions list
|
|
(region_id * REGION_BYTES);
|
|
|
|
//! Struct for reading data from a raw database of timezone information
|
|
struct PACKED {
|
|
int16_t gmt_offset_minutes; //!< timezone offset from UTC time (in minutes)
|
|
char tz_abbr[TZ_LEN - 1]; //!< timezone abbreviation (without terminating nul)
|
|
int8_t dst_id; //!< daylight savings time index identifier
|
|
} tz_data;
|
|
|
|
// Load the timezone information for the region_id, excluding the country + city_name itself
|
|
if (!prv_database_read(region_offset + 1 + TIMEZONE_CITY_LENGTH, &tz_data, sizeof(tz_data))) {
|
|
*tz_info = (TimezoneInfo) { .dst_id = 0 };
|
|
return false;
|
|
}
|
|
|
|
*tz_info = (TimezoneInfo) {
|
|
.dst_id = tz_data.dst_id,
|
|
.timezone_id = region_id,
|
|
.tm_gmtoff = tz_data.gmt_offset_minutes * SECONDS_PER_MINUTE,
|
|
// Leave the dst_start and dst_end timestamps uninitialized
|
|
.dst_start = 0,
|
|
.dst_end = 0
|
|
};
|
|
memcpy(tz_info->tm_zone, tz_data.tz_abbr, sizeof(tz_data.tz_abbr));
|
|
|
|
return true;
|
|
}
|
|
|
|
bool timezone_database_load_region_name(uint16_t region_id, char *region_name) {
|
|
if (region_id > timezone_database_get_region_count()) {
|
|
return false;
|
|
}
|
|
|
|
const int region_offset =
|
|
// Skip over the region count
|
|
TZDATA_HEADER_BYTES +
|
|
// Skip over the regions list
|
|
(region_id * REGION_BYTES);
|
|
|
|
// Read the continent index, which is the first byte
|
|
uint8_t continent_index = 0;
|
|
prv_database_read(region_offset, &continent_index, sizeof(continent_index));
|
|
PBL_ASSERTN(continent_index < ARRAY_LENGTH(CONTINENT_NAMES));
|
|
|
|
// Copy the continent name into our buffer, followed by a slash.
|
|
const int continent_name_length = strlen(CONTINENT_NAMES[continent_index]);
|
|
memcpy(region_name, CONTINENT_NAMES[continent_index], continent_name_length);
|
|
region_name[continent_name_length] = '/';
|
|
|
|
char *city_name = region_name + continent_name_length + 1 /* slash */;
|
|
|
|
// How many bytes left we have of our buffer to fill in the city information.
|
|
const int remaining_size = (region_name + TIMEZONE_NAME_LENGTH) - city_name;
|
|
|
|
// Fill the rest of our buffer with city name.
|
|
// Our generation script will ensure that continent + slash + city name + null will always
|
|
// fit in our buffer with a null terminator to spare.
|
|
prv_database_read(region_offset + 1 /* continent index */, city_name, remaining_size);
|
|
|
|
// FIXME: Perhaps we should refactor this to do one read instead of two? The information is
|
|
// right beside each other and it's pretty wasteful to do a 1 byte read followed by a 15 byte
|
|
// read as separate reads.
|
|
|
|
return true;
|
|
}
|
|
|
|
bool timezone_database_load_dst_rule(uint8_t dst_id, TimezoneDSTRule *start, TimezoneDSTRule *end) {
|
|
const int region_count = timezone_database_get_region_count();
|
|
|
|
const int dst_rule_pair_offset =
|
|
// Skip over the region count
|
|
TZDATA_HEADER_BYTES +
|
|
// Skip over the regions list
|
|
(region_count * REGION_BYTES) +
|
|
// Find the appropriate DST zone (DST ID is 1 indexed)
|
|
((dst_id - 1) * DST_RULE_PAIR_BYTES);
|
|
|
|
// First half of the DST rule pair
|
|
if (!prv_database_read(dst_rule_pair_offset, start, DST_RULE_BYTES) ||
|
|
!prv_database_read(dst_rule_pair_offset + DST_RULE_BYTES, end, DST_RULE_BYTES)) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Failed to load timezone for DST ID %"PRIu8, dst_id);
|
|
return false;
|
|
}
|
|
|
|
if (start->ds_label == '\0' || end->ds_label == '\0') {
|
|
// Does not observe DST
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int prv_search_regions_by_name(const char *region_name, int region_name_length) {
|
|
const int region_count = timezone_database_get_region_count();
|
|
|
|
for (int i = 0; i < region_count; i++) {
|
|
char lookup_region_name[TIMEZONE_NAME_LENGTH];
|
|
timezone_database_load_region_name(i, lookup_region_name);
|
|
if (strncmp(region_name, lookup_region_name, region_name_length) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int prv_search_links_by_name(const char *region_name, int region_name_length) {
|
|
char name_asciz[256] = {0};
|
|
memcpy(name_asciz, region_name, region_name_length);
|
|
|
|
const int link_section_offset =
|
|
// Skip over the region count
|
|
TZDATA_HEADER_BYTES +
|
|
// Skip over the regions list
|
|
(timezone_database_get_region_count() * REGION_BYTES) +
|
|
// Skip over the DST list
|
|
((prv_get_dst_rule_count() - 1) * DST_RULE_PAIR_BYTES);
|
|
|
|
const uint16_t link_count = prv_get_link_count();
|
|
|
|
for (int i = 0; i < link_count; i++) {
|
|
const int link_offset = link_section_offset + (i * LINK_BYTES);
|
|
|
|
char link_name[LINK_NAME_LENGTH + 1]; // + max length + null terminator
|
|
prv_database_read(link_offset + LINK_REGION_LENGTH, link_name, LINK_NAME_LENGTH);
|
|
link_name[sizeof(link_name) - 1] = '\0';
|
|
|
|
if (strncmp(name_asciz, link_name, LINK_NAME_LENGTH) == 0) {
|
|
// Found it!
|
|
uint16_t linked_region_id;
|
|
prv_database_read(link_offset, &linked_region_id, sizeof(linked_region_id));
|
|
return linked_region_id;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
int timezone_database_find_region_by_name(const char *region_name, int region_name_length) {
|
|
int region_id = prv_search_regions_by_name(region_name, region_name_length);
|
|
|
|
if (region_id == -1) {
|
|
// Might be a Link, let's check.
|
|
// To explain: iOS, when not synchronized from the internet, uses _ancient_ IANA region names.
|
|
// For example, when in California, iOS will send "US/Pacific" which hasn't been the name of
|
|
// that timezone since 1993. So we need to support linked timezones sent from the phone.
|
|
region_id = prv_search_links_by_name(region_name, region_name_length);
|
|
}
|
|
|
|
return region_id;
|
|
}
|