Repository: log2timeline/dfdatetime

Issue:
Add support for Golang binary timestamps
Golang serializes timestamps (time.Time structs) in the following binary form [0]

- 1 byte - version (1)
- 8 bytes BE signed int - seconds
- 4 bytes BE signed int - nanoseconds
- 2 bytes BE signed int - zone offset in minutes (where -1 == UTC)

Epoch/zero value is January 1, year 1, 00:00:00.000000000 UTC [1]

[0] Ref: https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/time/time.go;drc=1d91551b7326383343c7c143a8ac299d0a685289;l=1169

[1] https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/time/time.go;drc=1d91551b7326383343c7c143a8ac299d0a685289;l=333

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/log2timeline/dfdatetime/pull/203?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline) Report
> Merging [#203](https://codecov.io/gh/log2timeline/dfdatetime/pull/203?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline) (dc87dfe) into [main](https://codecov.io/gh/log2timeline/dfdatetime/commit/80297dbadfa0681e83e64f252783c353f5ba2d37?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline) (80297db) will **decrease** coverage by `0.00%`.
> The diff coverage is `96.61%`.

[![Impacted file tree graph](https://codecov.io/gh/log2timeline/dfdatetime/pull/203/graphs/tree.svg?width=650&height=150&src=pr&token=Kt70kF2sv2&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline)](https://codecov.io/gh/log2timeline/dfdatetime/pull/203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline)

```diff
@@            Coverage Diff             @@
##             main     #203      +/-   ##
==========================================
- Coverage   96.84%   96.84%   -0.01%     
==========================================
  Files          22       23       +1     
  Lines        1999     2058      +59     
==========================================
+ Hits         1936     1993      +57     
- Misses         63       65       +2     
```


| [Impacted Files](https://codecov.io/gh/log2timeline/dfdatetime/pull/203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline) | Coverage Δ | |
|---|---|---|
| [dfdatetime/golang\_time.py](https://codecov.io/gh/log2timeline/dfdatetime/pull/203/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline#diff-ZGZkYXRldGltZS9nb2xhbmdfdGltZS5weQ==) | `96.55% <96.55%> (ø)` | |
| [dfdatetime/definitions.py](https://codecov.io/gh/log2timeline/dfdatetime/pull/203/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline#diff-ZGZkYXRldGltZS9kZWZpbml0aW9ucy5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/log2timeline/dfdatetime/pull/203?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/log2timeline/dfdatetime/pull/203?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline). Last update [80297db...dc87dfe](https://codecov.io/gh/log2timeline/dfdatetime/pull/203?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=log2timeline).

joachimmetz: @sydp is this ready for review?
sydp: Yes if you have spare cycles thanks, I have to add to the main documentation as well but I can do in another PR?
joachimmetz: >  I have to add to the main documentation as well but I can do in another PR?

I opt to add the documentation to this PR, you can find it under docs/
sydp: > > I have to add to the main documentation as well but I can do in another PR?
> 
> I opt to add the documentation to this PR, you can find it under docs/

Sure, will add to it tonight/tomorrow. 
joachimmetz: thx, no hurry, just easier to do all changes at once
joachimmetz: @sydp let's discuss the changes to the time_zone value. IMHO it would be closer to the current interface to pass the 15 or 16 byte value as a single timestamp and have dfDateTime do the parsing/validation of that value. 

Patch:
diff --git a/config/dpkg/changelog b/config/dpkg/changelog
index 2ec47d2..6adb3dc 100644
--- a/config/dpkg/changelog
+++ b/config/dpkg/changelog
@@ -1,5 +1,5 @@
-dfdatetime (20211120-1) unstable; urgency=low
+dfdatetime (20211228-1) unstable; urgency=low
 
   * Auto-generated
 
- -- Log2Timeline maintainers <log2timeline-maintainers@googlegroups.com>  Sat, 20 Nov 2021 20:09:49 +0100
+ -- Log2Timeline maintainers <log2timeline-maintainers@googlegroups.com>  Tue, 28 Dec 2021 04:28:53 +0100
diff --git a/dfdatetime/__init__.py b/dfdatetime/__init__.py
index ec9787c..6fbc5e7 100644
--- a/dfdatetime/__init__.py
+++ b/dfdatetime/__init__.py
@@ -12,6 +12,7 @@ from dfdatetime import delphi_date_time
 from dfdatetime import fat_date_time
 from dfdatetime import filetime
 from dfdatetime import hfs_time
+from dfdatetime import golang_time
 from dfdatetime import java_time
 from dfdatetime import ole_automation_date
 from dfdatetime import posix_time
@@ -20,5 +21,7 @@ from dfdatetime import semantic_time
 from dfdatetime import systemtime
 from dfdatetime import time_elements
 from dfdatetime import uuid_time
+from dfdatetime import webkit_time
 
-__version__ = '20211120'
+
+__version__ = '20211228'
diff --git a/dfdatetime/definitions.py b/dfdatetime/definitions.py
index b38bdac..24c4bcc 100644
--- a/dfdatetime/definitions.py
+++ b/dfdatetime/definitions.py
@@ -18,6 +18,7 @@ MICROSECONDS_PER_SECOND = 1000000
 MICROSECONDS_PER_DECISECOND = 100000
 MICROSECONDS_PER_MILLISECOND = 1000
 
+NANOSECONDS_PER_MICROSECOND = 1000
 NANOSECONDS_PER_SECOND = 1000000000
 
 PRECISION_1_DAY = '1d'
diff --git a/dfdatetime/filetime.py b/dfdatetime/filetime.py
index f8f16e2..58ac42d 100644
--- a/dfdatetime/filetime.py
+++ b/dfdatetime/filetime.py
@@ -64,7 +64,7 @@ class Filetime(interface.DateTimeValues):
       if (self._timestamp is not None and self._timestamp >= 0 and
           self._timestamp <= self._UINT64_MAX):
         self._normalized_timestamp = (
-            decimal.Decimal(self._timestamp) / self._100NS_PER_SECOND)
+            decimal.Decimal(self._timestamp) / self._100_NANOSECONDS_PER_SECOND)
         self._normalized_timestamp -= self._FILETIME_TO_POSIX_BASE
 
         if self._time_zone_offset:
@@ -105,7 +105,7 @@ class Filetime(interface.DateTimeValues):
     timestamp += self._FILETIME_TO_POSIX_BASE
     timestamp *= definitions.MICROSECONDS_PER_SECOND
     timestamp += date_time_values.get('microseconds', 0)
-    timestamp *= self._100NS_PER_MICROSECOND
+    timestamp *= self._100_NANOSECONDS_PER_MICROSECOND
 
     self._normalized_timestamp = None
     self._timestamp = timestamp
@@ -122,7 +122,8 @@ class Filetime(interface.DateTimeValues):
         self._timestamp > self._UINT64_MAX):
       return None
 
-    timestamp, remainder = divmod(self._timestamp, self._100NS_PER_SECOND)
+    timestamp, remainder = divmod(
+        self._timestamp, self._100_NANOSECONDS_PER_SECOND)
     number_of_days, hours, minutes, seconds = self._GetTimeValues(timestamp)
 
     year, month, day_of_month = self._GetDateValuesWithEpoch(
diff --git a/dfdatetime/golang_time.py b/dfdatetime/golang_time.py
new file mode 100644
index 0000000..a34d4df
--- /dev/null
+++ b/dfdatetime/golang_time.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+"""Golang time.Time timestamp implementation."""
+
+import decimal
+import struct
+
+from dfdatetime import definitions
+from dfdatetime import factory
+from dfdatetime import interface
+
+
+class GolangTimeEpoch(interface.DateTimeEpoch):
+  """Golang time.Time epoch."""
+
+  def __init__(self):
+    """Initializes a Golang time.Time epoch."""
+    super(GolangTimeEpoch, self).__init__(1, 1, 1)
+
+
+class GolangTime(interface.DateTimeValues):
+  """Golang time.Time timestamp.
+
+  A Golang time.Time timestamp contans the number of nanoseconds since
+  January 1, 1 UTC. Depending on the version of the timestamp, the time
+  zone is stored in minutes or seconds relative to UTC.
+
+  A serialized version 1 Golang time.Time timestamp is a 15 byte value
+  that consists of:
+
+  * byte 0 - version as an 8-bit integer.
+  * bytes 1-8 - number of seconds since January 1, 1 as a big-endian signed
+      integer.
+  * bytes 9-12 - fraction of second, number of nanoseconds as a big-endian
+      signed integer.
+  * bytes 13-14 - time zone offset in minutes as a 16-bit big endian integer,
+      where -1 represents UTC.
+
+  A serialized version 2 Golang time.Time timestamp is a 16 byte value
+  that consists of:
+
+  * byte 0 - version as an 8-bit integer.
+  * bytes 1-8 - number of seconds since January 1, 1 as a big-endian signed
+      integer.
+  * bytes 9-12 - fraction of second, number of nanoseconds as a big-endian
+      signed integer.
+  * bytes 13-14 - time zone offset in minutes as a 16-bit big endian integer,
+      where -1 represents UTC.
+  * byte 15 - time zone offset in seconds as an 8-bit integer.
+
+  Attributes:
+    is_local_time (bool): True if the date and time value is in local time
+  """
+
+  # The delta between January 1, 1970 (unix epoch) and January 1, 1
+  # (Golang epoch).
+  _GOLANG_TO_POSIX_BASE = (
+      ((1969 * 365) + (1969 // 4) - (1969 // 100) + (1969 // 400)) *
+      definitions.SECONDS_PER_DAY)
+
+  _EPOCH = GolangTimeEpoch()
+
+  def __init__(self, golang_timestamp=None):
+    """Initializes a Golang time.Time timestamp.
+
+    Args:
+      golang_timestamp (Optional[bytes]): the Golang time.Time timestamp.
+    """
+    number_of_seconds, nanoseconds, time_zone_offset = (None, None, None)
+    if golang_timestamp is not None:
+      number_of_seconds, nanoseconds, time_zone_offset = (
+          self._GetNumberOfSeconds(golang_timestamp))
+
+    super(GolangTime, self).__init__(time_zone_offset=time_zone_offset)
+    self._golang_timestamp = golang_timestamp
+    self._nanoseconds = nanoseconds
+    self._number_of_seconds = number_of_seconds
+    self._precision = definitions.PRECISION_1_NANOSECOND
+
+    if time_zone_offset:
+      self.is_local_time = True
+
+  @property
+  def golang_timestamp(self):
+    """int: Golang time.Time timestamp or None if not set."""
+    return self._golang_timestamp
+
+  def _GetNormalizedTimestamp(self):
+    """Retrieves the normalized timestamp.
+
+    Returns:
+      decimal.Decimal: normalized timestamp, which contains the number of
+          seconds since January 1, 1970 00:00:00 and a fraction of second used
+          for increased precision, or None if the normalized timestamp cannot be
+          determined.
+    """
+    if self._normalized_timestamp is None:
+      if (self._number_of_seconds is not None and
+          self._number_of_seconds >= self._GOLANG_TO_POSIX_BASE and
+          self._nanoseconds is not None and self._nanoseconds >= 0):
+
+        self._normalized_timestamp = decimal.Decimal(
+            self._number_of_seconds - GolangTime._GOLANG_TO_POSIX_BASE)
+
+        if self._nanoseconds is not None and self._nanoseconds >= 0:
+          self._normalized_timestamp += (
+              decimal.Decimal(self._nanoseconds) /
+              definitions.NANOSECONDS_PER_SECOND)
+
+    return self._normalized_timestamp
+
+  def _GetNumberOfSeconds(self, golang_timestamp):
+    """Retrieves the number of seconds from a Golang time.Time timestamp.
+
+    Args:
+      golang_timestamp (bytes): the Golang time.Time timestamp.
+
+    Returns:
+      tuple[int, int, int]: number of seconds since January 1, 1 00:00:00,
+          fraction of second in nanoseconds and time zone offset in minutes.
+
+    Raises:
+      ValueError: if the Golang time.Time timestamp could not be parsed.
+    """
+    byte_size = len(golang_timestamp)
+    if byte_size < 15:
+      raise ValueError('Unsupported Golang time.Time timestamp.')
+
+    version = golang_timestamp[0]
+    if version not in (1, 2):
+      raise ValueError(
+          'Unsupported Golang time.Time timestamp version: {0:d}.'.format(
+              version))
+
+    if (version == 1 and byte_size != 15) or (version == 2 and byte_size != 16):
+      raise ValueError('Unsupported Golang time.Time timestamp.')
+
+    try:
+      number_of_seconds, nanoseconds, time_zone_offset = struct.unpack(
+          '>qih', golang_timestamp[1:15])
+
+      # TODO: add support for version 2 time zone offset in seconds
+
+    except struct.error as exception:
+      raise ValueError((
+          'Unable to unpacke Golang time.Time timestamp with error: '
+          '{0:s}').format(exception))
+
+    # A time zone offset of -1 minute is a special representation for UTC.
+    if time_zone_offset == -1:
+      time_zone_offset = 0
+
+    return number_of_seconds, nanoseconds, time_zone_offset
+
+  def CopyFromDateTimeString(self, time_string):
+    """Copies a date time value from a date and time string.
+
+    Args:
+      time_string (str): date and time value formatted as:
+          YYYY-MM-DD hh:mm:ss.######[+-]##:##
+
+          Where # are numeric digits ranging from 0 to 9 and the seconds
+          fraction can be either 3 or 6 digits. The time of day, seconds
+          fraction and time zone offset are optional. The default time zone
+          is UTC.
+
+    Raises:
+      ValueError: if the time string is invalid or not supported.
+    """
+    date_time_values = self._CopyDateTimeFromString(time_string)
+    year = date_time_values.get('year', 0)
+    month = date_time_values.get('month', 0)
+    day_of_month = date_time_values.get('day_of_month', 0)
+    hours = date_time_values.get('hours', 0)
+    minutes = date_time_values.get('minutes', 0)
+    seconds = date_time_values.get('seconds', 0)
+    microseconds = date_time_values.get('microseconds', 0)
+    time_zone_offset = date_time_values.get('time_zone_offset', 0)
+
+    if year < 0:
+      raise ValueError('Year value not supported: {0!s}.'.format(year))
+
+    seconds = self._GetNumberOfSecondsFromElements(
+        year, month, day_of_month, hours, minutes, seconds)
+
+    seconds += self._GOLANG_TO_POSIX_BASE
+    nanoseconds = microseconds * definitions.NANOSECONDS_PER_MICROSECOND
+
+    self._normalized_timestamp = None
+    self._number_of_seconds = seconds
+    self._nanoseconds = nanoseconds
+    self._time_zone_offset = time_zone_offset
+
+  def CopyToDateTimeString(self):
+    """Copies the Golang time value to a date and time string.
+
+    Returns:
+      str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss.######" or
+          None if the timestamp cannot be copied to a date and time string.
+    """
+    if self._number_of_seconds is None or self._number_of_seconds < 0:
+      return None
+
+    seconds = self._number_of_seconds
+    nanoseconds_seconds, remainder = divmod(
+        self._nanoseconds, definitions.NANOSECONDS_PER_SECOND)
+
+    seconds += nanoseconds_seconds
+    remainder = remainder // definitions.NANOSECONDS_PER_MICROSECOND
+    number_of_days, hours, minutes, seconds = self._GetTimeValues(seconds)
+
+    year, month, day_of_month = self._GetDateValuesWithEpoch(
+        number_of_days, self._EPOCH)
+
+    return '{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}.{6:06d}'.format(
+        year, month, day_of_month, hours, minutes, seconds, remainder)
+
+
+factory.Factory.RegisterDateTimeValues(GolangTime)
diff --git a/dfdatetime/interface.py b/dfdatetime/interface.py
index 8bfbb3a..7585da8 100644
--- a/dfdatetime/interface.py
+++ b/dfdatetime/interface.py
@@ -61,10 +61,9 @@ class DateTimeValues(object):
 
   _EPOCH_NORMALIZED_TIME = NormalizedTimeEpoch()
 
-  _100NS_PER_SECOND = 10000000
-  _100NS_PER_DECISECOND = 1000000
-  _100NS_PER_MILLISECOND = 10000
-  _100NS_PER_MICROSECOND = 10
+  _100_MILLISECONDS_PER_SECOND = 10
+  _100_NANOSECONDS_PER_SECOND = 10000000
+  _100_NANOSECONDS_PER_MICROSECOND = 10
 
   _INT64_MIN = -(1 << 63)
   _INT64_MAX = (1 << 63) - 1
@@ -855,6 +854,36 @@ class DateTimeValues(object):
 
     return int(normalized_timestamp)
 
+  def CopyToPosixTimestampWithFractionOfSecond(self):
+    """Copies the date time value to a POSIX timestamp with fraction of second.
+
+    Returns:
+      tuple[int, int]: a POSIX timestamp in seconds with fraction of second or
+          None, None if no timestamp is available.
+    """
+    normalized_timestamp = self._GetNormalizedTimestamp()
+    if normalized_timestamp is None:
+      return None, None
+
+    remainder = None
+    if self._precision == definitions.PRECISION_1_NANOSECOND:
+      remainder = int(
+          (normalized_timestamp % 1) * definitions.NANOSECONDS_PER_SECOND)
+    elif self._precision == definitions.PRECISION_100_NANOSECONDS:
+      remainder = int(
+          (normalized_timestamp % 1) * self._100_NANOSECONDS_PER_SECOND)
+    elif self._precision == definitions.PRECISION_1_MICROSECOND:
+      remainder = int(
+          (normalized_timestamp % 1) * definitions.MICROSECONDS_PER_SECOND)
+    elif self._precision == definitions.PRECISION_1_MILLISECOND:
+      remainder = int(
+          (normalized_timestamp % 1) * definitions.MILLISECONDS_PER_SECOND)
+    elif self._precision == definitions.PRECISION_100_MILLISECONDS:
+      remainder = int(
+          (normalized_timestamp % 1) * self._100_MILLISECONDS_PER_SECOND)
+
+    return int(normalized_timestamp), remainder
+
   # TODO: remove this method when there is no more need for it in dfvfs.
   def CopyToStatTimeTuple(self):
     """Copies the date time value to a stat timestamp tuple.
@@ -873,7 +902,8 @@ class DateTimeValues(object):
         definitions.PRECISION_1_MICROSECOND,
         definitions.PRECISION_1_MILLISECOND,
         definitions.PRECISION_100_MILLISECONDS):
-      remainder = int((normalized_timestamp % 1) * self._100NS_PER_SECOND)
+      remainder = int(
+          (normalized_timestamp % 1) * self._100_NANOSECONDS_PER_SECOND)
 
       return int(normalized_timestamp), remainder
 
diff --git a/dfdatetime/rfc2579_date_time.py b/dfdatetime/rfc2579_date_time.py
index f18d62d..546a964 100644
--- a/dfdatetime/rfc2579_date_time.py
+++ b/dfdatetime/rfc2579_date_time.py
@@ -27,7 +27,7 @@ class RFC2579DateTime(interface.DateTimeValues):
   }
 
   Also see:
-    https://tools.ietf.org/html/rfc2579
+    https://datatracker.ietf.org/doc/html/rfc2579
 
   Attributes:
     year (int): year, 0 through 65536.
diff --git a/dfdatetime/serializer.py b/dfdatetime/serializer.py
index b58d719..90bfa2b 100644
--- a/dfdatetime/serializer.py
+++ b/dfdatetime/serializer.py
@@ -100,6 +100,9 @@ class Serializer(object):
     elif class_name == 'FATDateTime':
       json_dict['fat_date_time'] = date_time_values.fat_date_time
 
+    elif class_name == 'GolangTime':
+      json_dict['golang_timestamp'] = date_time_values.golang_timestamp
+
     elif class_name == 'RFC2579DateTime':
       json_dict['rfc2579_date_time_tuple'] = (
           date_time_values.year, date_time_values.month,
diff --git a/dfdatetime/uuid_time.py b/dfdatetime/uuid_time.py
index 8b9aee1..e165b88 100644
--- a/dfdatetime/uuid_time.py
+++ b/dfdatetime/uuid_time.py
@@ -69,7 +69,7 @@ class UUIDTime(interface.DateTimeValues):
       if (self._timestamp is not None and self._timestamp >= 0 and
           self._timestamp <= self._UINT60_MAX):
         self._normalized_timestamp = (
-            decimal.Decimal(self._timestamp) / self._100NS_PER_SECOND)
+            decimal.Decimal(self._timestamp) / self._100_NANOSECONDS_PER_SECOND)
         self._normalized_timestamp -= self._UUID_TO_POSIX_BASE
 
         if self._time_zone_offset:
@@ -110,7 +110,7 @@ class UUIDTime(interface.DateTimeValues):
     timestamp += self._UUID_TO_POSIX_BASE
     timestamp *= definitions.MICROSECONDS_PER_SECOND
     timestamp += date_time_values.get('microseconds', 0)
-    timestamp *= self._100NS_PER_MICROSECOND
+    timestamp *= self._100_NANOSECONDS_PER_MICROSECOND
 
     self._normalized_timestamp = None
     self._timestamp = timestamp
@@ -127,7 +127,8 @@ class UUIDTime(interface.DateTimeValues):
         self._timestamp > self._UINT60_MAX):
       return None
 
-    timestamp, remainder = divmod(self._timestamp, self._100NS_PER_SECOND)
+    timestamp, remainder = divmod(
+        self._timestamp, self._100_NANOSECONDS_PER_SECOND)
     number_of_days, hours, minutes, seconds = self._GetTimeValues(timestamp)
 
     year, month, day_of_month = self._GetDateValuesWithEpoch(
diff --git a/docs/sources/Date-and-time-values.md b/docs/sources/Date-and-time-values.md
index 4360270..626341b 100644
--- a/docs/sources/Date-and-time-values.md
+++ b/docs/sources/Date-and-time-values.md
@@ -197,6 +197,37 @@ SetFileTime Windows API function are overloaded with a special meaning.
 
 * [MSDN: FILETIME](https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime)
 
+## Golang time.Time timestamp
+
+### Characteristics
+
+Attribute | Description
+--- | ---
+Supported date range | 0001-01-01 00:00:00.0000000 through ...
+Storage granularity | 1 nanosecond
+Time zone | internally represented.
+
+The granularity of the time zone value depends on the version of the timestamp.
+Version 1 timestamps are stored in minutes and version 2 timestamps add a
+seconds component.  *Note: Version 2 is currently not supported.*
+
+### Format
+
+Offset | Size | Description
+--- | --- | ---
+0 | 1 | version (known values are 1 or 2)
+1 | 8 | seconds since January, 1, 1 stored as a 64-bit big-endian signed integer
+9 | 4 | fraction of second, in nanoseconds stored as a 32-bit big-endian signed integer
+13 | 2 | time zone offset in minutes as a 16-bit big-endian signed integer.
+15 | 1 | time zone offset in seconds (only for version 2)
+
+A value of -1 is a special value when the Time instance is initialised as UTC
+(e.g. ```time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)```).
+
+### Also see
+
+* [Golang time.go](https://cs.opensource.google/go/go/+/master:src/time/time.go)
+
 ## HFS timestamp
 
 Sometimes a distinction is made between HFS and HFS+ timestamps is that the
diff --git a/docs/sources/api/dfdatetime.rst b/docs/sources/api/dfdatetime.rst
index cb7362a..1e1185f 100644
--- a/docs/sources/api/dfdatetime.rst
+++ b/docs/sources/api/dfdatetime.rst
@@ -76,6 +76,14 @@ dfdatetime.filetime module
    :undoc-members:
    :show-inheritance:
 
+dfdatetime.golang\_time module
+------------------------------
+
+.. automodule:: dfdatetime.golang_time
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 dfdatetime.hfs\_time module
 ---------------------------
 
diff --git a/docs/sources/user/Installation-instructions.md b/docs/sources/user/Installation-instructions.md
index 31e3ddc..6bdf0ed 100644
--- a/docs/sources/user/Installation-instructions.md
+++ b/docs/sources/user/Installation-instructions.md
@@ -63,5 +63,5 @@ To install the release versions of the dependencies run:
 ```
 set PYTHONPATH=.
 
-C:\Python38\python.exe tools\update.py --preset dfdatetime
+C:\Python3\python.exe tools\update.py --preset dfdatetime
 ```
diff --git a/setup.py b/setup.py
index 524773d..b04a8aa 100755
--- a/setup.py
+++ b/setup.py
@@ -155,22 +155,23 @@ def parse_requirements_from_file(path):
   Args:
     path (str): path to the requirements file.
 
-  Yields:
-    str: name and optional version information of the required package.
+  Returns:
+    list[str]: name and optional version information of the required packages.
   """
   with open(path, 'r') as file_object:
     file_contents = file_object.read()
 
+  requirements = []
   for requirement in pkg_resources.parse_requirements(file_contents):
     try:
       name = str(requirement.req)
     except AttributeError:
       name = str(requirement)
 
-    if name.startswith('pip '):
-      continue
+    if not name.startswith('pip '):
+      requirements.append(name)
 
-    yield name
+  return requirements
 
 
 dfdatetime_description = (


Test patch:
diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml
index dcfb36b..9456538 100644
--- a/.github/workflows/test_docker.yml
+++ b/.github/workflows/test_docker.yml
@@ -6,7 +6,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        version: ['33', '34']
+        version: ['34', '35']
     container:
       image: registry.fedoraproject.org/fedora:${{ matrix.version }}
     steps:
diff --git a/tests/golang_time.py b/tests/golang_time.py
new file mode 100644
index 0000000..b9f63ed
--- /dev/null
+++ b/tests/golang_time.py
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+"""Tests for the Golang time.Time timestamp implementation."""
+
+import decimal
+import struct
+import unittest
+
+from dfdatetime import golang_time
+
+
+class GolangEpochTest(unittest.TestCase):
+  """Test for the Golang time.Time epoch."""
+
+  def testInitialize(self):
+    """Tests the __init__ function."""
+    golang_epoch = golang_time.GolangTimeEpoch()
+    self.assertIsNotNone(golang_epoch)
+
+  def testEpochDate(self):
+    """Tests the Golang time.Time epoch properties."""
+    golang_epoch = golang_time.GolangTimeEpoch()
+    self.assertEqual(golang_epoch.year, 1)
+    self.assertEqual(golang_epoch.month, 1)
+    self.assertEqual(golang_epoch.day_of_month, 1)
+
+
+class GolangTest(unittest.TestCase):
+  """Tests for the Golang time.Time timestamp."""
+
+  # pylint: disable=protected-access
+
+  def testProperties(self):
+    """Tests the Golang time.Time timestamp properties."""
+    golang_timestamp = struct.pack('>Bqih', 1, 0, 0, -1)
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+    self.assertEqual(golang_time_object._number_of_seconds, 0)
+    self.assertEqual(golang_time_object._nanoseconds, 0)
+    self.assertEqual(golang_time_object.is_local_time, False)
+    self.assertEqual(golang_time_object._time_zone_offset, 0)
+
+    golang_timestamp = struct.pack(
+        '>Bqih', 1, golang_time.GolangTime._GOLANG_TO_POSIX_BASE, 0, 1)
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+    self.assertEqual(golang_time_object._number_of_seconds,
+                     golang_time.GolangTime._GOLANG_TO_POSIX_BASE)
+    self.assertEqual(golang_time_object._nanoseconds, 0)
+    self.assertEqual(golang_time_object.is_local_time, True)
+    self.assertEqual(golang_time_object._time_zone_offset, 1)
+
+    golang_timestamp = bytes.fromhex('010000000e7791f70000000000ffff')
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+    self.assertEqual(golang_time_object._number_of_seconds,
+                     golang_time.GolangTime._GOLANG_TO_POSIX_BASE)
+    self.assertEqual(golang_time_object._nanoseconds, 0)
+    self.assertEqual(golang_time_object.is_local_time, False)
+    self.assertEqual(golang_time_object._time_zone_offset, 0)
+
+  def testGetNormalizedTimestamp(self):
+    """Test the _GetNormalizedTimestamp function."""
+    golang_timestamp = bytes.fromhex('010000000000000000000000000000')
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+
+    normalized_timestamp = golang_time_object._GetNormalizedTimestamp()
+    self.assertIsNone(normalized_timestamp)
+
+    golang_timestamp = struct.pack('>Bqih', 1, 63772480949, 711098348, 0)
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+
+    normalized_timestamp = golang_time_object._GetNormalizedTimestamp()
+    self.assertEqual(
+        normalized_timestamp, decimal.Decimal('1636884149.711098348'))
+
+    golang_timestamp = bytes.fromhex('010000000e7791f70000000000ffff')
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+
+    normalized_timestamp = golang_time_object._GetNormalizedTimestamp()
+    self.assertEqual(normalized_timestamp, decimal.Decimal('0'))
+
+    golang_timestamp = bytes.fromhex('010000000e7791f60000000000ffff')
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+
+    normalized_timestamp = golang_time_object._GetNormalizedTimestamp()
+    self.assertIsNone(normalized_timestamp)
+
+  def testGetNumberOfSeconds(self):
+    """Test the _GetNumberOfSeconds function."""
+    golang_time_object = golang_time.GolangTime()
+
+    golang_timestamp = bytes.fromhex('010000000000000002000000030004')
+    number_of_seconds, nanoseconds, time_zone_offset = (
+        golang_time_object._GetNumberOfSeconds(golang_timestamp))
+    self.assertEqual(number_of_seconds, 2)
+    self.assertEqual(nanoseconds, 3)
+    self.assertEqual(time_zone_offset, 4)
+
+    golang_timestamp = bytes.fromhex('02000000000000000500000006ffff08')
+    number_of_seconds, nanoseconds, time_zone_offset = (
+        golang_time_object._GetNumberOfSeconds(golang_timestamp))
+    self.assertEqual(number_of_seconds, 5)
+    self.assertEqual(nanoseconds, 6)
+    self.assertEqual(time_zone_offset, 0)
+
+    with self.assertRaises(ValueError):
+      golang_timestamp = bytes.fromhex('0100')
+      golang_time_object._GetNumberOfSeconds(golang_timestamp)
+
+    with self.assertRaises(ValueError):
+      golang_timestamp = bytes.fromhex('020000000000000000000000000000')
+      golang_time_object._GetNumberOfSeconds(golang_timestamp)
+
+    with self.assertRaises(ValueError):
+      golang_timestamp = bytes.fromhex('ff0000000000000000000000000000')
+      golang_time_object._GetNumberOfSeconds(golang_timestamp)
+
+  def testCopyFromDateTimeString(self):
+    """Tests the CopyFromDateTimeString function."""
+    golang_time_object = golang_time.GolangTime()
+
+    golang_time_object.CopyFromDateTimeString('0001-01-01')
+    self.assertEqual(golang_time_object._number_of_seconds, 0)
+    self.assertEqual(golang_time_object._nanoseconds, 0)
+    self.assertEqual(golang_time_object._time_zone_offset, 0)
+
+    golang_time_object.CopyFromDateTimeString('0001-01-01 00:01:00')
+    self.assertEqual(golang_time_object._number_of_seconds, 60)
+    self.assertEqual(golang_time_object._nanoseconds, 0)
+    self.assertEqual(golang_time_object._time_zone_offset, 0)
+
+    golang_time_object.CopyFromDateTimeString('0001-01-01 00:00:00.000001')
+    self.assertEqual(golang_time_object._number_of_seconds, 0)
+    self.assertEqual(golang_time_object._nanoseconds, 1000)
+    self.assertEqual(golang_time_object._time_zone_offset, 0)
+
+    golang_time_object.CopyFromDateTimeString('2000-01-01')
+    self.assertEqual(golang_time_object._number_of_seconds, 63082281600)
+    self.assertEqual(golang_time_object._nanoseconds, 0)
+    self.assertEqual(golang_time_object._time_zone_offset, 0)
+
+    golang_time_object.CopyFromDateTimeString('2000-01-01 12:23:45.567890')
+    self.assertEqual(golang_time_object._number_of_seconds, 63082326225)
+    self.assertEqual(golang_time_object._nanoseconds, 567890000)
+    self.assertEqual(golang_time_object._time_zone_offset, 0)
+
+    golang_time_object.CopyFromDateTimeString(
+        '2000-01-01 12:23:45.567890+01:00')
+    self.assertEqual(golang_time_object._number_of_seconds, 63082326225)
+    self.assertEqual(golang_time_object._nanoseconds, 567890000)
+    self.assertEqual(golang_time_object._time_zone_offset, 60)
+
+  def testCopyToDateTimeString(self):
+    """Test the CopyToDateTimeString function."""
+    golang_timestamp = bytes.fromhex('010000000eafffe8d121d95050ffff')
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+
+    self.assertEqual(golang_time_object._number_of_seconds, 63082326225)
+    self.assertEqual(golang_time_object._nanoseconds, 567890000)
+    self.assertEqual(golang_time_object._time_zone_offset, 0)
+
+    date_time_string = golang_time_object.CopyToDateTimeString()
+    self.assertEqual(date_time_string, '2000-01-01 12:23:45.567890')
+
+    golang_timestamp = bytes.fromhex('010000000eafffe8d10000ddd5ffff')
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+
+    self.assertEqual(golang_time_object._number_of_seconds, 63082326225)
+    self.assertEqual(golang_time_object._nanoseconds, 56789)
+    self.assertEqual(golang_time_object._time_zone_offset, 0)
+
+    date_time_string = golang_time_object.CopyToDateTimeString()
+    self.assertEqual(date_time_string, '2000-01-01 12:23:45.000056')
diff --git a/tests/posix_time.py b/tests/posix_time.py
index 43a0346..74f476e 100644
--- a/tests/posix_time.py
+++ b/tests/posix_time.py
@@ -95,6 +95,22 @@ class PosixTimeTest(unittest.TestCase):
     date_time_string = posix_time_object.CopyToDateTimeStringISO8601()
     self.assertEqual(date_time_string, '2010-08-12T20:06:31+00:00')
 
+  def testCopyToPosixTimestampWithFractionOfSecond(self):
+    """Tests the CopyToPosixTimestampWithFractionOfSecond function."""
+    posix_time_object = posix_time.PosixTime(timestamp=1281643591)
+
+    posix_timestamp, fraction_of_second = (
+        posix_time_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertEqual(posix_timestamp, 1281643591)
+    self.assertIsNone(fraction_of_second)
+
+    posix_time_object = posix_time.PosixTime()
+
+    posix_timestamp, fraction_of_second = (
+        posix_time_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertIsNone(posix_timestamp)
+    self.assertIsNone(fraction_of_second)
+
   # TODO: remove this method when there is no more need for it in dfvfs.
   def testCopyToStatTimeTuple(self):
     """Tests the CopyToStatTimeTuple function."""
@@ -230,6 +246,23 @@ class PosixTimeInMillisecondsTest(unittest.TestCase):
     date_time_string = posix_time_object.CopyToDateTimeStringISO8601()
     self.assertEqual(date_time_string, '2010-08-12T20:06:31.546+00:00')
 
+  def testCopyToPosixTimestampWithFractionOfSecond(self):
+    """Tests the CopyToPosixTimestampWithFractionOfSecond function."""
+    posix_time_object = posix_time.PosixTimeInMilliseconds(
+        timestamp=1281643591546)
+
+    posix_timestamp, fraction_of_second = (
+        posix_time_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertEqual(posix_timestamp, 1281643591)
+    self.assertEqual(fraction_of_second, 546)
+
+    posix_time_object = posix_time.PosixTime()
+
+    posix_timestamp, fraction_of_second = (
+        posix_time_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertIsNone(posix_timestamp)
+    self.assertIsNone(fraction_of_second)
+
   # TODO: remove this method when there is no more need for it in dfvfs.
   def testCopyToStatTimeTuple(self):
     """Tests the CopyToStatTimeTuple function."""
@@ -369,6 +402,23 @@ class PosixTimeInMicrosecondsTest(unittest.TestCase):
     date_time_string = posix_time_object.CopyToDateTimeStringISO8601()
     self.assertEqual(date_time_string, '2010-08-12T20:06:31.546875+00:00')
 
+  def testCopyToPosixTimestampWithFractionOfSecond(self):
+    """Tests the CopyToPosixTimestampWithFractionOfSecond function."""
+    posix_time_object = posix_time.PosixTimeInMicroseconds(
+        timestamp=1281643591546875)
+
+    posix_timestamp, fraction_of_second = (
+        posix_time_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertEqual(posix_timestamp, 1281643591)
+    self.assertEqual(fraction_of_second, 546875)
+
+    posix_time_object = posix_time.PosixTime()
+
+    posix_timestamp, fraction_of_second = (
+        posix_time_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertIsNone(posix_timestamp)
+    self.assertIsNone(fraction_of_second)
+
   # TODO: remove this method when there is no more need for it in dfvfs.
   def testCopyToStatTimeTuple(self):
     """Tests the CopyToStatTimeTuple function."""
diff --git a/tests/semantic_time.py b/tests/semantic_time.py
index edc5c6c..72b4739 100644
--- a/tests/semantic_time.py
+++ b/tests/semantic_time.py
@@ -85,6 +85,22 @@ class SemanticTimeTest(unittest.TestCase):
     date_time_string = semantic_time_object.CopyToDateTimeStringISO8601()
     self.assertIsNone(date_time_string)
 
+  def testCopyToPosixTimestamp(self):
+    """Tests the CopyToPosixTimestamp function."""
+    semantic_time_object = semantic_time.SemanticTime()
+
+    posix_timestamp = semantic_time_object.CopyToPosixTimestamp()
+    self.assertIsNone(posix_timestamp)
+
+  def testCopyToPosixTimestampWithFractionOfSecond(self):
+    """Tests the CopyToPosixTimestampWithFractionOfSecond function."""
+    semantic_time_object = semantic_time.SemanticTime()
+
+    posix_timestamp, fraction_of_second = (
+        semantic_time_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertIsNone(posix_timestamp)
+    self.assertIsNone(fraction_of_second)
+
   def testCopyToStatTimeTuple(self):
     """Tests the CopyToStatTimeTuple function."""
     semantic_time_object = semantic_time.SemanticTime()
diff --git a/tests/serializer.py b/tests/serializer.py
index 280479c..b3219f8 100644
--- a/tests/serializer.py
+++ b/tests/serializer.py
@@ -5,6 +5,7 @@
 import unittest
 
 from dfdatetime import fat_date_time
+from dfdatetime import golang_time
 from dfdatetime import posix_time
 from dfdatetime import rfc2579_date_time
 from dfdatetime import semantic_time
@@ -62,6 +63,21 @@ class SerializerTest(unittest.TestCase):
         fat_date_time_object)
     self.assertEqual(json_dict, expected_json_dict)
 
+    golang_timestamp = bytes.fromhex('01000000000000000200000003ffff')
+    golang_time_object = golang_time.GolangTime(
+        golang_timestamp=golang_timestamp)
+
+    expected_json_dict = {
+        '__class_name__': 'GolangTime',
+        '__type__': 'DateTimeValues',
+        'golang_timestamp': (
+            b'\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\xff\xff'),
+        'time_zone_offset': 0}
+
+    json_dict = serializer.Serializer.ConvertDateTimeValuesToJSON(
+        golang_time_object)
+    self.assertEqual(json_dict, expected_json_dict)
+
     rfc2579_date_time_object = rfc2579_date_time.RFC2579DateTime(
         rfc2579_date_time_tuple=(2010, 8, 12, 20, 6, 31, 6, '+', 2, 0))
 
diff --git a/tests/time_elements.py b/tests/time_elements.py
index bf5422c..9201d28 100644
--- a/tests/time_elements.py
+++ b/tests/time_elements.py
@@ -686,6 +686,23 @@ class TimeElementsTest(unittest.TestCase):
     posix_timestamp = time_elements_object.CopyToPosixTimestamp()
     self.assertIsNone(posix_timestamp)
 
+  def testCopyToPosixTimestampWithFractionOfSecond(self):
+    """Tests the CopyToPosixTimestampWithFractionOfSecond function."""
+    time_elements_object = time_elements.TimeElements(
+        time_elements_tuple=(2010, 8, 12, 20, 6, 31))
+
+    posix_timestamp, fraction_of_second = (
+        time_elements_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertEqual(posix_timestamp, 1281643591)
+    self.assertIsNone(fraction_of_second)
+
+    time_elements_object = time_elements.TimeElements()
+
+    posix_timestamp, fraction_of_second = (
+        time_elements_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertIsNone(posix_timestamp)
+    self.assertIsNone(fraction_of_second)
+
   def testGetDate(self):
     """Tests the GetDate function."""
     time_elements_object = time_elements.TimeElements(
@@ -997,6 +1014,36 @@ class TimeElementsInMillisecondsTest(unittest.TestCase):
     date_time_string = time_elements_object.CopyToDateTimeStringISO8601()
     self.assertEqual(date_time_string, '2010-08-12T20:06:31.429+00:00')
 
+  def testCopyToPosixTimestamp(self):
+    """Tests the CopyToPosixTimestamp function."""
+    time_elements_object = time_elements.TimeElementsInMilliseconds(
+        time_elements_tuple=(2010, 8, 12, 20, 6, 31, 429))
+
+    posix_timestamp = time_elements_object.CopyToPosixTimestamp()
+    self.assertEqual(posix_timestamp, 1281643591)
+
+    time_elements_object = time_elements.TimeElements()
+
+    posix_timestamp = time_elements_object.CopyToPosixTimestamp()
+    self.assertIsNone(posix_timestamp)
+
+  def testCopyToPosixTimestampWithFractionOfSecond(self):
+    """Tests the CopyToPosixTimestampWithFractionOfSecond function."""
+    time_elements_object = time_elements.TimeElementsInMilliseconds(
+        time_elements_tuple=(2010, 8, 12, 20, 6, 31, 429))
+
+    posix_timestamp, fraction_of_second = (
+        time_elements_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertEqual(posix_timestamp, 1281643591)
+    self.assertEqual(fraction_of_second, 429)
+
+    time_elements_object = time_elements.TimeElements()
+
+    posix_timestamp, fraction_of_second = (
+        time_elements_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertIsNone(posix_timestamp)
+    self.assertIsNone(fraction_of_second)
+
   def testGetDate(self):
     """Tests the GetDate function."""
     time_elements_object = time_elements.TimeElementsInMilliseconds(
@@ -1309,6 +1356,36 @@ class TimeElementsInMicrosecondsTest(unittest.TestCase):
     date_time_string = time_elements_object.CopyToDateTimeStringISO8601()
     self.assertEqual(date_time_string, '2010-08-12T20:06:31.429876+00:00')
 
+  def testCopyToPosixTimestamp(self):
+    """Tests the CopyToPosixTimestamp function."""
+    time_elements_object = time_elements.TimeElementsInMicroseconds(
+        time_elements_tuple=(2010, 8, 12, 20, 6, 31, 429876))
+
+    posix_timestamp = time_elements_object.CopyToPosixTimestamp()
+    self.assertEqual(posix_timestamp, 1281643591)
+
+    time_elements_object = time_elements.TimeElements()
+
+    posix_timestamp = time_elements_object.CopyToPosixTimestamp()
+    self.assertIsNone(posix_timestamp)
+
+  def testCopyToPosixTimestampWithFractionOfSecond(self):
+    """Tests the CopyToPosixTimestampWithFractionOfSecond function."""
+    time_elements_object = time_elements.TimeElementsInMicroseconds(
+        time_elements_tuple=(2010, 8, 12, 20, 6, 31, 429876))
+
+    posix_timestamp, fraction_of_second = (
+        time_elements_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertEqual(posix_timestamp, 1281643591)
+    self.assertEqual(fraction_of_second, 429876)
+
+    time_elements_object = time_elements.TimeElements()
+
+    posix_timestamp, fraction_of_second = (
+        time_elements_object.CopyToPosixTimestampWithFractionOfSecond())
+    self.assertIsNone(posix_timestamp)
+    self.assertIsNone(fraction_of_second)
+
   def testGetDate(self):
     """Tests the GetDate function."""
     time_elements_object = time_elements.TimeElementsInMicroseconds(