mirror of
https://github.com/google/pebble.git
synced 2025-03-15 08:41:21 +00:00
456 lines
17 KiB
Python
Executable file
456 lines
17 KiB
Python
Executable file
#!/usr/bin/python
|
|
# 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.
|
|
|
|
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import string
|
|
import struct
|
|
import sys
|
|
from collections import namedtuple
|
|
|
|
# dst-rules are formatted in 2 forms
|
|
# Jordan Oct lastFri 0:00 -
|
|
# US Mar Sun>=8 2:00 D
|
|
# so need starting day_of_month (mday), direction (increment or decrement)
|
|
# as well as dst_start or dst_end (D or S)
|
|
dstrule = namedtuple('dstrule', 'dstzone ds month wday flag mday hour minute')
|
|
|
|
tz_continent_list = ("Africa",
|
|
"America",
|
|
"Antarctica",
|
|
"Asia",
|
|
"Atlantic",
|
|
"Australia",
|
|
"Europe",
|
|
"Indian",
|
|
"Pacific",
|
|
"Etc")
|
|
tz_continent_dict = {name: index for index, name in enumerate(tz_continent_list)}
|
|
|
|
# This dictionary contains all of the daylight_savings_time zones, including No-DST ('-')
|
|
# Note the order of list matters, as it shouldn't change from release to release or else you'll
|
|
# have a very confused watch after an upgrade until the timezone is set again. We also hardcode
|
|
# certain indexes in the firmware, see the assertions below.
|
|
dstzone_list = ("-",
|
|
"AN",
|
|
"AS",
|
|
"AT",
|
|
"AV",
|
|
"Azer",
|
|
"Brazil",
|
|
"C-Eur",
|
|
"Canada",
|
|
"Chatham",
|
|
"ChileAQ",
|
|
"Cuba",
|
|
"E-Eur",
|
|
"E-EurAsia",
|
|
"EU",
|
|
"EUAsia",
|
|
"Egypt",
|
|
"Fiji",
|
|
"Haiti",
|
|
"Jordan",
|
|
"LH",
|
|
"Lebanon",
|
|
"Mexico",
|
|
"Morocco",
|
|
"NZ",
|
|
"Namibia",
|
|
"Palestine",
|
|
"Para",
|
|
"RussiaAsia",
|
|
"Syria",
|
|
"Thule",
|
|
"US",
|
|
"Uruguay",
|
|
"W-Eur",
|
|
"WS",
|
|
"Zion",
|
|
"Mongol",
|
|
"Moldova",
|
|
"Iran",
|
|
"Chile",
|
|
"Tonga")
|
|
dstzone_dict = {name: index for index, name in enumerate(dstzone_list)}
|
|
|
|
# Make sure some of these values don't move around, because the firmware code in
|
|
# fw/util/time/time.h depends on certain special case timezones having certain values (see the
|
|
# DSTID_* defines). This is gross and brittle and it would be better to generate a header.
|
|
# PBL-30559
|
|
assert dstzone_dict["Brazil"] == 6
|
|
assert dstzone_dict["LH"] == 20
|
|
|
|
|
|
def dstrule_cmp(a, b):
|
|
"""
|
|
Sort a list in the following order:
|
|
1) By dstzone
|
|
2) So that for each pair of dstzones, the 'DS' field has the following order D, S, -
|
|
"""
|
|
|
|
if (a.dstzone < b.dstzone):
|
|
return -1
|
|
elif (a.dstzone > b.dstzone):
|
|
return 1
|
|
else:
|
|
if a.ds == 'D':
|
|
return -1
|
|
elif b.ds == 'D':
|
|
return 1
|
|
elif a.ds == 'S':
|
|
return -1
|
|
elif b.ds == 'S':
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
|
|
def dstrules_parse(tzfile):
|
|
"""Top level wrapper, greps the raw zoneinfo file
|
|
Args:
|
|
Returns:
|
|
None
|
|
"""
|
|
|
|
dstrule_list = []
|
|
|
|
# struct tm->tm_mon is 0 indexed
|
|
month_dict = {'Jan': 0, 'Feb': 1, 'Mar': 2, 'Apr': 3, 'May': 4, 'Jun': 5,
|
|
'Jul': 6, 'Aug': 7, 'Sep': 8, 'Oct': 9, 'Nov': 10, 'Dec': 11}
|
|
|
|
# days in months for leapyear (feb is 1 day more)
|
|
month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
|
|
# we need an 'any' wday for rules that enact on a specific day
|
|
# the number picked is meaningless. Corresponds to DSTRULE_WDAY_ANY in clock.c
|
|
wday_dict = {'Sun': 0, 'Mon': 1, 'Tue': 2, 'Wed': 3, 'Thu': 4, 'Fri': 5, 'Sat': 6,
|
|
'Any': 255}
|
|
|
|
with open(tzfile, 'rb') as infile:
|
|
lines = infile.readlines()
|
|
for line_num, line in enumerate(lines):
|
|
match_list = re.finditer("^Rule\s+(?P<dstzone>[-A-Za-z]+)\s+[0-9]+\s+max\s+-\s+"
|
|
"(?P<month>[A-Za-z]+)\s+(?P<wday_stuff>[>=A-Za-z0-9]+)\s+"
|
|
"(?P<time>[:0-9]+)(?P<timemode>[swugz]*)\s+[:0-9su]+\s+"
|
|
"(?P<DS>[DS-])", line)
|
|
if match_list:
|
|
for match in match_list:
|
|
try:
|
|
dstzone = dstzone_dict[match.group("dstzone")]
|
|
except KeyError:
|
|
print("ERROR: Unknown dstzone found on line {}: {}"
|
|
.format(line_num, match.group("dstzone")),
|
|
file=sys.stderr)
|
|
print("You'll need to add this new dstzone to the dstzone_list.",
|
|
file=sys.stderr)
|
|
raise
|
|
|
|
ds = match.group("DS")
|
|
wday_stuff = match.group("wday_stuff")
|
|
month = month_dict[match.group("month")]
|
|
hour, minute = match.group("time").split(':')
|
|
flag = 0
|
|
for modech in match.group("timemode"):
|
|
if modech == 's': # Standard time (not wall time)
|
|
flag |= 2
|
|
elif modech == 'u' or modech == 'g' or modech == 'z': # UTC time
|
|
flag |= 4
|
|
elif modech == 'w': # Wall time
|
|
flag |= 8
|
|
else:
|
|
raise Exception("hurf char")
|
|
|
|
if 'last' in wday_stuff: # Last wday of the month
|
|
# pick the last day of the month
|
|
mday = month_days[month]
|
|
wday = wday_dict[wday_stuff[4:]]
|
|
flag |= 1
|
|
elif '>=' in wday_stuff: # wday after mday
|
|
# get the number after '>='
|
|
mday = wday_stuff.split('=')[1]
|
|
# get the dayname before '>='
|
|
wday = wday_dict[wday_stuff.split('>')[0]]
|
|
else: # specific mday
|
|
mday = int(wday_stuff)
|
|
wday = wday_dict['Any']
|
|
|
|
new_rule = dstrule(dstzone, ds, month, wday, int(flag),
|
|
int(mday), int(hour), int(minute))
|
|
dstrule_list.append(new_rule)
|
|
|
|
dstrule_list.sort(cmp=dstrule_cmp)
|
|
|
|
return dstrule_list
|
|
|
|
|
|
def build_zoneinfo_list(tzfile):
|
|
"""
|
|
Top level wrapper, searches the raw zoneinfo file
|
|
Args:
|
|
Returns:
|
|
None
|
|
"""
|
|
|
|
zoneinfo_list = []
|
|
|
|
with open(tzfile, 'rb') as infile:
|
|
lines = infile.readlines()
|
|
region = ""
|
|
continent = ""
|
|
for line in lines:
|
|
# Parse blocks that look like this
|
|
# Zone America/Toronto -5:17:32 - LMT 1895
|
|
# -5:00 Canada E%sT 1919
|
|
# -5:00 Toronto E%sT 1942 Feb 9 2:00s
|
|
# -5:00 Canada E%sT 1946
|
|
# -5:00 Toronto E%sT 1974
|
|
# -5:00 Canada E%sT
|
|
#
|
|
# We first find the zone line, that tells us what the continent and region names are
|
|
# for this zone, and then we look for the final line that tells us the current GMT
|
|
# offset, the dst rule, and then the timezone abbreviation. We discard any outdated
|
|
# information.
|
|
|
|
if line.startswith("Zone"):
|
|
match = re.match("^Zone\s+"
|
|
"(?P<continent>[A-Za-z]+)\/(?P<region>[-_\/A-Za-z]+)", line)
|
|
if match:
|
|
continent = match.group("continent")
|
|
region = match.group("region")
|
|
# fixup regions with an extra specifier, such as America/Indiana/Indianapolis
|
|
region = region.split('/')[-1]
|
|
|
|
full_region = continent + "/" + region
|
|
|
|
# Don't include Troll, Antarctica as their DST is 2 hours and overlapping rules
|
|
# not even a city, actually just a station :
|
|
# http://mm.icann.org/pipermail/tz/2014-February/020605.html
|
|
if (full_region == "Antarctica/Troll"):
|
|
region = ""
|
|
# Don't include Egypt because our rules for handling its DST are broken!
|
|
elif (full_region == "Africa/Cairo"):
|
|
region = ""
|
|
# Don't include Morocco because our rules for handling its DST are broken!
|
|
elif (full_region == "Africa/Casablanca"):
|
|
region = ""
|
|
elif (full_region == "Africa/El_Aaiun"):
|
|
region = ""
|
|
# Don't include Lord Howe Island because our rules for handling its DST are broken!
|
|
elif (full_region == "Australia/Lord_Howe"):
|
|
region = ""
|
|
|
|
# Now look to see if we've found the final line of the block
|
|
match = re.match("^(Zone\s+[-_\/A-Za-z]+\s+|\s+)" # Leading spaces or zone name
|
|
# The gmt offset (like 4:00 or -3:30)
|
|
"(?P<offset>[-0-9:]+)\s+"
|
|
# The name of the dstrule, such as US, or - if no DST
|
|
"(?P<dst_name>[-A-Za-z]+)\s+"
|
|
# The short name of the timezone, like E%sT (EST or EDT) or VET
|
|
# Or a GMT offset like +06
|
|
"(?P<tz_abbr>([A-Z%s\/]+)|\+\d+)"
|
|
# Trailing spaces and comments, no year or dates allowed
|
|
"(\s+\#.*)?$",
|
|
line, re.VERBOSE)
|
|
|
|
if match and region:
|
|
tz_abbr = match.group("tz_abbr").replace('%s', '*')
|
|
if tz_abbr.startswith('GMT/'):
|
|
tz_abbr = tz_abbr[4:]
|
|
|
|
zoneinfo_list.append(continent + " " + region + " " + match.group("offset") +
|
|
" " + tz_abbr + " " + match.group("dst_name"))
|
|
region = ""
|
|
|
|
# return the list, alphabetically sorted
|
|
zoneinfo_list.sort()
|
|
return zoneinfo_list
|
|
|
|
|
|
def zonelink_parse(tzfile):
|
|
"""
|
|
Top level wrapper, searches the raw zoneinfo file
|
|
Args:
|
|
Returns:
|
|
None
|
|
"""
|
|
|
|
zonelink_list = []
|
|
|
|
with open(tzfile, 'rb') as infile:
|
|
lines = infile.readlines()
|
|
for line in lines:
|
|
# Parse blocks that look like this
|
|
# Link America/Los_Angeles US/Pacific
|
|
|
|
# It's a link!
|
|
if line.startswith("Link"):
|
|
match = re.match("^Link\s+(?P<target>[-_\/A-Za-z]+)\s+"
|
|
"(?P<linkname>[-_\/A-Za-z]+)\s*", line)
|
|
if match:
|
|
target = match.group("target")
|
|
linkname = match.group("linkname")
|
|
# fixup regions with an extra specifier, such as America/Indiana/Indianapolis
|
|
target_parts = target.split('/')
|
|
target = target_parts[0]
|
|
if len(target_parts) != 1:
|
|
target += "/" + target_parts[-1]
|
|
zonelink_list.append(target + " " + linkname)
|
|
|
|
# return the list, alphabetically sorted
|
|
zonelink_list.sort()
|
|
return zonelink_list
|
|
|
|
|
|
def zoneinfo_to_bin(zoneinfo_list, dstrule_list, zonelink_list, output_bin):
|
|
# Corresponds to TIMEZONE_LINK_NAME_LENGTH in clock.c
|
|
# only reason we need 33 characters is for that
|
|
# troublemaker 'America/Argentina/ComodRivadavia'
|
|
TIMEZONE_LINK_NAME_LENGTH = 33
|
|
|
|
# format
|
|
# 1 byte + 15 bytes + 2 bytes + 5 bytes + 1 byte = 24 bytes
|
|
# Continent_index City gmt_offset_minutes tz_abbr dst_id
|
|
|
|
# Unsigned short - count of entries
|
|
output_bin.write(struct.pack('H', len(zoneinfo_list)))
|
|
# Unsigned short - count of DST rules
|
|
output_bin.write(struct.pack('H', len(dstzone_dict.values())))
|
|
# Unsigned short - count of links
|
|
output_bin.write(struct.pack('H', len(zonelink_list)))
|
|
|
|
region_id_list = []
|
|
# write all the timezones to file
|
|
for line in zoneinfo_list:
|
|
continent, region, gmt_offset_minutes, tz_abbr, dst_zone = line.split(' ')
|
|
|
|
# output the timezone continent index
|
|
continent_index = tz_continent_dict[continent]
|
|
output_bin.write(struct.pack('B', continent_index))
|
|
region_id_list.append(continent+"/"+region)
|
|
|
|
# fixup and output the timezone region name
|
|
output_bin.write(region.ljust(15, '\0')) # 15-character region zero padded
|
|
|
|
# fixup the gmt offset to be integer minutes
|
|
if ':' in gmt_offset_minutes:
|
|
hours, minutes = gmt_offset_minutes.split(':')
|
|
else:
|
|
hours = gmt_offset_minutes
|
|
minutes = 0
|
|
|
|
if int(hours) < 0:
|
|
gmt_offset_minutes = int(hours) * 60 - int(minutes)
|
|
else:
|
|
gmt_offset_minutes = int(hours) * 60 + int(minutes)
|
|
# signed short, for negative gmtoffsets
|
|
output_bin.write(struct.pack('h', gmt_offset_minutes))
|
|
|
|
# fix timezone abbreviations that no longer have a DST mode
|
|
if dst_zone not in dstzone_dict:
|
|
tz_abbr.replace('*', 'S') # remove
|
|
output_bin.write(tz_abbr.ljust(5, '\0')) # 5-character region zero padded
|
|
|
|
# dst table entry, 0 for NONE (ie. dash '-')
|
|
if dst_zone in dstzone_dict:
|
|
dstzone_index = dstzone_dict[dst_zone]
|
|
else:
|
|
dstzone_index = 0 # Includes '-', 'SA', 'CR', ... that no longer support DST
|
|
output_bin.write(struct.pack('B', dstzone_index))
|
|
|
|
# Write all the dstrules we know about
|
|
for id in sorted(dstzone_dict.values()):
|
|
# Don't write anything for the "No Rule" dst rule, aka "-".
|
|
if id == 0:
|
|
continue
|
|
|
|
# Look up the zone in our dstrule list. There should be two entries for each valid id,
|
|
# one for the start and one for the end of DST. Note that it may not be found if it's a
|
|
# dstrule that doesn't exist anymore.
|
|
dstrules = [r for r in dstrule_list if r.dstzone == id]
|
|
if len(dstrules) == 0:
|
|
# Just write out 16 bytes of padding instead of a real rule
|
|
output_bin.write(bytearray(16))
|
|
else:
|
|
for dstrule in dstrules:
|
|
output_bin.write(struct.pack('c', dstrule.ds))
|
|
output_bin.write(struct.pack('B', dstrule.wday))
|
|
output_bin.write(struct.pack('B', dstrule.flag))
|
|
output_bin.write(struct.pack('B', dstrule.month))
|
|
output_bin.write(struct.pack('B', dstrule.mday))
|
|
output_bin.write(struct.pack('B', dstrule.hour))
|
|
output_bin.write(struct.pack('B', dstrule.minute))
|
|
output_bin.write(struct.pack('B', 0))
|
|
|
|
# write all the timezone links to file
|
|
for line in zonelink_list:
|
|
target, linkname = line.split(' ')
|
|
try:
|
|
region_id = region_id_list.index(target)
|
|
except ValueError as e:
|
|
print("Couldn't find region, skipping:", e)
|
|
continue
|
|
output_bin.write(struct.pack('H', region_id))
|
|
output_bin.write(linkname.ljust(TIMEZONE_LINK_NAME_LENGTH, '\0'))
|
|
|
|
|
|
def build_zoneinfo_dict(olson_database):
|
|
timezones = {}
|
|
for zoneinfo in zoneinfo_list:
|
|
zoneinfo_parts = zoneinfo.split()
|
|
region = zoneinfo_parts[0]
|
|
city = zoneinfo_parts[1]
|
|
|
|
if region not in timezones:
|
|
timezones[region] = []
|
|
|
|
if city not in timezones[region]:
|
|
timezones[region].append(city)
|
|
|
|
return timezones
|
|
|
|
|
|
def build_and_create_tzdata(olson_database, output_text, output_bin):
|
|
zoneinfo_list = build_zoneinfo_list(olson_database)
|
|
|
|
# save output as text for reference
|
|
with open(output_text, 'wb') as output_txt:
|
|
for zoneinfo in zoneinfo_list:
|
|
output_txt.write("%s\n" % zoneinfo)
|
|
|
|
dstrule_list = dstrules_parse(olson_database)
|
|
|
|
zonelink_list = zonelink_parse(olson_database)
|
|
|
|
with open(output_bin, 'wb') as f:
|
|
zoneinfo_to_bin(zoneinfo_list, dstrule_list, zonelink_list, f)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Parse olson timezone database.')
|
|
|
|
parser.add_argument('olson_database', type=str)
|
|
parser.add_argument('tzdata_text', type=str)
|
|
parser.add_argument('tzdata_binary', type=str)
|
|
|
|
args = parser.parse_args()
|
|
build_and_create_tzdata(args.olson_database, args.tzdata_text, args.tzdata_binary)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|