dfdatetime 20260730__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dfdatetime/__init__.py +26 -0
- dfdatetime/apfs_time.py +86 -0
- dfdatetime/cocoa_time.py +149 -0
- dfdatetime/decorators.py +23 -0
- dfdatetime/definitions.py +97 -0
- dfdatetime/delphi_date_time.py +166 -0
- dfdatetime/dotnet_datetime.py +157 -0
- dfdatetime/factory.py +59 -0
- dfdatetime/fake_time.py +139 -0
- dfdatetime/fat_date_time.py +337 -0
- dfdatetime/filetime.py +164 -0
- dfdatetime/golang_time.py +234 -0
- dfdatetime/hfs_time.py +150 -0
- dfdatetime/interface.py +1071 -0
- dfdatetime/java_time.py +66 -0
- dfdatetime/ole_automation_date.py +158 -0
- dfdatetime/posix_time.py +501 -0
- dfdatetime/precisions.py +425 -0
- dfdatetime/rfc2579_date_time.py +289 -0
- dfdatetime/semantic_time.py +329 -0
- dfdatetime/serializer.py +154 -0
- dfdatetime/systemtime.py +253 -0
- dfdatetime/time_elements.py +1732 -0
- dfdatetime/uuid_time.py +170 -0
- dfdatetime/webkit_time.py +159 -0
- dfdatetime-20260730.dist-info/METADATA +25 -0
- dfdatetime-20260730.dist-info/RECORD +32 -0
- dfdatetime-20260730.dist-info/WHEEL +5 -0
- dfdatetime-20260730.dist-info/licenses/ACKNOWLEDGEMENTS +16 -0
- dfdatetime-20260730.dist-info/licenses/AUTHORS +14 -0
- dfdatetime-20260730.dist-info/licenses/LICENSE +202 -0
- dfdatetime-20260730.dist-info/top_level.txt +1 -0
dfdatetime/filetime.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""FILETIME timestamp implementation."""
|
|
2
|
+
|
|
3
|
+
import decimal
|
|
4
|
+
|
|
5
|
+
from dfdatetime import definitions
|
|
6
|
+
from dfdatetime import factory
|
|
7
|
+
from dfdatetime import interface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FiletimeEpoch(interface.DateTimeEpoch):
|
|
11
|
+
"""FILETIME epoch."""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
"""Initializes a FILETIME epoch."""
|
|
15
|
+
super().__init__(1601, 1, 1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Filetime(interface.DateTimeValues):
|
|
19
|
+
"""FILETIME timestamp.
|
|
20
|
+
|
|
21
|
+
The FILETIME timestamp is a 64-bit integer that contains the number
|
|
22
|
+
of 100th nano seconds since 1601-01-01 00:00:00.
|
|
23
|
+
|
|
24
|
+
Do not confuse this with the FILETIME structure that consists of
|
|
25
|
+
2 x 32-bit integers and is presumed to be unsigned.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
is_local_time (bool): True if the date and time value is in local time.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_EPOCH = FiletimeEpoch()
|
|
32
|
+
|
|
33
|
+
# The difference between January 1, 1601 and January 1, 1970 in seconds.
|
|
34
|
+
_FILETIME_TO_POSIX_BASE = 11644473600
|
|
35
|
+
|
|
36
|
+
def __init__(self, precision=None, time_zone_offset=None, timestamp=None):
|
|
37
|
+
"""Initializes a FILETIME timestamp.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
precision (Optional[str]): precision of the date and time value, which
|
|
41
|
+
should be one of the PRECISION_VALUES in definitions.
|
|
42
|
+
time_zone_offset (Optional[int]): time zone offset in number of minutes
|
|
43
|
+
from UTC or None if not set.
|
|
44
|
+
timestamp (Optional[int]): FILETIME timestamp.
|
|
45
|
+
"""
|
|
46
|
+
super().__init__(
|
|
47
|
+
precision=precision or definitions.PRECISION_100_NANOSECONDS,
|
|
48
|
+
time_zone_offset=time_zone_offset,
|
|
49
|
+
)
|
|
50
|
+
self._timestamp = timestamp
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def timestamp(self):
|
|
54
|
+
"""int: FILETIME timestamp or None if not set."""
|
|
55
|
+
return self._timestamp
|
|
56
|
+
|
|
57
|
+
def _GetNormalizedTimestamp(self):
|
|
58
|
+
"""Retrieves the normalized timestamp.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
decimal.Decimal: normalized timestamp, which contains the number of
|
|
62
|
+
seconds since January 1, 1970 00:00:00 and a fraction of second used
|
|
63
|
+
for increased precision, or None if the normalized timestamp cannot be
|
|
64
|
+
determined.
|
|
65
|
+
"""
|
|
66
|
+
if self._normalized_timestamp is None:
|
|
67
|
+
if (
|
|
68
|
+
self._timestamp is not None
|
|
69
|
+
and self._timestamp >= 0
|
|
70
|
+
and self._timestamp <= self._UINT64_MAX
|
|
71
|
+
):
|
|
72
|
+
self._normalized_timestamp = (
|
|
73
|
+
decimal.Decimal(self._timestamp) / self._100_NANOSECONDS_PER_SECOND
|
|
74
|
+
)
|
|
75
|
+
self._normalized_timestamp -= self._FILETIME_TO_POSIX_BASE
|
|
76
|
+
|
|
77
|
+
if self._time_zone_offset:
|
|
78
|
+
self._normalized_timestamp -= self._time_zone_offset * 60
|
|
79
|
+
|
|
80
|
+
return self._normalized_timestamp
|
|
81
|
+
|
|
82
|
+
def CopyFromDateTimeString(self, time_string):
|
|
83
|
+
"""Copies a FILETIME timestamp from a date and time string.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
time_string (str): date and time value formatted as:
|
|
87
|
+
YYYY-MM-DD hh:mm:ss.######[+-]##:##
|
|
88
|
+
|
|
89
|
+
Where # are numeric digits ranging from 0 to 9 and the seconds
|
|
90
|
+
fraction can be either 3, 6 or 9 digits. The time of day, seconds
|
|
91
|
+
fraction and time zone offset are optional. The default time zone
|
|
92
|
+
is UTC.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: if the time string is invalid or not supported.
|
|
96
|
+
"""
|
|
97
|
+
date_time_values = self._CopyDateTimeFromString(time_string)
|
|
98
|
+
|
|
99
|
+
year = date_time_values.get("year", 0)
|
|
100
|
+
month = date_time_values.get("month", 0)
|
|
101
|
+
day_of_month = date_time_values.get("day_of_month", 0)
|
|
102
|
+
hours = date_time_values.get("hours", 0)
|
|
103
|
+
minutes = date_time_values.get("minutes", 0)
|
|
104
|
+
seconds = date_time_values.get("seconds", 0)
|
|
105
|
+
nanoseconds = date_time_values.get("nanoseconds", 0)
|
|
106
|
+
time_zone_offset = date_time_values.get("time_zone_offset")
|
|
107
|
+
|
|
108
|
+
if year < 1601:
|
|
109
|
+
raise ValueError(f"Year value not supported: {year!s}.")
|
|
110
|
+
|
|
111
|
+
nanoseconds, _ = divmod(nanoseconds, 100)
|
|
112
|
+
|
|
113
|
+
timestamp = self._GetNumberOfSecondsFromElements(
|
|
114
|
+
year, month, day_of_month, hours, minutes, seconds
|
|
115
|
+
)
|
|
116
|
+
timestamp += self._FILETIME_TO_POSIX_BASE
|
|
117
|
+
timestamp *= self._100_NANOSECONDS_PER_SECOND
|
|
118
|
+
timestamp += nanoseconds
|
|
119
|
+
|
|
120
|
+
self._normalized_timestamp = None
|
|
121
|
+
self._timestamp = timestamp
|
|
122
|
+
self._time_zone_offset = time_zone_offset
|
|
123
|
+
|
|
124
|
+
def CopyToDateTimeString(self):
|
|
125
|
+
"""Copies the FILETIME timestamp to a date and time string.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss.#######" or
|
|
129
|
+
None if the timestamp is missing or invalid.
|
|
130
|
+
"""
|
|
131
|
+
if (
|
|
132
|
+
self._timestamp is None
|
|
133
|
+
or self._timestamp < 0
|
|
134
|
+
or self._timestamp > self._UINT64_MAX
|
|
135
|
+
):
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
timestamp, fraction_of_second = divmod(
|
|
139
|
+
self._timestamp, self._100_NANOSECONDS_PER_SECOND
|
|
140
|
+
)
|
|
141
|
+
number_of_days, hours, minutes, seconds = self._GetTimeValues(timestamp)
|
|
142
|
+
|
|
143
|
+
year, month, day_of_month = self._GetDateValuesWithEpoch(
|
|
144
|
+
number_of_days, self._EPOCH
|
|
145
|
+
)
|
|
146
|
+
return (
|
|
147
|
+
f"{year:04d}-{month:02d}-{day_of_month:02d} "
|
|
148
|
+
f"{hours:02d}:{minutes:02d}:{seconds:02d}.{fraction_of_second:07d}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def CopyToSerializableDict(self):
|
|
152
|
+
"""Copies the date time value to a serializable dictionary.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
dict[str, object]: serializable dictionary.
|
|
156
|
+
"""
|
|
157
|
+
serializable_dict = self._CreateSerializableDict()
|
|
158
|
+
|
|
159
|
+
serializable_dict["timestamp"] = self._timestamp
|
|
160
|
+
|
|
161
|
+
return serializable_dict
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
factory.Factory.RegisterDateTimeValues(Filetime)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Golang time.Time timestamp implementation."""
|
|
2
|
+
|
|
3
|
+
import decimal
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
from dfdatetime import definitions
|
|
7
|
+
from dfdatetime import factory
|
|
8
|
+
from dfdatetime import interface
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GolangTimeEpoch(interface.DateTimeEpoch):
|
|
12
|
+
"""Golang time.Time epoch."""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
"""Initializes a Golang time.Time epoch."""
|
|
16
|
+
super().__init__(1, 1, 1)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GolangTime(interface.DateTimeValues):
|
|
20
|
+
"""Golang time.Time timestamp.
|
|
21
|
+
|
|
22
|
+
A Golang time.Time timestamp contains the number of nanoseconds since
|
|
23
|
+
January 1, 1 UTC. Depending on the version of the timestamp, the time
|
|
24
|
+
zone is stored in minutes or seconds relative to UTC.
|
|
25
|
+
|
|
26
|
+
A serialized version 1 Golang time.Time timestamp is a 15 byte value
|
|
27
|
+
that consists of:
|
|
28
|
+
|
|
29
|
+
* byte 0 - version as an 8-bit integer.
|
|
30
|
+
* bytes 1-8 - number of seconds since January 1, 1 as a big-endian signed
|
|
31
|
+
integer.
|
|
32
|
+
* bytes 9-12 - fraction of second, number of nanoseconds as a big-endian
|
|
33
|
+
signed integer.
|
|
34
|
+
* bytes 13-14 - time zone offset in minutes as a 16-bit big-endian integer,
|
|
35
|
+
where -1 represents UTC.
|
|
36
|
+
|
|
37
|
+
A serialized version 2 Golang time.Time timestamp is a 16 byte value
|
|
38
|
+
that consists of:
|
|
39
|
+
|
|
40
|
+
* byte 0 - version as an 8-bit integer.
|
|
41
|
+
* bytes 1-8 - number of seconds since January 1, 1 as a big-endian signed
|
|
42
|
+
integer.
|
|
43
|
+
* bytes 9-12 - fraction of second, number of nanoseconds as a big-endian
|
|
44
|
+
signed integer.
|
|
45
|
+
* bytes 13-14 - time zone offset in minutes as a 16-bit big-endian integer,
|
|
46
|
+
where -1 represents UTC.
|
|
47
|
+
* byte 15 - time zone offset in seconds as an 8-bit integer.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
is_local_time (bool): True if the date and time value is in local time
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# The delta between January 1, 1970 (unix epoch) and January 1, 1
|
|
54
|
+
# (Golang epoch).
|
|
55
|
+
_GOLANG_TO_POSIX_BASE = (
|
|
56
|
+
(1969 * 365) + (1969 // 4) - (1969 // 100) + (1969 // 400)
|
|
57
|
+
) * definitions.SECONDS_PER_DAY
|
|
58
|
+
|
|
59
|
+
_EPOCH = GolangTimeEpoch()
|
|
60
|
+
|
|
61
|
+
def __init__(self, golang_timestamp=None, precision=None):
|
|
62
|
+
"""Initializes a Golang time.Time timestamp.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
golang_timestamp (Optional[bytes]): the Golang time.Time timestamp.
|
|
66
|
+
precision (Optional[str]): precision of the date and time value, which
|
|
67
|
+
should be one of the PRECISION_VALUES in definitions.
|
|
68
|
+
"""
|
|
69
|
+
number_of_seconds, nanoseconds, time_zone_offset = (None, None, None)
|
|
70
|
+
if golang_timestamp is not None:
|
|
71
|
+
number_of_seconds, nanoseconds, time_zone_offset = self._GetNumberOfSeconds(
|
|
72
|
+
golang_timestamp
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
super().__init__(
|
|
76
|
+
precision=precision or definitions.PRECISION_1_NANOSECOND,
|
|
77
|
+
time_zone_offset=time_zone_offset,
|
|
78
|
+
)
|
|
79
|
+
self._golang_timestamp = golang_timestamp
|
|
80
|
+
self._nanoseconds = nanoseconds
|
|
81
|
+
self._number_of_seconds = number_of_seconds
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def golang_timestamp(self):
|
|
85
|
+
"""bytes: Golang time.Time timestamp or None if not set."""
|
|
86
|
+
return self._golang_timestamp
|
|
87
|
+
|
|
88
|
+
def _GetNormalizedTimestamp(self):
|
|
89
|
+
"""Retrieves the normalized timestamp.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
decimal.Decimal: normalized timestamp, which contains the number of
|
|
93
|
+
seconds since January 1, 1970 00:00:00 and a fraction of second used
|
|
94
|
+
for increased precision, or None if the normalized timestamp cannot be
|
|
95
|
+
determined.
|
|
96
|
+
"""
|
|
97
|
+
if self._normalized_timestamp is None:
|
|
98
|
+
if (
|
|
99
|
+
self._number_of_seconds is not None
|
|
100
|
+
and self._number_of_seconds >= self._GOLANG_TO_POSIX_BASE
|
|
101
|
+
and self._nanoseconds is not None
|
|
102
|
+
and self._nanoseconds >= 0
|
|
103
|
+
):
|
|
104
|
+
self._normalized_timestamp = decimal.Decimal(
|
|
105
|
+
self._number_of_seconds - GolangTime._GOLANG_TO_POSIX_BASE
|
|
106
|
+
)
|
|
107
|
+
if self._nanoseconds is not None and self._nanoseconds >= 0:
|
|
108
|
+
self._normalized_timestamp += (
|
|
109
|
+
decimal.Decimal(self._nanoseconds)
|
|
110
|
+
/ definitions.NANOSECONDS_PER_SECOND
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if self._time_zone_offset:
|
|
114
|
+
self._normalized_timestamp -= self._time_zone_offset * 60
|
|
115
|
+
|
|
116
|
+
return self._normalized_timestamp
|
|
117
|
+
|
|
118
|
+
def _GetNumberOfSeconds(self, golang_timestamp):
|
|
119
|
+
"""Retrieves the number of seconds from a Golang time.Time timestamp.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
golang_timestamp (bytes): the Golang time.Time timestamp.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
tuple[int, int, int]: number of seconds since January 1, 1 00:00:00,
|
|
126
|
+
fraction of second in nanoseconds and time zone offset in minutes.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: if the Golang time.Time timestamp could not be parsed.
|
|
130
|
+
"""
|
|
131
|
+
byte_size = len(golang_timestamp)
|
|
132
|
+
if byte_size < 15:
|
|
133
|
+
raise ValueError("Unsupported Golang time.Time timestamp.")
|
|
134
|
+
|
|
135
|
+
version = golang_timestamp[0]
|
|
136
|
+
if version not in (1, 2):
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Unsupported Golang time.Time timestamp version: {version:d}."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if (version == 1 and byte_size != 15) or (version == 2 and byte_size != 16):
|
|
142
|
+
raise ValueError("Unsupported Golang time.Time timestamp.")
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
number_of_seconds, nanoseconds, time_zone_offset = struct.unpack(
|
|
146
|
+
">qih", golang_timestamp[1:15]
|
|
147
|
+
)
|
|
148
|
+
# TODO: add support for version 2 time zone offset in seconds
|
|
149
|
+
|
|
150
|
+
except (TypeError, struct.error) as exception:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Unable to unpacked Golang time.Time timestamp with error: "
|
|
153
|
+
f"{exception!s}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# A time zone offset of -1 minute is a special representation for UTC.
|
|
157
|
+
if time_zone_offset == -1:
|
|
158
|
+
time_zone_offset = 0
|
|
159
|
+
|
|
160
|
+
return number_of_seconds, nanoseconds, time_zone_offset
|
|
161
|
+
|
|
162
|
+
def CopyFromDateTimeString(self, time_string):
|
|
163
|
+
"""Copies a date time value from a date and time string.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
time_string (str): date and time value formatted as:
|
|
167
|
+
YYYY-MM-DD hh:mm:ss.######[+-]##:##
|
|
168
|
+
|
|
169
|
+
Where # are numeric digits ranging from 0 to 9 and the seconds
|
|
170
|
+
fraction can be either 3, 6 or 9 digits. The time of day, seconds
|
|
171
|
+
fraction and time zone offset are optional. The default time zone
|
|
172
|
+
is UTC.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: if the time string is invalid or not supported.
|
|
176
|
+
"""
|
|
177
|
+
date_time_values = self._CopyDateTimeFromString(time_string)
|
|
178
|
+
year = date_time_values.get("year", 0)
|
|
179
|
+
month = date_time_values.get("month", 0)
|
|
180
|
+
day_of_month = date_time_values.get("day_of_month", 0)
|
|
181
|
+
hours = date_time_values.get("hours", 0)
|
|
182
|
+
minutes = date_time_values.get("minutes", 0)
|
|
183
|
+
seconds = date_time_values.get("seconds", 0)
|
|
184
|
+
nanoseconds = date_time_values.get("nanoseconds", 0)
|
|
185
|
+
time_zone_offset = date_time_values.get("time_zone_offset")
|
|
186
|
+
|
|
187
|
+
if year < 0:
|
|
188
|
+
raise ValueError(f"Year value not supported: {year!s}.")
|
|
189
|
+
|
|
190
|
+
seconds = self._GetNumberOfSecondsFromElements(
|
|
191
|
+
year, month, day_of_month, hours, minutes, seconds
|
|
192
|
+
)
|
|
193
|
+
seconds += self._GOLANG_TO_POSIX_BASE
|
|
194
|
+
|
|
195
|
+
self._normalized_timestamp = None
|
|
196
|
+
self._number_of_seconds = seconds
|
|
197
|
+
self._nanoseconds = nanoseconds
|
|
198
|
+
self._time_zone_offset = time_zone_offset
|
|
199
|
+
|
|
200
|
+
def CopyToDateTimeString(self):
|
|
201
|
+
"""Copies the Golang time value to a date and time string.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss.######" or
|
|
205
|
+
None if the timestamp cannot be copied to a date and time string.
|
|
206
|
+
"""
|
|
207
|
+
if self._number_of_seconds is None or self._number_of_seconds < 0:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
number_of_days, hours, minutes, seconds = self._GetTimeValues(
|
|
211
|
+
self._number_of_seconds
|
|
212
|
+
)
|
|
213
|
+
year, month, day_of_month = self._GetDateValuesWithEpoch(
|
|
214
|
+
number_of_days, self._EPOCH
|
|
215
|
+
)
|
|
216
|
+
return (
|
|
217
|
+
f"{year:04d}-{month:02d}-{day_of_month:02d} "
|
|
218
|
+
f"{hours:02d}:{minutes:02d}:{seconds:02d}.{self._nanoseconds:09d}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def CopyToSerializableDict(self):
|
|
222
|
+
"""Copies the date time value to a serializable dictionary.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
dict[str, object]: serializable dictionary.
|
|
226
|
+
"""
|
|
227
|
+
return {
|
|
228
|
+
"__class_name__": type(self).__name__,
|
|
229
|
+
"__type__": "DateTimeValues",
|
|
230
|
+
"golang_timestamp": self._golang_timestamp,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
factory.Factory.RegisterDateTimeValues(GolangTime)
|
dfdatetime/hfs_time.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""HFS time implementation."""
|
|
2
|
+
|
|
3
|
+
import decimal
|
|
4
|
+
|
|
5
|
+
from dfdatetime import definitions
|
|
6
|
+
from dfdatetime import factory
|
|
7
|
+
from dfdatetime import interface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HFSTimeEpoch(interface.DateTimeEpoch):
|
|
11
|
+
"""HFS time epoch."""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
"""Initializes a HFS time epoch."""
|
|
15
|
+
super().__init__(1904, 1, 1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HFSTime(interface.DateTimeValues):
|
|
19
|
+
"""HFS timestamp.
|
|
20
|
+
|
|
21
|
+
The HFS timestamp is an unsigned 32-bit integer that contains the number of
|
|
22
|
+
seconds since 1904-01-01 00:00:00. Where in HFS the timestamp is typically
|
|
23
|
+
in local time and in HFS+/HFSX in UTC.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
is_local_time (bool): True if the date and time value is in local time.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_EPOCH = HFSTimeEpoch()
|
|
30
|
+
|
|
31
|
+
# The difference between Jan 1, 1904 and Jan 1, 1970 in seconds.
|
|
32
|
+
_HFS_TO_POSIX_BASE = 2082844800
|
|
33
|
+
|
|
34
|
+
def __init__(self, precision=None, time_zone_offset=None, timestamp=None):
|
|
35
|
+
"""Initializes a HFS timestamp.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
precision (Optional[str]): precision of the date and time value, which
|
|
39
|
+
should be one of the PRECISION_VALUES in definitions.
|
|
40
|
+
time_zone_offset (Optional[int]): time zone offset in number of minutes
|
|
41
|
+
from UTC or None if not set.
|
|
42
|
+
timestamp (Optional[int]): HFS timestamp.
|
|
43
|
+
"""
|
|
44
|
+
super().__init__(
|
|
45
|
+
precision=precision or definitions.PRECISION_1_SECOND,
|
|
46
|
+
time_zone_offset=time_zone_offset,
|
|
47
|
+
)
|
|
48
|
+
self._timestamp = timestamp
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def timestamp(self):
|
|
52
|
+
"""int: HFS timestamp or None if not set."""
|
|
53
|
+
return self._timestamp
|
|
54
|
+
|
|
55
|
+
def _GetNormalizedTimestamp(self):
|
|
56
|
+
"""Retrieves the normalized timestamp.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
decimal.Decimal: normalized timestamp, which contains the number of
|
|
60
|
+
seconds since January 1, 1970 00:00:00 and a fraction of second used
|
|
61
|
+
for increased precision, or None if the normalized timestamp cannot be
|
|
62
|
+
determined.
|
|
63
|
+
"""
|
|
64
|
+
if self._normalized_timestamp is None:
|
|
65
|
+
if (
|
|
66
|
+
self._timestamp is not None
|
|
67
|
+
and self._timestamp >= 0
|
|
68
|
+
and self._timestamp <= self._UINT32_MAX
|
|
69
|
+
):
|
|
70
|
+
self._normalized_timestamp = (
|
|
71
|
+
decimal.Decimal(self._timestamp) - self._HFS_TO_POSIX_BASE
|
|
72
|
+
)
|
|
73
|
+
if self._time_zone_offset:
|
|
74
|
+
self._normalized_timestamp -= self._time_zone_offset * 60
|
|
75
|
+
|
|
76
|
+
return self._normalized_timestamp
|
|
77
|
+
|
|
78
|
+
def CopyFromDateTimeString(self, time_string):
|
|
79
|
+
"""Copies a HFS timestamp from a date and time string.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
time_string (str): date and time value formatted as:
|
|
83
|
+
YYYY-MM-DD hh:mm:ss.######[+-]##:##
|
|
84
|
+
|
|
85
|
+
Where # are numeric digits ranging from 0 to 9 and the seconds
|
|
86
|
+
fraction can be either 3, 6 or 9 digits. The time of day, seconds
|
|
87
|
+
fraction and time zone offset are optional. The default time zone
|
|
88
|
+
is UTC.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: if the time string is invalid or not supported.
|
|
92
|
+
"""
|
|
93
|
+
date_time_values = self._CopyDateTimeFromString(time_string)
|
|
94
|
+
|
|
95
|
+
year = date_time_values.get("year", 0)
|
|
96
|
+
month = date_time_values.get("month", 0)
|
|
97
|
+
day_of_month = date_time_values.get("day_of_month", 0)
|
|
98
|
+
hours = date_time_values.get("hours", 0)
|
|
99
|
+
minutes = date_time_values.get("minutes", 0)
|
|
100
|
+
seconds = date_time_values.get("seconds", 0)
|
|
101
|
+
time_zone_offset = date_time_values.get("time_zone_offset")
|
|
102
|
+
|
|
103
|
+
if year < 1904 or year > 2040:
|
|
104
|
+
raise ValueError("Year value not supported.")
|
|
105
|
+
|
|
106
|
+
self._normalized_timestamp = None
|
|
107
|
+
self._timestamp = self._GetNumberOfSecondsFromElements(
|
|
108
|
+
year, month, day_of_month, hours, minutes, seconds
|
|
109
|
+
)
|
|
110
|
+
self._timestamp += self._HFS_TO_POSIX_BASE
|
|
111
|
+
self._time_zone_offset = time_zone_offset
|
|
112
|
+
|
|
113
|
+
def CopyToDateTimeString(self):
|
|
114
|
+
"""Copies the HFS timestamp to a date and time string.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss" or None
|
|
118
|
+
if the timestamp is missing or invalid.
|
|
119
|
+
"""
|
|
120
|
+
if (
|
|
121
|
+
self._timestamp is None
|
|
122
|
+
or self._timestamp < 0
|
|
123
|
+
or self._timestamp > self._UINT32_MAX
|
|
124
|
+
):
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
number_of_days, hours, minutes, seconds = self._GetTimeValues(self._timestamp)
|
|
128
|
+
|
|
129
|
+
year, month, day_of_month = self._GetDateValuesWithEpoch(
|
|
130
|
+
number_of_days, self._EPOCH
|
|
131
|
+
)
|
|
132
|
+
return (
|
|
133
|
+
f"{year:04d}-{month:02d}-{day_of_month:02d} "
|
|
134
|
+
f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def CopyToSerializableDict(self):
|
|
138
|
+
"""Copies the date time value to a serializable dictionary.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
dict[str, object]: serializable dictionary.
|
|
142
|
+
"""
|
|
143
|
+
serializable_dict = self._CreateSerializableDict()
|
|
144
|
+
|
|
145
|
+
serializable_dict["timestamp"] = self._timestamp
|
|
146
|
+
|
|
147
|
+
return serializable_dict
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
factory.Factory.RegisterDateTimeValues(HFSTime)
|