diff --git a/dissect/target/helpers/data/windowsZones.xml b/dissect/target/helpers/data/windowsZones.xml deleted file mode 100644 index b0fc6af3bb..0000000000 --- a/dissect/target/helpers/data/windowsZones.xml +++ /dev/null @@ -1,798 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dissect/target/helpers/locale/__init__.py b/dissect/target/helpers/locale/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dissect/target/helpers/localeutil.py b/dissect/target/helpers/locale/util.py similarity index 53% rename from dissect/target/helpers/localeutil.py rename to dissect/target/helpers/locale/util.py index adae44ee8d..c451c89f74 100644 --- a/dissect/target/helpers/localeutil.py +++ b/dissect/target/helpers/locale/util.py @@ -1,20 +1,19 @@ from __future__ import annotations -import importlib -import importlib.resources from locale import normalize -from pathlib import Path -import defusedxml.ElementTree as ET +from dissect.target.helpers.locale.windows_zones import WINDOWS_ZONE_MAP def normalize_timezone(input: str) -> str: - """Returns normalized timezone format per IANA TZ standard. + """Return normalized timezone format per IANA TZ standard. Takes a Windows registry ``TimeZoneKeyName`` string as input and translates it to IANA TZ format. Will return the IANA preferred territory ``001`` value of the corresponding timezone. For example, ``Pacific Standard Time`` is translated to ``America/Los_Angeles``. + + Returns the original input string if the input does not exist in the CLDR ``WindowsZones.xml`` document. """ return WINDOWS_ZONE_MAP.get(input, input) @@ -31,28 +30,3 @@ def normalize_language(input: str) -> str: - https://en.wikipedia.org/wiki/ISO_3166-2 """ return normalize(input.replace("-", "_", 1)).split(".")[0] - - -def get_resource_string(path: str) -> str: - return _get_resource_path(path).read_text() - - -def _get_resource_path(path: str) -> Path: - root = importlib.resources.files(__package__) if __package__ else Path(__file__).parent - fpath = root.joinpath(path) - - if not fpath.exists(): - raise IOError(f"Can't find resource {fpath}") - - return fpath - - -WINDOWS_ZONE_MAP = { - child.attrib["other"]: child.attrib["type"] - for child in ET.fromstring(get_resource_string("data/windowsZones.xml")).findall( - "./windowsZones/mapTimezones/mapZone" - ) - if child.attrib["territory"] == "001" -} - -WINDOWS_ZONE_MAP["UTC"] = "UTC" # Change 'Etc/UTC' to 'UTC' to be consistent across operating systems. diff --git a/dissect/target/helpers/locale/windows_zones.py b/dissect/target/helpers/locale/windows_zones.py new file mode 100644 index 0000000000..dd4773cc1e --- /dev/null +++ b/dissect/target/helpers/locale/windows_zones.py @@ -0,0 +1,151 @@ +# This file is generated by target-build-winzones +# Do not edit manually, see ``build_winzones.py``. +# +# Generated on: 2026-05-27 09:49:52.251400+00:00 +# Source SHA1 hash: 7b2c9d6d09a4af4b8c2c10b19241268687e1cfe3 +# Number of timezones: 139 +# References: https://cldr.unicode.org/ +# https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml +from __future__ import annotations + +WINDOWS_ZONE_MAP = { + "Dateline Standard Time": "Etc/GMT+12", + "UTC-11": "Etc/GMT+11", + "Aleutian Standard Time": "America/Adak", + "Hawaiian Standard Time": "Pacific/Honolulu", + "Marquesas Standard Time": "Pacific/Marquesas", + "Alaskan Standard Time": "America/Anchorage", + "UTC-09": "Etc/GMT+9", + "Pacific Standard Time (Mexico)": "America/Tijuana", + "UTC-08": "Etc/GMT+8", + "Pacific Standard Time": "America/Los_Angeles", + "US Mountain Standard Time": "America/Phoenix", + "Mountain Standard Time (Mexico)": "America/Mazatlan", + "Mountain Standard Time": "America/Denver", + "Yukon Standard Time": "America/Whitehorse", + "Central America Standard Time": "America/Guatemala", + "Central Standard Time": "America/Chicago", + "Easter Island Standard Time": "Pacific/Easter", + "Central Standard Time (Mexico)": "America/Mexico_City", + "Canada Central Standard Time": "America/Regina", + "SA Pacific Standard Time": "America/Bogota", + "Eastern Standard Time (Mexico)": "America/Cancun", + "Eastern Standard Time": "America/New_York", + "Haiti Standard Time": "America/Port-au-Prince", + "Cuba Standard Time": "America/Havana", + "US Eastern Standard Time": "America/Indianapolis", + "Turks And Caicos Standard Time": "America/Grand_Turk", + "Paraguay Standard Time": "America/Asuncion", + "Atlantic Standard Time": "America/Halifax", + "Venezuela Standard Time": "America/Caracas", + "Central Brazilian Standard Time": "America/Cuiaba", + "SA Western Standard Time": "America/La_Paz", + "Pacific SA Standard Time": "America/Santiago", + "Newfoundland Standard Time": "America/St_Johns", + "Tocantins Standard Time": "America/Araguaina", + "E. South America Standard Time": "America/Sao_Paulo", + "SA Eastern Standard Time": "America/Cayenne", + "Argentina Standard Time": "America/Buenos_Aires", + "Greenland Standard Time": "America/Godthab", + "Montevideo Standard Time": "America/Montevideo", + "Magallanes Standard Time": "America/Punta_Arenas", + "Saint Pierre Standard Time": "America/Miquelon", + "Bahia Standard Time": "America/Bahia", + "UTC-02": "Etc/GMT+2", + "Azores Standard Time": "Atlantic/Azores", + "Cape Verde Standard Time": "Atlantic/Cape_Verde", + "UTC": "UTC", + "GMT Standard Time": "Europe/London", + "Greenwich Standard Time": "Atlantic/Reykjavik", + "Sao Tome Standard Time": "Africa/Sao_Tome", + "Morocco Standard Time": "Africa/Casablanca", + "W. Europe Standard Time": "Europe/Berlin", + "Central Europe Standard Time": "Europe/Budapest", + "Romance Standard Time": "Europe/Paris", + "Central European Standard Time": "Europe/Warsaw", + "W. Central Africa Standard Time": "Africa/Lagos", + "Jordan Standard Time": "Asia/Amman", + "GTB Standard Time": "Europe/Bucharest", + "Middle East Standard Time": "Asia/Beirut", + "Egypt Standard Time": "Africa/Cairo", + "E. Europe Standard Time": "Europe/Chisinau", + "Syria Standard Time": "Asia/Damascus", + "West Bank Standard Time": "Asia/Hebron", + "South Africa Standard Time": "Africa/Johannesburg", + "FLE Standard Time": "Europe/Kiev", + "Israel Standard Time": "Asia/Jerusalem", + "South Sudan Standard Time": "Africa/Juba", + "Kaliningrad Standard Time": "Europe/Kaliningrad", + "Sudan Standard Time": "Africa/Khartoum", + "Libya Standard Time": "Africa/Tripoli", + "Namibia Standard Time": "Africa/Windhoek", + "Arabic Standard Time": "Asia/Baghdad", + "Turkey Standard Time": "Europe/Istanbul", + "Arab Standard Time": "Asia/Riyadh", + "Belarus Standard Time": "Europe/Minsk", + "Russian Standard Time": "Europe/Moscow", + "E. Africa Standard Time": "Africa/Nairobi", + "Iran Standard Time": "Asia/Tehran", + "Arabian Standard Time": "Asia/Dubai", + "Astrakhan Standard Time": "Europe/Astrakhan", + "Azerbaijan Standard Time": "Asia/Baku", + "Russia Time Zone 3": "Europe/Samara", + "Mauritius Standard Time": "Indian/Mauritius", + "Saratov Standard Time": "Europe/Saratov", + "Georgian Standard Time": "Asia/Tbilisi", + "Volgograd Standard Time": "Europe/Volgograd", + "Caucasus Standard Time": "Asia/Yerevan", + "Afghanistan Standard Time": "Asia/Kabul", + "West Asia Standard Time": "Asia/Tashkent", + "Ekaterinburg Standard Time": "Asia/Yekaterinburg", + "Pakistan Standard Time": "Asia/Karachi", + "Qyzylorda Standard Time": "Asia/Qyzylorda", + "India Standard Time": "Asia/Calcutta", + "Sri Lanka Standard Time": "Asia/Colombo", + "Nepal Standard Time": "Asia/Katmandu", + "Central Asia Standard Time": "Asia/Bishkek", + "Bangladesh Standard Time": "Asia/Dhaka", + "Omsk Standard Time": "Asia/Omsk", + "Myanmar Standard Time": "Asia/Rangoon", + "SE Asia Standard Time": "Asia/Bangkok", + "Altai Standard Time": "Asia/Barnaul", + "W. Mongolia Standard Time": "Asia/Hovd", + "North Asia Standard Time": "Asia/Krasnoyarsk", + "N. Central Asia Standard Time": "Asia/Novosibirsk", + "Tomsk Standard Time": "Asia/Tomsk", + "China Standard Time": "Asia/Shanghai", + "North Asia East Standard Time": "Asia/Irkutsk", + "Singapore Standard Time": "Asia/Singapore", + "W. Australia Standard Time": "Australia/Perth", + "Taipei Standard Time": "Asia/Taipei", + "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar", + "Aus Central W. Standard Time": "Australia/Eucla", + "Transbaikal Standard Time": "Asia/Chita", + "Tokyo Standard Time": "Asia/Tokyo", + "North Korea Standard Time": "Asia/Pyongyang", + "Korea Standard Time": "Asia/Seoul", + "Yakutsk Standard Time": "Asia/Yakutsk", + "Cen. Australia Standard Time": "Australia/Adelaide", + "AUS Central Standard Time": "Australia/Darwin", + "E. Australia Standard Time": "Australia/Brisbane", + "AUS Eastern Standard Time": "Australia/Sydney", + "West Pacific Standard Time": "Pacific/Port_Moresby", + "Tasmania Standard Time": "Australia/Hobart", + "Vladivostok Standard Time": "Asia/Vladivostok", + "Lord Howe Standard Time": "Australia/Lord_Howe", + "Bougainville Standard Time": "Pacific/Bougainville", + "Russia Time Zone 10": "Asia/Srednekolymsk", + "Magadan Standard Time": "Asia/Magadan", + "Norfolk Standard Time": "Pacific/Norfolk", + "Sakhalin Standard Time": "Asia/Sakhalin", + "Central Pacific Standard Time": "Pacific/Guadalcanal", + "Russia Time Zone 11": "Asia/Kamchatka", + "New Zealand Standard Time": "Pacific/Auckland", + "UTC+12": "Etc/GMT-12", + "Fiji Standard Time": "Pacific/Fiji", + "Chatham Islands Standard Time": "Pacific/Chatham", + "UTC+13": "Etc/GMT-13", + "Tonga Standard Time": "Pacific/Tongatapu", + "Samoa Standard Time": "Pacific/Apia", + "Line Islands Standard Time": "Pacific/Kiritimati", +} diff --git a/dissect/target/plugins/os/unix/bsd/citrix/locale.py b/dissect/target/plugins/os/unix/bsd/citrix/locale.py index a696b04b63..3fc48f6c09 100644 --- a/dissect/target/plugins/os/unix/bsd/citrix/locale.py +++ b/dissect/target/plugins/os/unix/bsd/citrix/locale.py @@ -4,7 +4,7 @@ import re from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.localeutil import normalize_language +from dissect.target.helpers.locale.util import normalize_language from dissect.target.plugin import export from dissect.target.plugins.os.default.locale import LocalePlugin diff --git a/dissect/target/plugins/os/unix/bsd/darwin/ios/locale.py b/dissect/target/plugins/os/unix/bsd/darwin/ios/locale.py index acae31c835..241c1d658a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/ios/locale.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/ios/locale.py @@ -1,7 +1,7 @@ from __future__ import annotations from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.localeutil import normalize_language +from dissect.target.helpers.locale.util import normalize_language from dissect.target.plugin import OperatingSystem, Plugin, export from dissect.target.plugins.os.unix.locale import timezone_from_path diff --git a/dissect/target/plugins/os/unix/locale.py b/dissect/target/plugins/os/unix/locale.py index 5dbd9f54c3..3178e4aa4d 100644 --- a/dissect/target/plugins/os/unix/locale.py +++ b/dissect/target/plugins/os/unix/locale.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from dissect.target.helpers.localeutil import normalize_language +from dissect.target.helpers.locale.util import normalize_language from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import export from dissect.target.plugins.os.default.locale import LocalePlugin diff --git a/dissect/target/plugins/os/windows/locale.py b/dissect/target/plugins/os/windows/locale.py index 1102c0e5da..4c07985ec3 100644 --- a/dissect/target/plugins/os/windows/locale.py +++ b/dissect/target/plugins/os/windows/locale.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.localeutil import normalize_language, normalize_timezone +from dissect.target.helpers.locale.util import normalize_language, normalize_timezone from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import export from dissect.target.plugins.os.default.locale import LocalePlugin diff --git a/dissect/target/tools/build_winzones.py b/dissect/target/tools/build_winzones.py new file mode 100644 index 0000000000..6e2d05ed5a --- /dev/null +++ b/dissect/target/tools/build_winzones.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import argparse +import hashlib +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +import defusedxml.ElementTree as ET + +from dissect.target.tools.utils.cli import catch_sigpipe + +TEMPLATE = """\ +# This file is generated by target-build-winzones +# Do not edit manually, see ``build_winzones.py``. +# +# Generated on: {timestamp} +# Source SHA1 hash: {hash} +# Number of timezones: {count} +# References: https://cldr.unicode.org/ +# https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml +from __future__ import annotations + +WINDOWS_ZONE_MAP = {zones} +""" + + +def parse_winzones_xml(path: Path) -> str: + """Attempt to parse a Unicode ``WinZones.xml`` document.""" + content = path.read_bytes() + hash = hashlib.sha1(content).hexdigest() + + zones = { + child.attrib["other"]: child.attrib["type"] + for child in ET.fromstring(content.decode()).findall("./windowsZones/mapTimezones/mapZone") + if child.attrib["territory"] == "001" + } + + # Change 'Etc/UTC' to 'UTC' to be consistent across operating systems. + zones["UTC"] = "UTC" + + return TEMPLATE.format( + timestamp=datetime.now(tz=timezone.utc), + hash=hash, + count=len(zones), + zones=zones, + ) + + +@catch_sigpipe +def main() -> int: + help_formatter = argparse.ArgumentDefaultsHelpFormatter + parser = argparse.ArgumentParser( + description="target-build-winzones", + fromfile_prefix_chars="@", + formatter_class=help_formatter, + ) + parser.add_argument("path", metavar="PATH", type=Path, help="path to windowsZones.xml file") + parser.add_argument( + "--output", "-o", type=Path, help="optional alternative output path for generated types document" + ) + parser.add_argument("--format", action="store_true", default=False, help="format output using ruff") + args = parser.parse_args() + + if not args.path.is_file(): + print(f"Provided file does not exist: {args.path}") + return 1 + + output = (args.output or Path(__file__).parents[1].joinpath("helpers/locale/windows_zones.py")).resolve() + + if not output.parent.is_dir(): + print(f"Output path parent directory {output.parent} does not exist") + return 1 + + try: + content = parse_winzones_xml(args.path) + except Exception as e: + print(f"Exception while generating output: {e!s}") + return 1 + + try: + output.write_text(content) + except Exception as e: + print(f"Exception while writing output to {output!s}: {e!s}") + return 1 + + if args.format: + subprocess.run(["ruff", "format", output], capture_output=True) + subprocess.run(["ruff", "check", "--fix", "--unsafe-fixes", output], capture_output=True) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/helpers/test_localeutil.py b/tests/helpers/test_localeutil.py index a78552c13d..50836f3b73 100644 --- a/tests/helpers/test_localeutil.py +++ b/tests/helpers/test_localeutil.py @@ -2,7 +2,7 @@ import pytest -from dissect.target.helpers.localeutil import normalize_language, normalize_timezone +from dissect.target.helpers.locale.util import normalize_language, normalize_timezone def test_helpers_localeutil_normalize_timezone() -> None: