diff --git a/docs/guide/time-processing.md b/docs/guide/time-processing.md index 39bf727..3f4e802 100644 --- a/docs/guide/time-processing.md +++ b/docs/guide/time-processing.md @@ -190,14 +190,18 @@ playseek=2024-01-01T12:00:00.123-2024-01-01T13:00:00.456 #### 默认行为 -如果 User-Agent 中没有时区信息,则不进行任何时区转换,只应用 `r2h-seek-offset` 指定的秒数偏移。 +如果 User-Agent 中没有时区信息,则按以下规则处理: + +- 对 Unix 时间戳、带 `Z` / `±HH:MM` 的 ISO 8601、以及带 `GMT` 后缀的 `yyyyMMddHHmmssGMT`,继续使用时间字符串自身携带的时区语义 +- 对不带时区信息的 `yyyyMMddHHmmss`、简化 ISO 8601、完整 ISO 8601,默认按 **rtp2httpd 所在系统时区** 解析,而不是按 UTC 解析 +- 然后再应用 `r2h-seek-offset` 指定的额外秒数偏移 > [!NOTE] > rtp2httpd 按以下步骤处理时间参数: > > 1. **解析时间格式** — 识别参数值属于哪种格式:Unix 时间戳(≤10 位数字)、`yyyyMMddHHmmss`(14 位数字)、`yyyyMMddHHmmssGMT`(14 位 + GMT 后缀)、简化 ISO 8601(`yyyyMMddTHHmmss`)、完整 ISO 8601(`yyyy-MM-ddTHH:mm:ss`) -> 2. **解析 User-Agent 时区** — 从 User-Agent 中查找 `TZ/` 标记,提取 UTC 偏移量(秒)。如果没有时区信息,默认为 0(UTC) -> 3. **时区转换** — Unix 时间戳和 ISO 8601 带时区的格式跳过转换;`yyyyMMddHHmmss` 和 ISO 8601 无时区的格式应用 User-Agent 时区转换 +> 2. **解析 User-Agent 时区** — 从 User-Agent 中查找 `TZ/` 标记,提取 UTC 偏移量(秒)。如果没有时区信息,则对“不带时区的日期时间格式”回退到系统时区 +> 3. **时区转换** — Unix 时间戳、`yyyyMMddHHmmssGMT` 和 ISO 8601 带时区的格式跳过该回退;`yyyyMMddHHmmss` 和 ISO 8601 无时区的格式应用 User-Agent 时区或系统时区转换 > 4. **应用 `r2h-seek-offset`** — 如果指定了该参数,对所有格式应用额外的秒数偏移(可正可负) > 5. **格式化输出** — 保持原始格式,保留原有时区后缀(如有) > 6. **附加到上游 URL** — 将处理后的时间参数作为查询参数附加到上游请求中(RTSP 发送 DESCRIBE 请求,HTTP 转发给上游服务器) diff --git a/e2e/helpers/r2h_process.py b/e2e/helpers/r2h_process.py index 1c213ac..8d322fc 100644 --- a/e2e/helpers/r2h_process.py +++ b/e2e/helpers/r2h_process.py @@ -19,11 +19,13 @@ def __init__( port: int, extra_args: list[str] | None = None, config_content: str | None = None, + env: dict[str, str] | None = None, ): self.binary = str(binary) self.port = port self.extra_args = list(extra_args or []) self.config_content = config_content + self.env = dict(env or {}) self.process: subprocess.Popen | None = None self._config_path: str | None = None @@ -31,10 +33,13 @@ def __init__( def start(self, wait: bool = True) -> None: args = self._build_args() + # Override only the keys provided by the test while preserving the rest + # of the parent environment for the child rtp2httpd process. self.process = subprocess.Popen( args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + env={**os.environ, **self.env}, ) if wait and not wait_for_port(self.port, timeout=6.0): self.stop() diff --git a/e2e/test_http_proxy.py b/e2e/test_http_proxy.py index 532887c..53d8320 100644 --- a/e2e/test_http_proxy.py +++ b/e2e/test_http_proxy.py @@ -6,6 +6,8 @@ """ import time +from datetime import datetime +from zoneinfo import ZoneInfo import pytest @@ -19,6 +21,7 @@ ) pytestmark = pytest.mark.http_proxy +_SYSTEM_TZ = ZoneInfo("Asia/Shanghai") # --------------------------------------------------------------------------- @@ -171,6 +174,48 @@ def test_query_forwarded(self, shared_r2h): finally: upstream.stop() + def test_playseek_uses_system_timezone_without_client_timezone(self, r2h_binary): + upstream = MockHTTPUpstream( + routes={ + "/vod": {"status": 200, "body": b"ok"}, + } + ) + upstream.start() + try: + r2h = R2HProcess( + r2h_binary, + find_free_port(), + extra_args=["-v", "4", "-m", "100"], + env={"TZ": "Asia/Shanghai"}, + ) + r2h.start() + try: + start_ts = int(time.time()) - 1800 + end_ts = start_ts + 300 + start_local = datetime.fromtimestamp(start_ts, _SYSTEM_TZ).strftime("%Y%m%d%H%M%S") + end_local = datetime.fromtimestamp(end_ts, _SYSTEM_TZ).strftime("%Y%m%d%H%M%S") + start_utc = time.strftime("%Y%m%d%H%M%S", time.gmtime(start_ts)) + end_utc = time.strftime("%Y%m%d%H%M%S", time.gmtime(end_ts)) + expected_playseek = "playseek=%s-%s" % (start_utc, end_utc) + local_playseek = "playseek=%s-%s" % (start_local, end_local) + + status, _, body = http_get( + "127.0.0.1", + r2h.port, + "/http/127.0.0.1:%d/vod?playseek=%s-%s" % (upstream.port, start_local, end_local), + timeout=5.0, + ) + + assert status == 200 + assert body == b"ok" + assert len(upstream.requests_log) > 0, "Expected upstream request" + assert expected_playseek in upstream.requests_log[0]["path"] + assert local_playseek not in upstream.requests_log[0]["path"] + finally: + r2h.stop() + finally: + upstream.stop() + # --------------------------------------------------------------------------- # Upstream unreachable diff --git a/e2e/test_rtsp_misc.py b/e2e/test_rtsp_misc.py index d36b9d1..ac3cd08 100644 --- a/e2e/test_rtsp_misc.py +++ b/e2e/test_rtsp_misc.py @@ -8,6 +8,8 @@ import json import socket import time +from datetime import datetime +from zoneinfo import ZoneInfo import pytest @@ -26,6 +28,7 @@ pytestmark = pytest.mark.rtsp _STREAM_TIMEOUT = 20.0 +_SYSTEM_TZ = ZoneInfo("Asia/Shanghai") # Timeout constants matching C code (3s each) _RTSP_HANDSHAKE_TIMEOUT = 3 @@ -82,6 +85,11 @@ def _format_basic_utc(ts: int) -> str: return time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(ts)) +def _format_local_compact(ts: int) -> str: + """Format a Unix timestamp as `yyyyMMddHHmmss` in Asia/Shanghai.""" + return datetime.fromtimestamp(ts, _SYSTEM_TZ).strftime("%Y%m%d%H%M%S") + + @pytest.fixture(scope="module") def shared_r2h(r2h_binary): """A single rtp2httpd instance shared by all misc RTSP tests.""" @@ -495,6 +503,49 @@ def test_recent_playseek_ignores_r2h_start(self, shared_r2h): finally: rtsp.stop() + def test_playseek_uses_system_timezone_without_client_timezone(self, r2h_binary): + rtsp = MockRTSPServer(num_packets=500) + rtsp.start() + r2h = R2HProcess( + r2h_binary, + find_free_port(), + extra_args=["-v", "4", "-m", "100"], + env={"TZ": "Asia/Shanghai"}, + ) + r2h.start() + try: + start_ts = int(time.time()) - 1800 + end_ts = start_ts + 300 + start_local = _format_local_compact(start_ts) + end_local = _format_local_compact(end_ts) + expected_range = "clock=%s-" % _format_basic_utc(start_ts) + incorrect_utc_range = "clock=%sT%sZ-" % (start_local[:8], start_local[8:]) + url = "/rtsp/127.0.0.1:%d/stream?playseek=%s-%s" % ( + rtsp.port, + start_local, + end_local, + ) + + stream_get( + "127.0.0.1", + r2h.port, + url, + read_bytes=4096, + timeout=_STREAM_TIMEOUT, + ) + + describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] + assert len(describe_reqs) > 0, "Expected DESCRIBE" + assert "playseek=" not in describe_reqs[0]["uri"] + + play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] + assert len(play_reqs) > 0, "Expected PLAY request" + assert play_reqs[0]["headers"].get("Range") == expected_range + assert expected_range != incorrect_utc_range + finally: + r2h.stop() + rtsp.stop() + @pytest.mark.parametrize("param_name", ["playseek", "Playseek", "tvdr", "custom_seek"]) def test_boundary_playseek_is_forwarded(self, shared_r2h, param_name): rtsp = MockRTSPServer(num_packets=500) diff --git a/src/service.c b/src/service.c index 8b5d913..991b25d 100644 --- a/src/service.c +++ b/src/service.c @@ -592,9 +592,11 @@ int service_parse_seek_value(const char *seek_param_value, int seek_offset_secon memset(parse_result, 0, sizeof(*parse_result)); parse_result->seek_offset_seconds = seek_offset_seconds; parse_result->now_utc = time(NULL); + parse_result->tz_offset_seconds = TIMEZONE_USE_SYSTEM_LOCAL_OFFSET; - if (user_agent) - timezone_parse_from_user_agent(user_agent, &parse_result->tz_offset_seconds); + if (user_agent && timezone_parse_from_user_agent(user_agent, &parse_result->tz_offset_seconds) != 0) { + logger(LOG_DEBUG, "Timezone: Keeping system timezone fallback because User-Agent timezone parse failed"); + } if (!seek_param_value || seek_param_value[0] == '\0') return 0; @@ -638,8 +640,12 @@ int service_parse_seek_value(const char *seek_param_value, int seek_offset_secon if (!tmp) return -1; parse_result->begin_tm_utc = *tmp; - time_t local_ts = parse_result->begin_utc + parse_result->tz_offset_seconds; - tmp = gmtime(&local_ts); + if (parse_result->tz_offset_seconds == TIMEZONE_USE_SYSTEM_LOCAL_OFFSET) + tmp = localtime(&parse_result->begin_utc); + else { + time_t local_ts = parse_result->begin_utc + parse_result->tz_offset_seconds; + tmp = gmtime(&local_ts); + } if (!tmp) return -1; parse_result->begin_tm_local = *tmp; @@ -651,8 +657,12 @@ int service_parse_seek_value(const char *seek_param_value, int seek_offset_secon if (!tmp) return -1; parse_result->end_tm_utc = *tmp; - time_t local_ts = parse_result->end_utc + parse_result->tz_offset_seconds; - tmp = gmtime(&local_ts); + if (parse_result->tz_offset_seconds == TIMEZONE_USE_SYSTEM_LOCAL_OFFSET) + tmp = localtime(&parse_result->end_utc); + else { + time_t local_ts = parse_result->end_utc + parse_result->tz_offset_seconds; + tmp = gmtime(&local_ts); + } if (!tmp) return -1; parse_result->end_tm_local = *tmp; diff --git a/src/timezone.c b/src/timezone.c index b0e1f2f..425166a 100644 --- a/src/timezone.c +++ b/src/timezone.c @@ -15,6 +15,44 @@ #define SECONDS_PER_DAY 86400 #define MAX_TIMEZONE_OFFSET_SECONDS (TIMEZONE_MAX_OFFSET_HOURS * SECONDS_PER_HOUR) #define MIN_TIMEZONE_OFFSET_SECONDS (TIMEZONE_MIN_OFFSET_HOURS * SECONDS_PER_HOUR) +#define COMPACT_DATETIME_LENGTH 14 +#define COMPACT_DATETIME_GMT_LENGTH 17 + +static int timezone_is_using_system_local(int tz_offset_seconds) { + return tz_offset_seconds == TIMEZONE_USE_SYSTEM_LOCAL_OFFSET; +} + +static int timezone_validate_offset(int tz_offset_seconds) { + if (timezone_is_using_system_local(tz_offset_seconds)) + return 0; + + if (tz_offset_seconds < MIN_TIMEZONE_OFFSET_SECONDS || tz_offset_seconds > MAX_TIMEZONE_OFFSET_SECONDS) { + logger(LOG_ERROR, "Timezone: Invalid timezone offset %d seconds (range: [%d, %d])", tz_offset_seconds, + MIN_TIMEZONE_OFFSET_SECONDS, MAX_TIMEZONE_OFFSET_SECONDS); + return -1; + } + + return 0; +} + +static int timezone_local_tm_to_timestamp(struct tm *local_time, time_t *timestamp_out) { + time_t timestamp; + + if (!local_time || !timestamp_out) { + logger(LOG_ERROR, "Timezone: NULL pointer in timezone_local_tm_to_timestamp"); + return -1; + } + + local_time->tm_isdst = -1; + timestamp = mktime(local_time); + if (timestamp == (time_t)-1) { + logger(LOG_ERROR, "Timezone: Failed to convert local time to timestamp"); + return -1; + } + + *timestamp_out = timestamp; + return 0; +} /* * Parse timezone information from User-Agent header @@ -249,9 +287,7 @@ int timezone_convert_time_with_offset(const char *input_time, int tz_offset_seco } /* Validate timezone offset range */ - if (tz_offset_seconds < MIN_TIMEZONE_OFFSET_SECONDS || tz_offset_seconds > MAX_TIMEZONE_OFFSET_SECONDS) { - logger(LOG_ERROR, "Timezone: Invalid timezone offset %d seconds (range: [%d, %d])", tz_offset_seconds, - MIN_TIMEZONE_OFFSET_SECONDS, MAX_TIMEZONE_OFFSET_SECONDS); + if (timezone_validate_offset(tz_offset_seconds) != 0) { return -1; } @@ -270,9 +306,10 @@ int timezone_convert_time_with_offset(const char *input_time, int tz_offset_seco /* Format 2: yyyyMMddHHmmss and yyyyMMddHHmmssGMT (14 digits, optionally * followed by "GMT") */ - if ((input_len == 14 && digit_count == 14) || - (input_len == 17 && digit_count == 14 && strcmp(input_time + 14, "GMT") == 0)) { - int has_gmt_suffix = (input_len == 17); + if ((input_len == COMPACT_DATETIME_LENGTH && digit_count == COMPACT_DATETIME_LENGTH) || + (input_len == COMPACT_DATETIME_GMT_LENGTH && digit_count == COMPACT_DATETIME_LENGTH && + strcmp(input_time + COMPACT_DATETIME_LENGTH, "GMT") == 0)) { + int has_gmt_suffix = (input_len == COMPACT_DATETIME_GMT_LENGTH); /* Parse the time string (first 14 digits) */ if (sscanf(input_time, "%4d%2d%2d%2d%2d%2d", &year, &month, &day, &hour, &min, &sec) != 6) { @@ -316,15 +353,21 @@ int timezone_convert_time_with_offset(const char *input_time, int tz_offset_seco local_time.tm_sec = sec; local_time.tm_isdst = 0; - timestamp = timegm(&local_time); + if (timezone_is_using_system_local(tz_offset_seconds) && !has_gmt_suffix) { + if (timezone_local_tm_to_timestamp(&local_time, ×tamp) != 0) + return -1; + } else { + timestamp = timegm(&local_time); - if (timestamp == -1) { - logger(LOG_ERROR, "Timezone: Failed to convert time to timestamp"); - return -1; + if (timestamp == -1) { + logger(LOG_ERROR, "Timezone: Failed to convert time to timestamp"); + return -1; + } + + timestamp -= tz_offset_seconds; } /* Apply timezone conversion and additional offset */ - timestamp -= tz_offset_seconds; timestamp += additional_offset_seconds; /* Convert back to yyyyMMddHHmmss format */ @@ -349,10 +392,15 @@ int timezone_convert_time_with_offset(const char *input_time, int tz_offset_seco } else { strncpy(output_time, temp_time, output_size - 1); output_time[output_size - 1] = '\0'; - logger(LOG_DEBUG, - "Timezone: yyyyMMddHHmmss '%s' (TZ offset %d) + seek offset %d = " - "'%s'", - input_time, -tz_offset_seconds, additional_offset_seconds, output_time); + if (timezone_is_using_system_local(tz_offset_seconds)) { + logger(LOG_DEBUG, "Timezone: yyyyMMddHHmmss '%s' (system TZ) + seek offset %d = '%s'", input_time, + additional_offset_seconds, output_time); + } else { + logger(LOG_DEBUG, + "Timezone: yyyyMMddHHmmss '%s' (TZ offset %d) + seek offset %d = " + "'%s'", + input_time, -tz_offset_seconds, additional_offset_seconds, output_time); + } } return 0; @@ -371,16 +419,26 @@ int timezone_convert_time_with_offset(const char *input_time, int tz_offset_seco return -1; } - timestamp = timegm(&tm); + if (has_timezone) { + timestamp = timegm(&tm); - if (timestamp == -1) { - logger(LOG_ERROR, "Timezone: Failed to convert basic ISO 8601 time to timestamp"); - return -1; - } + if (timestamp == -1) { + logger(LOG_ERROR, "Timezone: Failed to convert basic ISO 8601 time to timestamp"); + return -1; + } - if (has_timezone) { timestamp -= timezone_offset; + } else if (timezone_is_using_system_local(tz_offset_seconds)) { + if (timezone_local_tm_to_timestamp(&tm, ×tamp) != 0) + return -1; } else { + timestamp = timegm(&tm); + + if (timestamp == -1) { + logger(LOG_ERROR, "Timezone: Failed to convert basic ISO 8601 time to timestamp"); + return -1; + } + timestamp -= tz_offset_seconds; } timestamp += additional_offset_seconds; @@ -402,10 +460,15 @@ int timezone_convert_time_with_offset(const char *input_time, int tz_offset_seco return -1; } - logger(LOG_DEBUG, - "Timezone: basic ISO 8601 '%s' (TZ offset %d) + seek offset %d = " - "'%s'", - input_time, has_timezone ? timezone_offset : -tz_offset_seconds, additional_offset_seconds, output_time); + if (!has_timezone && timezone_is_using_system_local(tz_offset_seconds)) { + logger(LOG_DEBUG, "Timezone: basic ISO 8601 '%s' (system TZ) + seek offset %d = '%s'", input_time, + additional_offset_seconds, output_time); + } else { + logger(LOG_DEBUG, + "Timezone: basic ISO 8601 '%s' (TZ offset %d) + seek offset %d = " + "'%s'", + input_time, has_timezone ? timezone_offset : -tz_offset_seconds, additional_offset_seconds, output_time); + } return 0; } @@ -630,6 +693,9 @@ int timezone_parse_to_utc(const char *input_time, int tz_offset_seconds, int add return -1; } + if (timezone_validate_offset(tz_offset_seconds) != 0) + return -1; + input_len = strlen(input_time); digit_count = strspn(input_time, "0123456789"); @@ -642,8 +708,9 @@ int timezone_parse_to_utc(const char *input_time, int tz_offset_seconds, int add } /* Format 2: yyyyMMddHHmmss or yyyyMMddHHmmssGMT */ - if ((input_len == 14 && digit_count == 14) || - (input_len == 17 && digit_count == 14 && strcmp(input_time + 14, "GMT") == 0)) { + if ((input_len == COMPACT_DATETIME_LENGTH && digit_count == COMPACT_DATETIME_LENGTH) || + (input_len == COMPACT_DATETIME_GMT_LENGTH && digit_count == COMPACT_DATETIME_LENGTH && + strcmp(input_time + COMPACT_DATETIME_LENGTH, "GMT") == 0)) { if (sscanf(input_time, "%4d%2d%2d%2d%2d%2d", &year, &month, &day, &hour, &min, &sec) != 6) { return -1; } @@ -657,12 +724,20 @@ int timezone_parse_to_utc(const char *input_time, int tz_offset_seconds, int add local_time.tm_sec = sec; local_time.tm_isdst = 0; - timestamp = timegm(&local_time); + /* Only bare yyyyMMddHHmmss should fall back to the daemon's system + * timezone. yyyyMMddHHmmssGMT already carries explicit UTC semantics. */ + if (timezone_is_using_system_local(tz_offset_seconds) && input_len == COMPACT_DATETIME_LENGTH) { + if (timezone_local_tm_to_timestamp(&local_time, ×tamp) != 0) + return -1; + } else { + timestamp = timegm(&local_time); - if (timestamp == -1) - return -1; + if (timestamp == -1) + return -1; + + timestamp -= tz_offset_seconds; + } - timestamp -= tz_offset_seconds; timestamp += additional_offset_seconds; *out_utc = timestamp; return 0; @@ -680,14 +755,22 @@ int timezone_parse_to_utc(const char *input_time, int tz_offset_seconds, int add return -1; } - timestamp = timegm(&tm); + if (has_timezone) { + timestamp = timegm(&tm); - if (timestamp == -1) - return -1; + if (timestamp == -1) + return -1; - if (has_timezone) { timestamp -= timezone_offset; + } else if (timezone_is_using_system_local(tz_offset_seconds)) { + if (timezone_local_tm_to_timestamp(&tm, ×tamp) != 0) + return -1; } else { + timestamp = timegm(&tm); + + if (timestamp == -1) + return -1; + timestamp -= tz_offset_seconds; } timestamp += additional_offset_seconds; @@ -708,15 +791,24 @@ int timezone_parse_to_utc(const char *input_time, int tz_offset_seconds, int add return -1; } - timestamp = timegm(&tm); + if (has_timezone) { + timestamp = timegm(&tm); - if (timestamp == -1) - return -1; + if (timestamp == -1) + return -1; - if (has_timezone) { timestamp -= timezone_offset; timestamp += additional_offset_seconds; + } else if (timezone_is_using_system_local(tz_offset_seconds)) { + if (timezone_local_tm_to_timestamp(&tm, ×tamp) != 0) + return -1; + timestamp += additional_offset_seconds; } else { + timestamp = timegm(&tm); + + if (timestamp == -1) + return -1; + timestamp -= tz_offset_seconds; timestamp += additional_offset_seconds; } @@ -759,15 +851,15 @@ int timezone_convert_iso8601_with_offset(const char *iso_str, int external_tz_of return -1; } - timestamp = timegm(&tm); - - if (timestamp == -1) { - logger(LOG_ERROR, "Timezone: Failed to convert time to timestamp"); - return -1; - } - /* Apply timezone conversion and offset */ if (has_timezone) { + timestamp = timegm(&tm); + + if (timestamp == -1) { + logger(LOG_ERROR, "Timezone: Failed to convert time to timestamp"); + return -1; + } + /* Input has embedded timezone * The tm structure represents the time as parsed (without timezone) * We need to: @@ -779,7 +871,19 @@ int timezone_convert_iso8601_with_offset(const char *iso_str, int external_tz_of "Timezone: ISO 8601 has embedded timezone, only applying offset %d " "seconds", offset_seconds); + } else if (timezone_is_using_system_local(external_tz_offset)) { + if (timezone_local_tm_to_timestamp(&tm, ×tamp) != 0) + return -1; + timestamp += offset_seconds; + logger(LOG_DEBUG, "Timezone: ISO 8601 no timezone, using system TZ + offset %d seconds", offset_seconds); } else { + timestamp = timegm(&tm); + + if (timestamp == -1) { + logger(LOG_ERROR, "Timezone: Failed to convert time to timestamp"); + return -1; + } + /* No embedded timezone - apply timezone conversion then offset */ timestamp -= external_tz_offset; timestamp += offset_seconds; diff --git a/src/timezone.h b/src/timezone.h index b28181f..aeaab92 100644 --- a/src/timezone.h +++ b/src/timezone.h @@ -11,12 +11,14 @@ #ifndef TIMEZONE_H #define TIMEZONE_H +#include #include #include /* Constants */ #define TIMEZONE_MAX_OFFSET_HOURS 14 /* Maximum timezone offset (UTC+14) */ #define TIMEZONE_MIN_OFFSET_HOURS -12 /* Minimum timezone offset (UTC-12) */ +#define TIMEZONE_USE_SYSTEM_LOCAL_OFFSET INT_MAX /* * Parse timezone information from User-Agent header @@ -76,7 +78,9 @@ int timezone_format_time_yyyyMMddHHmmss(const struct tm *utc_time, char *output_ * @param input_time Input time string (must not be NULL) * @param tz_offset_seconds Timezone offset from User-Agent in seconds * (positive for east, negative for west) - * Range: [-43200, 50400] seconds + * Range: [-43200, 50400] seconds, or + * TIMEZONE_USE_SYSTEM_LOCAL_OFFSET to parse naive + * datetimes in the daemon's system timezone. * Used only for formats without embedded timezone * @param additional_offset_seconds Additional offset in seconds to apply * (can be positive or negative) @@ -177,7 +181,8 @@ int timezone_format_time_iso8601(const struct tm *tm, int milliseconds, const ch * * @param iso_str Input ISO 8601 string (must not be NULL) * @param external_tz_offset Timezone offset from User-Agent in seconds (used - * only if no embedded timezone) + * only if no embedded timezone), or TIMEZONE_USE_SYSTEM_LOCAL_OFFSET to use + * the daemon's system timezone * @param offset_seconds Additional offset to always apply in seconds * @param output Output buffer (must not be NULL) * @param output_size Size of output buffer (minimum 30 bytes) @@ -197,7 +202,9 @@ int timezone_convert_iso8601_with_offset(const char *iso_str, int external_tz_of * Thread Safety: NOT thread-safe (uses gmtime() which returns static buffer) * * @param input_time Input time string (must not be NULL) - * @param tz_offset_seconds Timezone offset in seconds from UTC + * @param tz_offset_seconds Timezone offset in seconds from UTC, or + * TIMEZONE_USE_SYSTEM_LOCAL_OFFSET to parse naive datetimes in the daemon's + * system timezone * @param additional_offset_seconds Additional offset in seconds * @param out_utc Output: resulting UTC epoch value (must not be NULL) * @return 0 on success, -1 on error