Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Sources/ContainerizationEXT4/EXT4+Formatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
19 changes: 12 additions & 7 deletions Sources/ContainerizationEXT4/EXT4Reader+Export.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions Tests/ContainerizationEXT4Tests/TestEXT4Format+Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading