diff --git a/Sources/ContainerizationEXT4/EXT4+Formatter.swift b/Sources/ContainerizationEXT4/EXT4+Formatter.swift index 6eb22df0..95190bfd 100644 --- a/Sources/ContainerizationEXT4/EXT4+Formatter.swift +++ b/Sources/ContainerizationEXT4/EXT4+Formatter.swift @@ -1326,9 +1326,13 @@ extension Date { return 0x3_7fff_ffff } - let seconds = UInt64(s) - let nanoseconds = UInt64(self.timeIntervalSince1970.truncatingRemainder(dividingBy: 1) * 1_000_000_000) - - return seconds | (nanoseconds << 34) + // 32 bits - base: seconds since January 1, 1970, signed (negative for pre-1970 dates) + // 2 bits - epoch: overflow counter (0-3), how many times the 32-bit seconds field has wrapped + // 30 bits - nanoseconds (0-999,999,999) + let sInt64 = Int64(floor(s)) + let base = Int32(truncatingIfNeeded: sInt64) + let epoch = UInt64(sInt64 - Int64(base)) + let nanoseconds = min(UInt32((s - floor(s)) * 1_000_000_000), 999_999_999) + return UInt64(UInt32(bitPattern: base)) | epoch | (UInt64(nanoseconds) << 34) } } diff --git a/Sources/ContainerizationEXT4/EXT4Reader+Export.swift b/Sources/ContainerizationEXT4/EXT4Reader+Export.swift index 9b32b71e..49f4978c 100644 --- a/Sources/ContainerizationEXT4/EXT4Reader+Export.swift +++ b/Sources/ContainerizationEXT4/EXT4Reader+Export.swift @@ -73,9 +73,9 @@ extension EXT4.EXT4Reader { entry.size = Int64(size) entry.group = gid_t(inode.gid) entry.owner = uid_t(inode.uid) - entry.creationDate = Date(fsTimestamp: UInt64((inode.ctimeExtra << 32) | inode.ctime)) - entry.modificationDate = Date(fsTimestamp: UInt64((inode.mtimeExtra << 32) | inode.mtime)) - entry.contentAccessDate = Date(fsTimestamp: UInt64((inode.atimeExtra << 32) | inode.atime)) + entry.creationDate = Date(fsTimestamp: UInt64(inode.crtimeExtra) << 32 | UInt64(inode.crtime)) + entry.modificationDate = Date(fsTimestamp: UInt64(inode.mtimeExtra) << 32 | UInt64(inode.mtime)) + entry.contentAccessDate = Date(fsTimestamp: UInt64(inode.atimeExtra) << 32 | UInt64(inode.atime)) entry.xattrs = xattrs if mode.isDir() { @@ -156,9 +156,9 @@ extension EXT4.EXT4Reader { entry.permissions = inode.mode entry.group = gid_t(inode.gid) entry.owner = uid_t(inode.uid) - entry.creationDate = Date(fsTimestamp: UInt64((inode.ctimeExtra << 32) | inode.ctime)) - entry.modificationDate = Date(fsTimestamp: UInt64((inode.mtimeExtra << 32) | inode.mtime)) - entry.contentAccessDate = Date(fsTimestamp: UInt64((inode.atimeExtra << 32) | inode.atime)) + entry.creationDate = Date(fsTimestamp: UInt64(inode.crtimeExtra) << 32 | UInt64(inode.crtime)) + entry.modificationDate = Date(fsTimestamp: UInt64(inode.mtimeExtra) << 32 | UInt64(inode.mtime)) + entry.contentAccessDate = Date(fsTimestamp: UInt64(inode.atimeExtra) << 32 | UInt64(inode.atime)) try writer.writeEntry(entry: entry, data: nil) } try writer.finishEncoding() @@ -203,7 +203,12 @@ extension Date { return } - let seconds = Int64(fsTimestamp & 0x3_ffff_ffff) + // 32 bits - base: seconds since January 1, 1970, signed (negative for pre-1970 dates) + // 2 bits - epoch: overflow counter (0-3), how many times the 32-bit seconds field has wrapped + // 30 bits - nanoseconds (0-999,999,999) + let base = Int32(truncatingIfNeeded: fsTimestamp) + let epoch = Int64(fsTimestamp & 0x3_0000_0000) + let seconds = Int64(base) + epoch let nanoseconds = Double(fsTimestamp >> 34) / 1_000_000_000 self = Date(timeIntervalSince1970: Double(seconds) + nanoseconds) diff --git a/Tests/ContainerizationEXT4Tests/TestEXT4Format+Create.swift b/Tests/ContainerizationEXT4Tests/TestEXT4Format+Create.swift index ef81b634..1a435d0e 100644 --- a/Tests/ContainerizationEXT4Tests/TestEXT4Format+Create.swift +++ b/Tests/ContainerizationEXT4Tests/TestEXT4Format+Create.swift @@ -78,3 +78,33 @@ struct Ext4FormatCreateTests { } // should create /parent automatically } } + +@Suite(.serialized) +struct NegativeTimestampRoundtripTests { + private let fsPath = FilePath( + FileManager.default.temporaryDirectory + .appendingPathComponent("ext4-pre1970-roundtrip.img", isDirectory: false)) + private let apollo11MoonLanding: Date = { + let f = ISO8601DateFormatter() + f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return f.date(from: "1969-07-20T20:17:39.9Z")! + }() + + @Test func encodeNegativeTimestamp() throws { + let formatter = try EXT4.Formatter(fsPath, minDiskSize: 32.kib()) + defer { try? formatter.close() } + let ts = FileTimestamps(access: apollo11MoonLanding, modification: apollo11MoonLanding, creation: apollo11MoonLanding) + try formatter.create(path: FilePath("/file"), mode: EXT4.Inode.Mode(.S_IFREG, 0o755), ts: ts, buf: nil) + } + + @Test func decodeNegativeTimestamp() throws { + let reader = try EXT4.EXT4Reader(blockDevice: fsPath) + let (_, inode) = try reader.stat(FilePath("/file")) + let mtime = Date(fsTimestamp: UInt64(inode.mtime) | (UInt64(inode.mtimeExtra) << 32)) + let atime = Date(fsTimestamp: UInt64(inode.atime) | (UInt64(inode.atimeExtra) << 32)) + let crtime = Date(fsTimestamp: UInt64(inode.crtime) | (UInt64(inode.crtimeExtra) << 32)) + #expect(mtime == apollo11MoonLanding) + #expect(atime == apollo11MoonLanding) + #expect(crtime == apollo11MoonLanding) + } +}