Commit 3dab1bae authored by Hilko Bengen's avatar Hilko Bengen

New upstream version 20181025

parent af4cf038
# Pylint 1.7.x configuration file
# Pylint 1.7.x - 1.9.x configuration file
#
# This file is generated by l2tdevtools update-dependencies.py, any dependency
# related changes should be made in dependencies.ini.
......@@ -55,24 +55,9 @@ confidence=
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
#
# Additional:
# fixme
# logging-format-interpolation
# no-self-use
# too-few-public-methods
# too-many-ancestors
# too-many-arguments
# too-many-boolean-expressions
# too-many-branches
# too-many-instance-attributes
# too-many-lines
# too-many-locals
# too-many-nested-blocks
# too-many-public-methods
# too-many-return-statements
# too-many-statements
# unsubscriptable-object
disable=parameter-unpacking,
disable=
duplicate-code,
parameter-unpacking,
raw-checker-failed,
bad-inline-option,
locally-disabled,
......
environment:
matrix:
- TARGET: python27
MACHINE_TYPE: "x86"
PYTHON: "C:\\Python27"
- TARGET: python27
MACHINE_TYPE: "amd64"
PYTHON: "C:\\Python27-x64"
- TARGET: python36
MACHINE_TYPE: "x86"
PYTHON: "C:\\Python36"
- TARGET: python36
MACHINE_TYPE: "amd64"
PYTHON: "C:\\Python36-x64"
install:
- cmd: '"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x86 /release'
- cmd: "%PYTHON%\\python.exe -m pip install --upgrade pip"
- cmd: "%PYTHON%\\Scripts\\pip.exe install pywin32 WMI"
- cmd: "%PYTHON%\\python.exe -m pip install pywin32 WMI"
- cmd: "%PYTHON%\\python.exe %PYTHON%\\Scripts\\pywin32_postinstall.py -install"
- cmd: git clone https://github.com/log2timeline/l2tdevtools.git ..\l2tdevtools
- cmd: if [%TARGET%]==[python27] (
mkdir dependencies &&
set PYTHONPATH=..\l2tdevtools &&
"%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type x86 --msi-targetdir "%PYTHON%" --track dev funcsigs mock pbr six )
"%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type %MACHINE_TYPE% --msi-targetdir "%PYTHON%" --track dev funcsigs mock pbr six )
- cmd: if [%TARGET%]==[python36] (
mkdir dependencies &&
set PYTHONPATH=..\l2tdevtools &&
"%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type x86 --msi-targetdir "%PYTHON%" --track dev funcsigs mock pbr six )
"%PYTHON%\\python.exe" ..\l2tdevtools\tools\update.py --download-directory dependencies --machine-type %MACHINE_TYPE% --msi-targetdir "%PYTHON%" --track dev funcsigs mock pbr six )
build: off
......
dfdatetime (20180704-1) unstable; urgency=low
dfdatetime (20181025-1) unstable; urgency=low
* Auto-generated
-- Log2Timeline <log2timeline-dev@googlegroups.com> Wed, 04 Jul 2018 20:18:28 +0200
\ No newline at end of file
-- Log2Timeline maintainers <log2timeline-maintainers@googlegroups.com> Thu, 25 Oct 2018 12:44:52 -0700
\ No newline at end of file
......@@ -42,7 +42,7 @@ then
sudo /usr/bin/hdiutil detach /Volumes/${PACKAGE}-*.pkg
done
elif test ${TRAVIS_OS_NAME} = "linux";
elif test ${TRAVIS_OS_NAME} = "linux" && test ${TARGET} != "jenkins";
then
sudo rm -f /etc/apt/sources.list.d/travis_ci_zeromq3-source.list;
......
......@@ -41,3 +41,7 @@ do
sleep 60;
done
wait ${PID_COMMAND};
exit $?;
......@@ -8,7 +8,11 @@
# Exit on error.
set -e;
if test "${TARGET}" = "pylint";
if test "${TARGET}" = "jenkins";
then
./config/jenkins/linux/run_end_to_end_tests.sh "travis";
elif test "${TARGET}" = "pylint";
then
pylint --version
......@@ -29,6 +33,11 @@ then
python ./setup.py bdist
if test -f tests/end-to-end.py;
then
PYTHONPATH=. python ./tests/end-to-end.py --debug -c config/end-to-end.ini;
fi
elif test "${TRAVIS_OS_NAME}" = "linux";
then
if test -n "${TOXENV}";
......@@ -55,4 +64,9 @@ then
mkdir -p ${TMPSITEPACKAGES};
PYTHONPATH=${TMPSITEPACKAGES} python ./setup.py install --prefix=${TMPDIR};
if test -f tests/end-to-end.py;
then
PYTHONPATH=. python ./tests/end-to-end.py --debug -c config/end-to-end.ini;
fi
fi
[project]
name: dfdatetime
status: alpha
name_description: dfDateTime
maintainer: Log2Timeline maintainers <log2timeline-maintainers@googlegroups.com>
homepage_url: https://github.com/log2timeline/dfdatetime
......
......@@ -5,4 +5,4 @@ dfDateTime, or Digital Forensics date and time, provides date and time
objects to preserve accuracy and precision.
"""
__version__ = '20180704'
__version__ = '20181025'
# -*- coding: utf-8 -*-
"""Apple File System (APFS) time implementation."""
from __future__ import unicode_literals
import decimal
from dfdatetime import definitions
from dfdatetime import posix_time
class APFSTime(posix_time.PosixTimeInNanoseconds):
"""Apple File System (APFS) timestamp.
The APFS timestamp is a signed 64-bit integer that contains the number of
nanoseconds since 1970-01-01 00:00:00.
Attributes:
is_local_time (bool): True if the date and time value is in local time.
"""
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._timestamp is not None and self._timestamp >= self._INT64_MIN and
self._timestamp <= self._INT64_MAX):
self._normalized_timestamp = (
decimal.Decimal(self._timestamp) /
definitions.NANOSECONDS_PER_SECOND)
return self._normalized_timestamp
def CopyFromDateTimeString(self, time_string):
"""Copies a APFS timestamp 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.
"""
super(APFSTime, self)._CopyFromDateTimeString(time_string)
if (self._timestamp is None or self._timestamp < self._INT64_MIN or
self._timestamp > self._INT64_MAX):
raise ValueError('Date time value not supported.')
def CopyToDateTimeString(self):
"""Copies the APFS timestamp 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 is missing or invalid.
"""
if (self._timestamp is None or self._timestamp < self._INT64_MIN or
self._timestamp > self._INT64_MAX):
return None
return super(APFSTime, self)._CopyToDateTimeString()
......@@ -21,6 +21,8 @@ MICROSECONDS_PER_SECOND = 1000000
MICROSECONDS_PER_DECISECOND = 100000
MICROSECONDS_PER_MILLISECOND = 1000
NANOSECONDS_PER_SECOND = 1000000000
PRECISION_1_DAY = '1d'
PRECISION_1_HOUR = '1h'
PRECISION_1_NANOSECOND = '1ns'
......
......@@ -6,11 +6,10 @@ from __future__ import unicode_literals
import decimal
from dfdatetime import definitions
from dfdatetime import interface
from dfdatetime import posix_time
class JavaTime(interface.DateTimeValues):
class JavaTime(posix_time.PosixTimeInMilliseconds):
"""Java java.util.Date timestamp.
The Java java.util.Date timestamp is a signed integer that contains the
......@@ -24,23 +23,6 @@ class JavaTime(interface.DateTimeValues):
is_local_time (bool): True if the date and time value is in local time.
"""
_EPOCH = posix_time.PosixTimeEpoch()
def __init__(self, timestamp=None):
"""Initializes a Java timestamp.
Args:
timestamp (Optional[int]): Java timestamp.
"""
super(JavaTime, self).__init__()
self._precision = definitions.PRECISION_1_MILLISECOND
self._timestamp = timestamp
@property
def timestamp(self):
"""int: Java timestamp or None if timestamp is not set."""
return self._timestamp
def _GetNormalizedTimestamp(self):
"""Retrieves the normalized timestamp.
......@@ -59,58 +41,15 @@ class JavaTime(interface.DateTimeValues):
return self._normalized_timestamp
def CopyFromDateTimeString(self, time_string):
"""Copies a Java timestamp 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.
"""
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', None)
timestamp = self._GetNumberOfSecondsFromElements(
year, month, day_of_month, hours, minutes, seconds)
timestamp *= definitions.MILLISECONDS_PER_SECOND
if microseconds:
milliseconds, _ = divmod(
microseconds, definitions.MILLISECONDS_PER_SECOND)
timestamp += milliseconds
self._normalized_timestamp = None
self._timestamp = timestamp
self.is_local_time = False
def CopyToDateTimeString(self):
"""Copies the Java timestamp to a date and time string.
"""Copies the POSIX timestamp 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 is missing or invalid.
str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss.######" or
None if the timestamp is missing.
"""
if (self._timestamp is None or self._timestamp < self._INT64_MIN or
self._timestamp > self._INT64_MAX):
return None
timestamp, milliseconds = divmod(
self._timestamp, definitions.MILLISECONDS_PER_SECOND)
number_of_days, hours, minutes, seconds = self._GetTimeValues(timestamp)
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:03d}'.format(
year, month, day_of_month, hours, minutes, seconds, milliseconds)
return super(JavaTime, self).CopyToDateTimeString()
......@@ -109,6 +109,104 @@ class PosixTime(interface.DateTimeValues):
year, month, day_of_month, hours, minutes, seconds)
class PosixTimeInMilliseconds(interface.DateTimeValues):
"""POSIX timestamp in milliseconds.
Variant of the POSIX timestamp in milliseconds.
Attributes:
is_local_time (bool): True if the date and time value is in local time.
"""
_EPOCH = PosixTimeEpoch()
def __init__(self, timestamp=None):
"""Initializes a POSIX timestamp in milliseconds.
Args:
timestamp (Optional[int]): POSIX timestamp in milliseconds.
"""
super(PosixTimeInMilliseconds, self).__init__()
self._precision = definitions.PRECISION_1_MILLISECOND
self._timestamp = timestamp
@property
def timestamp(self):
"""int: POSIX timestamp in milliseconds or None if timestamp is not set."""
return self._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._timestamp is not None:
self._normalized_timestamp = (
decimal.Decimal(self._timestamp) /
definitions.MILLISECONDS_PER_SECOND)
return self._normalized_timestamp
def CopyFromDateTimeString(self, time_string):
"""Copies a POSIX timestamp 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.
"""
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)
timestamp = self._GetNumberOfSecondsFromElements(
year, month, day_of_month, hours, minutes, seconds)
timestamp *= definitions.MILLISECONDS_PER_SECOND
if microseconds:
milliseconds, _ = divmod(
microseconds, definitions.MILLISECONDS_PER_SECOND)
timestamp += milliseconds
self._timestamp = timestamp
self.is_local_time = False
def CopyToDateTimeString(self):
"""Copies the POSIX timestamp 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 is missing.
"""
if self._timestamp is None:
return None
timestamp, milliseconds = divmod(
self._timestamp, definitions.MILLISECONDS_PER_SECOND)
number_of_days, hours, minutes, seconds = self._GetTimeValues(timestamp)
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:03d}'.format(
year, month, day_of_month, hours, minutes, seconds, milliseconds)
class PosixTimeInMicroseconds(interface.DateTimeValues):
"""POSIX timestamp in microseconds.
......@@ -201,3 +299,123 @@ class PosixTimeInMicroseconds(interface.DateTimeValues):
return '{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}.{6:06d}'.format(
year, month, day_of_month, hours, minutes, seconds, microseconds)
class PosixTimeInNanoseconds(interface.DateTimeValues):
"""POSIX timestamp in nanoseconds.
Variant of the POSIX timestamp in nanoseconds.
Attributes:
is_local_time (bool): True if the date and time value is in local time.
"""
_EPOCH = PosixTimeEpoch()
def __init__(self, timestamp=None):
"""Initializes a POSIX timestamp in nanoseconds.
Args:
timestamp (Optional[int]): POSIX timestamp in nanoseconds.
"""
super(PosixTimeInNanoseconds, self).__init__()
self._precision = definitions.PRECISION_1_NANOSECOND
self._timestamp = timestamp
@property
def timestamp(self):
"""int: POSIX timestamp or None if timestamp is not set."""
return self._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._timestamp is not None:
self._normalized_timestamp = (
decimal.Decimal(self._timestamp) /
definitions.NANOSECONDS_PER_SECOND)
return self._normalized_timestamp
def _CopyFromDateTimeString(self, time_string):
"""Copies a POSIX timestamp 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.
"""
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', None)
timestamp = self._GetNumberOfSecondsFromElements(
year, month, day_of_month, hours, minutes, seconds)
timestamp *= definitions.NANOSECONDS_PER_SECOND
if microseconds:
nanoseconds = microseconds * definitions.MILLISECONDS_PER_SECOND
timestamp += nanoseconds
self._normalized_timestamp = None
self._timestamp = timestamp
def CopyFromDateTimeString(self, time_string):
"""Copies a POSIX timestamp 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.
"""
self._CopyFromDateTimeString(time_string)
def _CopyToDateTimeString(self):
"""Copies the POSIX timestamp 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 is missing or invalid.
"""
if self._timestamp is None:
return None
timestamp, nanoseconds = divmod(
self._timestamp, definitions.NANOSECONDS_PER_SECOND)
number_of_days, hours, minutes, seconds = self._GetTimeValues(timestamp)
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:09d}'.format(
year, month, day_of_month, hours, minutes, seconds, nanoseconds)
def CopyToDateTimeString(self):
"""Copies the POSIX timestamp 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 is missing or invalid.
"""
return self._CopyToDateTimeString()
......@@ -3,6 +3,8 @@
"""Installation and deployment script."""
from __future__ import print_function
import locale
import sys
try:
......@@ -20,9 +22,21 @@ try:
except ImportError:
bdist_rpm = None
if sys.version < '2.7':
version_tuple = (sys.version_info[0], sys.version_info[1])
if version_tuple[0] not in (2, 3):
print('Unsupported Python version: {0:s}.'.format(sys.version))
print('Supported Python versions are 2.7 or a later 2.x version.')
sys.exit(1)
elif version_tuple[0] == 2 and version_tuple < (2, 7):
print((
'Unsupported Python 2 version: {0:s}, version 2.7 or higher '
'required.').format(sys.version))
sys.exit(1)
elif version_tuple[0] == 3 and version_tuple < (3, 4):
print((
'Unsupported Python 3 version: {0:s}, version 3.4 or higher '
'required.').format(sys.version))
sys.exit(1)
# Change PYTHONPATH to include dfdatetime so that we can get the version.
......@@ -65,7 +79,7 @@ else:
spec_file = bdist_rpm._make_spec_file(self)
if sys.version_info[0] < 3:
python_package = 'python'
python_package = 'python2'
else:
python_package = 'python3'
......@@ -79,28 +93,51 @@ else:
summary = line
elif line.startswith('BuildRequires: '):
line = 'BuildRequires: {0:s}-setuptools'.format(python_package)
line = 'BuildRequires: {0:s}-setuptools, {0:s}-devel'.format(
python_package)
elif line.startswith('Requires: '):
if python_package == 'python3':
line = line.replace('python', 'python3')
line = line.replace('python-', 'python3-')
line = line.replace('python2-', 'python3-')
elif line.startswith('%description'):
in_description = True
elif line.startswith('python setup.py build'):
if python_package == 'python3':
line = '%py3_build'
else:
line = '%py2_build'
elif line.startswith('python setup.py install'):
if python_package == 'python3':
line = '%py3_install'
else:
line = '%py2_install'
elif line.startswith('%files'):
# Cannot use %{_libdir} here since it can expand to "lib64".
lines = [
'%files -n {0:s}-%{{name}}'.format(python_package),
'%defattr(644,root,root,755)',
'%doc ACKNOWLEDGEMENTS AUTHORS LICENSE README',
'%{_prefix}/lib/python*/site-packages/**/*.py',
'%{_prefix}/lib/python*/site-packages/dfdatetime*.egg-info/*',
'',
'%exclude %{_prefix}/share/doc/*',
'%exclude %{_prefix}/lib/python*/site-packages/**/*.pyc',
'%exclude %{_prefix}/lib/python*/site-packages/**/*.pyo',
'%exclude %{_prefix}/lib/python*/site-packages/**/__pycache__/*']
'%doc ACKNOWLEDGEMENTS AUTHORS LICENSE README']
if python_package == 'python3':
lines.extend([
'%{python3_sitelib}/**/*.py',
'%{python3_sitelib}/dfdatetime*.egg-info/*',
'',
'%exclude %{_prefix}/share/doc/*',
'%exclude %{python3_sitelib}/**/__pycache__/*'])
else:
lines.extend([
'%{python2_sitelib}/**/*.py',
'%{python2_sitelib}/dfdatetime*.egg-info/*',
'',
'%exclude %{_prefix}/share/doc/*',
'%exclude %{python2_sitelib}/**/*.pyc',
'%exclude %{python2_sitelib}/**/*.pyo'])
python_spec_file.extend(lines)
break
......@@ -110,6 +147,12 @@ else:
python_spec_file.append(
'%package -n {0:s}-%{{name}}'.format(python_package))
if python_package == 'python2':
python_spec_file.append(
'Obsoletes: python-dfdatetime < %{version}')
python_spec_file.append(
'Provides: python-dfdatetime = %{version}')
python_spec_file.append('{0:s}'.format(summary))
python_spec_file.append('')
python_spec_file.append(
......@@ -128,6 +171,20 @@ else:
return python_spec_file
if version_tuple[0] == 2:
encoding = sys.stdin.encoding # pylint: disable=invalid-name
# Note that sys.stdin.encoding can be None.
if not encoding:
encoding = locale.getpreferredencoding()
# Make sure the default encoding is set correctly otherwise on Python 2
# setup.py sdist will fail to include filenames with Unicode characters.
reload(sys) # pylint: disable=undefined-variable
sys.setdefaultencoding(encoding) # pylint: disable=no-member
dfdatetime_description = (
'Digital Forensics date and time (dfDateTime).')
......@@ -142,8 +199,8 @@ setup(
long_description=dfdatetime_long_description,
license='Apache License, Version 2.0',
url='https://github.com/log2timeline/dfdatetime',
maintainer='dfDateTime development team',
maintainer_email='log2timeline-dev@googlegroups.com',
maintainer='Log2Timeline maintainers',
maintainer_email='log2timeline-maintainers@googlegroups.com',
cmdclass={
'bdist_msi': BdistMSICommand,
'bdist_rpm': BdistRPMCommand},
......@@ -154,7 +211,7 @@ setup(
'Programming Language :: Python',
],
packages=find_packages('.', exclude=[
'examples', 'tests', 'tests.*', 'utils']),
'tests', 'tests.*', 'utils']),
package_dir={
'dfdatetime': 'dfdatetime'
},
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Tests for the APFS timestamp implementation."""
from __future__ import unicode_literals
import decimal
import unittest
from dfdatetime import apfs_time
class APFSTimeTest(unittest.TestCase):
"""Tests for the APFS timestamp."""
# pylint: disable=protected-access
def testGetNormalizedTimestamp(self):
"""Tests the _GetNormalizedTimestamp function."""
apfs_time_object = apfs_time.APFSTime(timestamp=1281643591987654321)
normalized_timestamp = apfs_time_object._GetNormalizedTimestamp()
self.assertEqual(
normalized_timestamp, decimal.Decimal('1281643591.987654321'))
apfs_time_object = apfs_time.APFSTime()
normalized_timestamp = apfs_time_object._GetNormalizedTimestamp()
self.assertIsNone(normalized_timestamp)
apfs_time_object = apfs_time.APFSTime(timestamp=9223372036854775810)
date_time_string = apfs_time_object._GetNormalizedTimestamp()
self.assertIsNone(date_time_string)
def testCopyFromDateTimeString(self):