#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright 2012 The Plaso Project Authors. # Please see the AUTHORS file for details on individual authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """This file contains functions and variables used for time manipulations. This file should contain common methods that can be used in Plaso to convert timestamps in various formats into the standard micro seconds precision integer Epoch UTC time that is used internally to store timestamps in Plaso. The file can also contain common functions to change the default timestamp into a more human readable one. """ import calendar import datetime import dateutil.parser import logging import time import pytz MONTH_DICT = { 'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} class Timestamp(object): """Class for converting timestamps to plaso timestamps. The Plaso timestamp is a 64-bit signed timestamp value containing: micro seconds since 1970-01-01 00:00:00. The timestamp is not necessarily in UTC. """ # The minimum timestamp in seconds TIMESTAMP_MIN_SECONDS = -(((1 << 63L) - 1) / 1000000) # The maximum timestamp in seconds TIMESTAMP_MAX_SECONDS = ((1 << 63L) - 1) / 1000000 # The minimum timestamp in micro seconds TIMESTAMP_MIN_MICRO_SECONDS = -((1 << 63L) - 1) # The maximum timestamp in micro seconds TIMESTAMP_MAX_MICRO_SECONDS = (1 << 63L) - 1 # The days per month of a non leap year DAYS_PER_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] # The number of seconds in a day SECONDS_PER_DAY = 24 * 60 * 60 # The number of micro seconds per second MICRO_SECONDS_PER_SECOND = 1000000 # The multiplication factor to change milli seconds to micro seconds. MILLI_SECONDS_TO_MICRO_SECONDS = 1000 # The difference between Jan 1, 1980 and Jan 1, 1970 in seconds. FAT_DATE_TO_POSIX_BASE = 315532800 # The difference between Jan 1, 1601 and Jan 1, 1970 in micro seconds WEBKIT_TIME_TO_POSIX_BASE = 11644473600L * 1000000 # The difference between Jan 1, 1601 and Jan 1, 1970 in 100s of nanoseconds. FILETIME_TO_POSIX_BASE = 11644473600L * 10000000 # The number of seconds between January 1, 1904 and Jan 1, 1970. # Value confirmed with sleuthkit: # http://svn.sleuthkit.org/repos/sleuthkit/trunk/tsk3/fs/tsk_hfs.h # and linux source file linux/include/linux/hfsplus_fs.h HFSTIME_TO_POSIX_BASE = 2082844800 # The number of seconds between January 1, 1970 and January 1, 2001. # As specified in: # https://developer.apple.com/library/ios/documentation/ # cocoa/Conceptual/DatesAndTimes/Articles/dtDates.html COCOA_TIME_TO_POSIX_BASE = 978307200 # The difference between POSIX (Jan 1, 1970) and DELPHI (Dec 30, 1899). # http://docwiki.embarcadero.com/Libraries/XE3/en/System.TDateTime DELPHI_TIME_TO_POSIX_BASE = 25569 @classmethod def CopyToDatetime(cls, timestamp, timezone, raise_error=False): """Copies the timestamp to a datetime object. Args: timestamp: An integer containing the timestamp. timezone: The timezone (pytz.timezone) object. raise_error: Boolean that if set to True will not absorb an OverflowError if the timestamp is out of bounds. By default there will be no error raised. Returns: A datetime object. Raises: OverflowError: If raises_error is set to True and an OverflowError error occurs. Otherwise the error is absorbed and a datetime object from the beginning of UNIX Epoch is returned. """ datetime_object = datetime.datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=pytz.utc) try: datetime_object += datetime.timedelta(microseconds=timestamp) return datetime_object.astimezone(timezone) except OverflowError as exception: if raise_error: raise else: logging.error(( u'Unable to copy {0:d} to a datetime object with error: ' u'{1:s}').format(timestamp, exception)) return datetime_object @classmethod def CopyToIsoFormat(cls, timestamp, timezone=pytz.utc, raise_error=False): """Copies the timestamp to an ISO 8601 formatted string. Args: timestamp: An integer containing the timestamp. timezone: Optional timezone (instance of pytz.timezone). The default is UTC. raise_error: Boolean that if set to True will not absorb an OverflowError if the timestamp is out of bounds. By default there will be no error raised. Returns: A string containing an ISO 8601 formatted date and time. """ datetime_object = cls.CopyToDatetime( timestamp, timezone, raise_error=raise_error) return datetime_object.isoformat() @classmethod def CopyToPosix(cls, timestamp): """Converts microsecond timestamps to POSIX timestamps. Args: timestamp: An integer containing the microsecond timestamp. Returns: An integer value containing the timestamp. """ return timestamp // cls.MICRO_SECONDS_PER_SECOND @classmethod def DaysInMonth(cls, month, year): """Determines the days in a month for a specific year. Args: month: The month where 0 represents January. year: The year as in 1970. Returns: An integer containing the number of days in the month. Raises: ValueError: if the month value is invalid. """ if month not in range(0, 12): raise ValueError(u'Invalid month value') days_per_month = cls.DAYS_PER_MONTH[month] if month == 1 and cls.IsLeapYear(year): days_per_month += 1 return days_per_month @classmethod def DaysInYear(cls, year): """Determines the days in a year. Args: year: The year as in 1970. Returns: An integer containing the number of days in the year. """ days_in_year = 365 if cls.IsLeapYear(year): return days_in_year + 1 return days_in_year @classmethod def DayOfYear(cls, day, month, year): """Determines the day of the year for a specific day of a month in a year. Args: day: The day of the month where 0 represents the first day. month: The month where 0 represents January. year: The year as in 1970. Returns: An integer containing the day of year. """ day_of_year = day for past_month in range(0, month): day_of_year += cls.DaysInMonth(past_month, year) return day_of_year @classmethod def FromCocoaTime(cls, cocoa_time): """Converts a Cocoa time to a timestamp. In Cocoa, time and date values are stored in a unsigned 32-bit integer containing the number of seconds since January 1, 2001 at 00:00:00 (midnight) UTC (GMT). Args: cocoa_time: The timestamp in Cocoa format. Returns: An integer containing the timestamp or 0 on error. """ return cls.FromPosixTime(cocoa_time + cls.COCOA_TIME_TO_POSIX_BASE) @classmethod def FromDelphiTime(cls, delphi_time): """Converts a Delphi time to a timestamp. In Delphi, time and date values (TDateTime) are stored in a unsigned little endian 64-bit floating point containing the number of seconds since December 30, 1899 at 00:00:00 (midnight) Local Timezone. TDateTime does not have any time zone information. Args: delphi_time: The timestamp in Delphi format. Returns: An integer containing the timestamp or 0 on error. """ posix_time = (delphi_time - cls.DELPHI_TIME_TO_POSIX_BASE) * 86400.0 if (posix_time < cls.TIMESTAMP_MIN_SECONDS or posix_time > cls.TIMESTAMP_MAX_SECONDS): return 0 return cls.FromPosixTime(int(posix_time)) @classmethod def FromFatDateTime(cls, fat_date_time): """Converts a FAT date and time into a timestamp. FAT date time is mainly used in DOS/Windows file formats and FAT. The FAT date and time is a 32-bit value containing two 16-bit values: * The date (lower 16-bit). * bits 0 - 4: day of month, where 1 represents the first day * bits 5 - 8: month of year, where 1 represent January * bits 9 - 15: year since 1980 * The time of day (upper 16-bit). * bits 0 - 4: seconds (in 2 second intervals) * bits 5 - 10: minutes * bits 11 - 15: hours Args: fat_date_time: The 32-bit FAT date time. Returns: An integer containing the timestamp or 0 on error. """ number_of_seconds = cls.FAT_DATE_TO_POSIX_BASE day_of_month = (fat_date_time & 0x1f) - 1 month = ((fat_date_time >> 5) & 0x0f) - 1 year = (fat_date_time >> 9) & 0x7f if day_of_month < 0 or day_of_month > 30 or month < 0 or month > 11: return 0 number_of_days = cls.DayOfYear(day_of_month, month, 1980 + year) for past_year in range(0, year): number_of_days += cls.DaysInYear(past_year) fat_date_time >>= 16 seconds = (fat_date_time & 0x1f) * 2 minutes = (fat_date_time >> 5) & 0x3f hours = (fat_date_time >> 11) & 0x1f if hours > 23 or minutes > 59 or seconds > 59: return 0 number_of_seconds += (((hours * 60) + minutes) * 60) + seconds number_of_seconds += number_of_days * cls.SECONDS_PER_DAY return number_of_seconds * cls.MICRO_SECONDS_PER_SECOND @classmethod def FromFiletime(cls, filetime): """Converts a FILETIME into a timestamp. FILETIME is mainly used in Windows file formats and NTFS. The FILETIME is a 64-bit value containing: 100th nano seconds since 1601-01-01 00:00:00 Technically FILETIME consists of 2 x 32-bit parts and is presumed to be unsigned. Args: filetime: The 64-bit FILETIME timestamp. Returns: An integer containing the timestamp or 0 on error. """ # TODO: Add a handling for if the timestamp equals to zero. if filetime < 0: return 0 timestamp = (filetime - cls.FILETIME_TO_POSIX_BASE) / 10 if timestamp > cls.TIMESTAMP_MAX_MICRO_SECONDS: return 0 return timestamp @classmethod def FromHfsTime(cls, hfs_time, timezone=pytz.utc, is_dst=False): """Converts a HFS time to a timestamp. HFS time is the same as HFS+ time, except stored in the local timezone of the user. Args: hfs_time: Timestamp in the hfs format (32 bit unsigned int). timezone: The timezone object of the system's local time. is_dst: A boolean to indicate the timestamp is corrected for daylight savings time (DST) only used for the DST transition period. The default is false. Returns: An integer containing the timestamp or 0 on error. """ timestamp_local = cls.FromHfsPlusTime(hfs_time) return cls.LocaltimeToUTC(timestamp_local, timezone, is_dst) @classmethod def FromHfsPlusTime(cls, hfs_time): """Converts a HFS+ time to a timestamp. In HFS+ date and time values are stored in an unsigned 32-bit integer containing the number of seconds since January 1, 1904 at 00:00:00 (midnight) UTC (GMT). Args: hfs_time: The timestamp in HFS+ format. Returns: An integer containing the timestamp or 0 on error. """ return cls.FromPosixTime(hfs_time - cls.HFSTIME_TO_POSIX_BASE) @classmethod def FromJavaTime(cls, java_time): """Converts a Java time to a timestamp. Jave time is the number of milliseconds since January 1, 1970, 00:00:00 UTC. URL: http://docs.oracle.com/javase/7/docs/api/ java/sql/Timestamp.html#getTime%28%29 Args: java_time: The Java Timestamp. Returns: An integer containing the timestamp or 0 on error. """ return java_time * cls.MILLI_SECONDS_TO_MICRO_SECONDS @classmethod def FromPosixTime(cls, posix_time): """Converts a POSIX timestamp into a timestamp. The POSIX time is a signed 32-bit or 64-bit value containing: seconds since 1970-01-01 00:00:00 Args: posix_time: The POSIX timestamp. Returns: An integer containing the timestamp or 0 on error. """ if (posix_time < cls.TIMESTAMP_MIN_SECONDS or posix_time > cls.TIMESTAMP_MAX_SECONDS): return 0 return int(posix_time) * cls.MICRO_SECONDS_PER_SECOND @classmethod def FromPosixTimeWithMicrosecond(cls, posix_time, microsecond): """Converts a POSIX timestamp with microsecond into a timestamp. The POSIX time is a signed 32-bit or 64-bit value containing: seconds since 1970-01-01 00:00:00 Args: posix_time: The POSIX timestamp. microsecond: The microseconds to add to the timestamp. Returns: An integer containing the timestamp or 0 on error. """ timestamp = cls.FromPosixTime(posix_time) if not timestamp: return 0 return timestamp + microsecond @classmethod def FromPythonDatetime(cls, datetime_object): """Converts a Python datetime object into a timestamp.""" if not isinstance(datetime_object, datetime.datetime): return 0 posix_epoch = int(calendar.timegm(datetime_object.utctimetuple())) epoch = cls.FromPosixTime(posix_epoch) return epoch + datetime_object.microsecond @classmethod def FromTimeParts( cls, year, month, day, hour, minutes, seconds, microseconds=0, timezone=pytz.utc): """Converts a list of time entries to a timestamp. Args: year: An integer representing the year. month: An integer between 1 and 12. day: An integer representing the number of day in the month. hour: An integer representing the hour, 0 <= hour < 24. minutes: An integer, 0 <= minute < 60. seconds: An integer, 0 <= second < 60. microseconds: Optional number of microseconds ranging from: 0 <= microsecond < 1000000. The default is 0. timezone: Optional timezone (instance of pytz.timezone). The default is UTC. Returns: An integer containing the timestamp or 0 on error. """ try: date = datetime.datetime( year, month, day, hour, minutes, seconds, microseconds) except ValueError as exception: logging.warning(( u'Unable to create timestamp from {0:04d}-{1:02d}-{2:02d} ' u'{3:02d}:{4:02d}:{5:02d}.{6:06d} with error: {7:s}').format( year, month, day, hour, minutes, seconds, microseconds, exception)) return 0 if type(timezone) is str: timezone = pytz.timezone(timezone) date_use = timezone.localize(date) epoch = int(calendar.timegm(date_use.utctimetuple())) return cls.FromPosixTime(epoch) + microseconds @classmethod def FromTimeString( cls, time_string, timezone=pytz.utc, dayfirst=False, gmt_as_timezone=True): """Converts a string containing a date and time value into a timestamp. Args: time_string: String that contains a date and time value. timezone: Optional timezone object (instance of pytz.timezone) that the data and time value in the string represents. This value is used when the timezone cannot be determined from the string. dayfirst: An optional boolean argument. If set to true then the parser will change the precedence in which it parses timestamps from MM-DD-YYYY to DD-MM-YYYY (and YYYY-MM-DD will be YYYY-DD-MM, etc). gmt_as_timezone: Sometimes the dateutil parser will interpret GMT and UTC the same way, that is not make a distinction. By default this is set to true, that is GMT can be intepreted differently than UTC. If that is not the expected result this attribute can be set to false. Returns: An integer containing the timestamp or 0 on error. """ datetime_object = StringToDatetime( time_string, timezone=timezone, dayfirst=dayfirst, gmt_as_timezone=gmt_as_timezone) return cls.FromPythonDatetime(datetime_object) @classmethod def FromWebKitTime(cls, webkit_time): """Converts a WebKit time into a timestamp. The WebKit time is a 64-bit value containing: micro seconds since 1601-01-01 00:00:00 Args: webkit_time: The 64-bit WebKit time timestamp. Returns: An integer containing the timestamp or 0 on error. """ if webkit_time < (cls.TIMESTAMP_MIN_MICRO_SECONDS + cls.WEBKIT_TIME_TO_POSIX_BASE): return 0 return webkit_time - cls.WEBKIT_TIME_TO_POSIX_BASE @classmethod def GetNow(cls): """Retrieves the current time (now) as a timestamp in UTC.""" time_elements = time.gmtime() return calendar.timegm(time_elements) * 1000000 @classmethod def IsLeapYear(cls, year): """Determines if a year is a leap year. A leap year is dividable by 4 and not by 100 or by 400 without a remainder. Args: year: The year as in 1970. Returns: A boolean value indicating the year is a leap year. """ return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0 @classmethod def LocaltimeToUTC(cls, timestamp, timezone, is_dst=False): """Converts the timestamp in localtime of the timezone to UTC. Args: timestamp: An integer containing the timestamp. timezone: The timezone (pytz.timezone) object. is_dst: A boolean to indicate the timestamp is corrected for daylight savings time (DST) only used for the DST transition period. The default is false. Returns: An integer containing the timestamp or 0 on error. """ if timezone and timezone != pytz.utc: datetime_object = ( datetime.datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=None) + datetime.timedelta(microseconds=timestamp)) # Check if timezone is UTC since utcoffset() does not support is_dst # for UTC and will raise. datetime_delta = timezone.utcoffset(datetime_object, is_dst=is_dst) seconds_delta = int(datetime_delta.total_seconds()) timestamp -= seconds_delta * cls.MICRO_SECONDS_PER_SECOND return timestamp @classmethod def RoundToSeconds(cls, timestamp): """Takes a timestamp value and rounds it to a second precision.""" leftovers = timestamp % cls.MICRO_SECONDS_PER_SECOND scrubbed = timestamp - leftovers rounded = round(float(leftovers) / cls.MICRO_SECONDS_PER_SECOND) return int(scrubbed + rounded * cls.MICRO_SECONDS_PER_SECOND) def StringToDatetime( time_string, timezone=pytz.utc, dayfirst=False, gmt_as_timezone=True): """Converts a string representation of a timestamp into a datetime object. Args: time_string: String that contains a date and time value. timezone: Optional timezone object (instance of pytz.timezone) that the data and time value in the string represents. This value is used when the timezone cannot be determined from the string. dayfirst: An optional boolean argument. If set to true then the parser will change the precedence in which it parses timestamps from MM-DD-YYYY to DD-MM-YYYY (and YYYY-MM-DD will be YYYY-DD-MM, etc). gmt_as_timezone: Sometimes the dateutil parser will interpret GMT and UTC the same way, that is not make a distinction. By default this is set to true, that is GMT can be intepreted differently than UTC. If that is not the expected result this attribute can be set to false. Returns: A datetime object. """ if not gmt_as_timezone and time_string.endswith(' GMT'): time_string = u'{0:s}UTC'.format(time_string[:-3]) try: datetime_object = dateutil.parser.parse(time_string, dayfirst=dayfirst) except (TypeError, ValueError) as exception: logging.error( u'Unable to copy {0:s} to a datetime object with error: {1:s}'.format( time_string, exception)) return datetime.datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=pytz.utc) if datetime_object.tzinfo: return datetime_object.astimezone(pytz.utc) return timezone.localize(datetime_object) def GetCurrentYear(): """Determines the current year.""" datetime_object = datetime.datetime.now() return datetime_object.year