// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : DateHandlers.cs // Author(s) : Natalia Portillo // // Component : Helpers. // // --[ Description ] ---------------------------------------------------------- // // Convert several timestamp formats to C# DateTime. // // --[ License ] -------------------------------------------------------------- // // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation; either version 2.1 of the // License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, see . // // ---------------------------------------------------------------------------- // Copyright © 2011-2025 Natalia Portillo // ****************************************************************************/ using System; using System.Text; using Aaru.Logging; namespace Aaru.Helpers; /// Helper operations for timestamp management (date and time) public static class DateHandlers { const string ISO9660_MODULE_NAME = "ISO9600ToDateTime handler"; const string PASCAL_MODULE_NAME = "UCSDPascalToDateTime handler"; const string DOS_MODULE_NAME = "DOSToDateTime handler"; static readonly DateTime _lisaEpoch = new(1901, 1, 1, 0, 0, 0); static readonly DateTime _macEpoch = new(1904, 1, 1, 0, 0, 0); static readonly DateTime _unixEpoch = new(1970, 1, 1, 0, 0, 0); /// Day 0 of Julian Date system static readonly DateTime _julianEpoch = new(1858, 11, 17, 0, 0, 0); static readonly DateTime _amigaEpoch = new(1978, 1, 1, 0, 0, 0); /// Converts a Macintosh timestamp to a .NET DateTime /// Macintosh timestamp (seconds since 1st Jan. 1904) /// .NET DateTime public static DateTime MacToDateTime(ulong macTimeStamp) => _macEpoch.AddTicks((long)(macTimeStamp * 10000000)); /// Converts a Lisa timestamp to a .NET DateTime /// Lisa timestamp (seconds since 1st Jan. 1901) /// .NET DateTime public static DateTime LisaToDateTime(uint lisaTimeStamp) => _lisaEpoch.AddSeconds(lisaTimeStamp); /// Converts a UNIX timestamp to a .NET DateTime /// UNIX timestamp (seconds since 1st Jan. 1970) /// .NET DateTime public static DateTime UnixToDateTime(int unixTimeStamp) => _unixEpoch.AddSeconds(unixTimeStamp); /// Converts a UNIX timestamp to a .NET DateTime /// UNIX timestamp (seconds since 1st Jan. 1970) /// .NET DateTime public static DateTime UnixToDateTime(long unixTimeStamp) => _unixEpoch.AddSeconds(unixTimeStamp); /// Converts a UNIX timestamp to a .NET DateTime /// UNIX timestamp (seconds since 1st Jan. 1970) /// .NET DateTime public static DateTime UnixUnsignedToDateTime(uint unixTimeStamp) => _unixEpoch.AddSeconds(unixTimeStamp); /// Converts a UNIX timestamp to a .NET DateTime /// Seconds since 1st Jan. 1970) /// Nanoseconds /// .NET DateTime public static DateTime UnixUnsignedToDateTime(uint seconds, uint nanoseconds) => _unixEpoch.AddSeconds(seconds).AddTicks((long)nanoseconds / 100); /// Converts a UNIX timestamp to a .NET DateTime /// UNIX timestamp (seconds since 1st Jan. 1970) /// .NET DateTime public static DateTime UnixUnsignedToDateTime(ulong unixTimeStamp) => _unixEpoch.AddSeconds(unixTimeStamp); /// Converts a High Sierra Format timestamp to a .NET DateTime /// High Sierra Format timestamp /// .NET DateTime public static DateTime HighSierraToDateTime(byte[] vdDateTime) { var isoTime = new byte[17]; Array.Copy(vdDateTime, 0, isoTime, 0, 16); return Iso9660ToDateTime(isoTime); } // TODO: Timezone /// Converts an ISO9660 timestamp to a .NET DateTime /// ISO9660 timestamp /// .NET DateTime public static DateTime Iso9660ToDateTime(byte[] vdDateTime) { var twoCharValue = new byte[2]; var fourCharValue = new byte[4]; fourCharValue[0] = vdDateTime[0]; fourCharValue[1] = vdDateTime[1]; fourCharValue[2] = vdDateTime[2]; fourCharValue[3] = vdDateTime[3]; AaruLogging.Debug(ISO9660_MODULE_NAME, "year = \"{0}\"", StringHandlers.CToString(fourCharValue, Encoding.ASCII)); if(!int.TryParse(StringHandlers.CToString(fourCharValue, Encoding.ASCII), out int year)) year = 0; twoCharValue[0] = vdDateTime[4]; twoCharValue[1] = vdDateTime[5]; AaruLogging.Debug(ISO9660_MODULE_NAME, "month = \"{0}\"", StringHandlers.CToString(twoCharValue, Encoding.ASCII)); if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int month)) month = 0; twoCharValue[0] = vdDateTime[6]; twoCharValue[1] = vdDateTime[7]; AaruLogging.Debug(ISO9660_MODULE_NAME, "day = \"{0}\"", StringHandlers.CToString(twoCharValue, Encoding.ASCII)); if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int day)) day = 0; twoCharValue[0] = vdDateTime[8]; twoCharValue[1] = vdDateTime[9]; AaruLogging.Debug(ISO9660_MODULE_NAME, "hour = \"{0}\"", StringHandlers.CToString(twoCharValue, Encoding.ASCII)); if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int hour)) hour = 0; twoCharValue[0] = vdDateTime[10]; twoCharValue[1] = vdDateTime[11]; AaruLogging.Debug(ISO9660_MODULE_NAME, "minute = \"{0}\"", StringHandlers.CToString(twoCharValue, Encoding.ASCII)); if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int minute)) minute = 0; twoCharValue[0] = vdDateTime[12]; twoCharValue[1] = vdDateTime[13]; AaruLogging.Debug(ISO9660_MODULE_NAME, "second = \"{0}\"", StringHandlers.CToString(twoCharValue, Encoding.ASCII)); if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int second)) second = 0; twoCharValue[0] = vdDateTime[14]; twoCharValue[1] = vdDateTime[15]; AaruLogging.Debug(ISO9660_MODULE_NAME, "hundredths = \"{0}\"", StringHandlers.CToString(twoCharValue, Encoding.ASCII)); if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int hundredths)) hundredths = 0; AaruLogging.Debug(ISO9660_MODULE_NAME, "decodedDT = new DateTime({0}, {1}, {2}, {3}, {4}, {5}, {6}, DateTimeKind.Unspecified);", year, month, day, hour, minute, second, hundredths * 10); var difference = (sbyte)vdDateTime[16]; var decodedDt = new DateTime(year, month, day, hour, minute, second, hundredths * 10, DateTimeKind.Utc); return decodedDt.AddMinutes(difference * -15); } /// Converts a VMS timestamp to a .NET DateTime /// VMS timestamp (tenths of microseconds since day 0 of the Julian Date) /// .NET DateTime /// C# works in UTC, VMS on Julian Date, some displacement may occur on disks created outside UTC public static DateTime VmsToDateTime(ulong vmsDate) { double delta = vmsDate * 0.0001; // Tenths of microseconds to milliseconds, will lose some detail return _julianEpoch.AddMilliseconds(delta); } /// Converts an Amiga timestamp to a .NET DateTime /// Days since the 1st Jan. 1978 /// Minutes since o'clock /// Ticks /// .NET DateTime public static DateTime AmigaToDateTime(uint days, uint minutes, uint ticks) { DateTime temp = _amigaEpoch.AddDays(days); temp = temp.AddMinutes(minutes); return temp.AddMilliseconds(ticks * 20); } /// Converts an UCSD Pascal timestamp to a .NET DateTime /// UCSD Pascal timestamp /// .NET DateTime public static DateTime UcsdPascalToDateTime(short dateRecord) { int year = ((dateRecord & 0xFE00) >> 9) + 1900; int day = (dateRecord & 0x01F0) >> 4; int month = dateRecord & 0x000F; AaruLogging.Debug(PASCAL_MODULE_NAME, "dateRecord = 0x{0:X4}, year = {1}, month = {2}, day = {3}", dateRecord, year, month, day); return new DateTime(year, month, day); } /// Converts a DOS timestamp to a .NET DateTime /// Date /// Time /// .NET DateTime public static DateTime DosToDateTime(ushort date, ushort time) { int year = ((date & 0xFE00) >> 9) + 1980; int month = (date & 0x1E0) >> 5; int day = date & 0x1F; int hour = (time & 0xF800) >> 11; int minute = (time & 0x7E0) >> 5; int second = (time & 0x1F) * 2; AaruLogging.Debug(DOS_MODULE_NAME, "date = 0x{0:X4}, year = {1}, month = {2}, day = {3}", date, year, month, day); AaruLogging.Debug(DOS_MODULE_NAME, "time = 0x{0:X4}, hour = {1}, minute = {2}, second = {3}", time, hour, minute, second); DateTime dosDate; try { dosDate = new DateTime(year, month, day, hour, minute, second); } catch(ArgumentOutOfRangeException) { dosDate = new DateTime(1980, 1, 1, 0, 0, 0); } return dosDate; } /// Converts a CP/M timestamp to .NET DateTime /// CP/M timestamp /// .NET DateTime public static DateTime CpmToDateTime(byte[] timestamp) { var days = BitConverter.ToUInt16(timestamp, 0); int hours = timestamp[2]; int minutes = timestamp[3]; DateTime temp = _amigaEpoch.AddDays(days); temp = temp.AddHours(hours); temp = temp.AddMinutes(minutes); return temp; } /// Converts an ECMA timestamp to a .NET DateTime /// Timezone /// Year /// Month /// Day /// Hour /// Minute /// Second /// Centiseconds /// Hundreds of microseconds /// Microseconds /// public static DateTime EcmaToDateTime(ushort typeAndTimeZone, short year, byte month, byte day, byte hour, byte minute, byte second, byte centiseconds, byte hundredsOfMicroseconds, byte microseconds) { var specification = (byte)((typeAndTimeZone & 0xF000) >> 12); long ticks = (long)centiseconds * 100000 + (long)hundredsOfMicroseconds * 1000 + (long)microseconds * 10; if(specification == 0) return new DateTime(year, month, day, hour, minute, second, DateTimeKind.Utc).AddTicks(ticks); var preOffset = (ushort)(typeAndTimeZone & 0xFFF); short offset; if((preOffset & 0x800) == 0x800) offset = (short)(preOffset | 0xF000); else offset = (short)(preOffset & 0x7FF); switch(offset) { case -2047: return new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified).AddTicks(ticks); case < -1440 or > 1440: offset = 0; break; } return new DateTimeOffset(year, month, day, hour, minute, second, new TimeSpan(0, offset, 0)).AddTicks(ticks) .DateTime; } /// Converts a Solaris high resolution timestamp to .NET DateTime /// Solaris high resolution timestamp /// .NET DateTime public static DateTime UnixHrTimeToDateTime(ulong hrTimeStamp) => _unixEpoch.AddTicks((long)(hrTimeStamp / 100)); /// Converts an OS-9 timestamp to .NET DateTime /// OS-9 timestamp /// .NET DateTime public static DateTime Os9ToDateTime(byte[] date) { if(date == null || date.Length != 3 && date.Length != 5) return DateTime.MinValue; DateTime os9Date; try { os9Date = date.Length == 5 ? new DateTime(1900 + date[0], date[1], date[2], date[3], date[4], 0) : new DateTime(1900 + date[0], date[1], date[2], 0, 0, 0); } catch(ArgumentOutOfRangeException) { os9Date = new DateTime(1900, 0, 0, 0, 0, 0); } return os9Date; } /// Converts a LIF timestamp to .NET DateTime /// LIF timestamp /// .NET DateTime public static DateTime LifToDateTime(byte[] date) => date is not { Length: 6 } ? new DateTime(1970, 1, 1, 0, 0, 0) : LifToDateTime(date[0], date[1], date[2], date[3], date[4], date[5]); /// Converts a LIF timestamp to .NET DateTime /// Yer /// Month /// Day /// Hour /// Minute /// Second /// .NET DateTime public static DateTime LifToDateTime(byte year, byte month, byte day, byte hour, byte minute, byte second) { try { int iyear = (year >> 4) * 10 + (year & 0xF); int imonth = (month >> 4) * 10 + (month & 0xF); int iday = (day >> 4) * 10 + (day & 0xF); int iminute = (minute >> 4) * 10 + (minute & 0xF); int ihour = (hour >> 4) * 10 + (hour & 0xF); int isecond = (second >> 4) * 10 + (second & 0xF); if(iyear >= 70) iyear += 1900; else iyear += 2000; return new DateTime(iyear, imonth, iday, ihour, iminute, isecond); } catch(ArgumentOutOfRangeException) { return new DateTime(1970, 1, 1, 0, 0, 0); } } }