s3:utils: Fix 'Usage:' for 'net ads enctypes'
[samba4-gss.git] / python / samba / nt_time.py
blob714f9e97ef4ae738357ca1a65e839eea6ff9c0a5
2 # NT Time utility functions.
4 # Copyright (C) Catalyst.Net Ltd 2023
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <https://www.gnu.org/licenses/>.
20 import datetime
21 from typing import NewType, Optional
22 import re
25 NtTime = NewType("NtTime", int)
26 NtTimeDelta = NewType("NtTimeDelta", int)
28 NT_TIME_MAX = NtTime((1 << 64) - 1)
30 NT_EPOCH = datetime.datetime(1601, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc)
31 NT_TICKS_PER_μSEC = 10
32 NT_TICKS_PER_SEC = NT_TICKS_PER_μSEC * 1_000_000
35 def _validate_nt_time(nt_time: NtTime) -> None:
36 if not isinstance(nt_time, int):
37 raise ValueError(f"{nt_time} is not an integer")
38 if not 0 <= nt_time <= NT_TIME_MAX:
39 raise ValueError(f"{nt_time} is out of range")
42 def nt_time_from_datetime(tm: datetime.datetime) -> NtTime:
43 time_since_epoch = tm - NT_EPOCH
44 nt_time = NtTime(round(time_since_epoch.total_seconds() * NT_TICKS_PER_SEC))
45 _validate_nt_time(nt_time)
46 return nt_time
49 def nt_now() -> NtTime:
50 dt = datetime.datetime.now(datetime.timezone.utc)
51 return nt_time_from_datetime(dt)
54 def datetime_from_nt_time(nt_time: NtTime) -> datetime.datetime:
55 _validate_nt_time(nt_time)
56 time_since_epoch = datetime.timedelta(microseconds=nt_time / NT_TICKS_PER_μSEC)
57 return NT_EPOCH + time_since_epoch
60 def nt_time_delta_from_timedelta(dt: datetime.timedelta) -> NtTimeDelta:
61 return NtTimeDelta(round(dt.total_seconds() * NT_TICKS_PER_SEC))
64 def timedelta_from_nt_time_delta(nt_time_delta: NtTimeDelta) -> datetime.timedelta:
65 return datetime.timedelta(microseconds=nt_time_delta / NT_TICKS_PER_μSEC)
68 def nt_time_from_string(s: str) -> NtTime:
69 """Convert a subset of ISO 8601 date/time strings, ldap timestamps,
70 and the string 'now' into NT time.
72 The ldap format is
74 YYYYmmddHHMMSS.0Z
76 which is 14 digits followed by the fixed string '.0Z'. This is
77 used in LDIF and internally by ldb.
79 The ISO format is
81 YYYY-mm-dd[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]
83 where the '*' can be any character, and the optional last
84 '[+HH:MM[:SS[.ffffff]]]' is a timezone offset (use '+00:00' for
85 UTC).
86 """
87 try:
88 if s == "now":
89 dt = datetime.datetime.now(datetime.timezone.utc)
90 elif re.match(r"^\d{14}\.0Z$", s):
91 # "20230127223641.0Z"
92 dt = datetime.datetime.strptime(s, "%Y%m%d%H%M%S.0Z")
93 else:
94 dt = datetime.datetime.fromisoformat(s)
95 except ValueError:
96 raise ValueError(
97 "Expected a date in either "
98 "ISO8601 'YYYY-MM-DD HH:MM:SS' format, "
99 "LDAP timestamp 'YYYYmmddHHMMSS.0Z', "
100 "or the literal string 'now'. "
101 f" Got '{s}'."
104 if dt.tzinfo is None:
105 # This is a cursed timestamp with no timezone info. We have to
106 # guess or nt_time_from_datetime() will fail. The best guess
107 # is the system timezone, which we can get this way:
108 dt = dt.astimezone()
110 return nt_time_from_datetime(dt)
113 def string_from_nt_time(nttime: NtTime, format: Optional[str] = None) -> str:
114 """Format an NtTime date as a string.
116 If format is not provided, an ISO 8601 string is used.
118 dt = datetime_from_nt_time(nttime)
120 if format is not None:
121 return dt.strftime(format)
123 return dt.isoformat()