Skip to content

Commit 59f0a04

Browse files
authored
Fix date and time bugs in EXT4 (#592)
- Fixes EXT4 timestamp encoding for pre-1970 dates - Fixes EXT4 timestamp decoding for pre-1970 dates - Fixes the creation date - Fixes a `UInt32` overflow Resolves the following failures in the added tests: ``` Swift/arm64e-apple-macos.swiftinterface:38198: Fatal error: Double value cannot be converted to UInt64 because the result would be less than UInt64.min error: Process '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/libexec/swift/pm/swiftpm-testing-helper --test-bundle-path /Users/Dmitry/Apple/containerization/.build/arm64-apple-macosx/debug/containerizationPackageTests.xctest/Contents/MacOS/containerizationPackageTests --filter encodeNegativeTimestamp /Users/Dmitry/Apple/containerization/.build/arm64-apple-macosx/debug/containerizationPackageTests.xctest/Contents/MacOS/containerizationPackageTests --testing-library swift-testing' exited with unexpected signal code 5 ``` ``` ✘ Test decodeNegativeTimestamp() recorded an issue at TestEXT4Format+Create.swift:100:6: Caught error: not a valid EXT4 superblock ✘ Test decodeNegativeTimestamp() failed after 0.003 seconds with 1 issue. ✘ Suite NegativeTimestampRoundtripTests failed after 0.004 seconds with 1 issue. ✘ Test run with 1 test in 1 suite failed after 0.004 seconds with 1 issue. ```
1 parent 7da3779 commit 59f0a04

3 files changed

Lines changed: 50 additions & 11 deletions

File tree

Sources/ContainerizationEXT4/EXT4+Formatter.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,9 +1326,13 @@ extension Date {
13261326
return 0x3_7fff_ffff
13271327
}
13281328

1329-
let seconds = UInt64(s)
1330-
let nanoseconds = UInt64(self.timeIntervalSince1970.truncatingRemainder(dividingBy: 1) * 1_000_000_000)
1331-
1332-
return seconds | (nanoseconds << 34)
1329+
// 32 bits - base: seconds since January 1, 1970, signed (negative for pre-1970 dates)
1330+
// 2 bits - epoch: overflow counter (0-3), how many times the 32-bit seconds field has wrapped
1331+
// 30 bits - nanoseconds (0-999,999,999)
1332+
let sInt64 = Int64(floor(s))
1333+
let base = Int32(truncatingIfNeeded: sInt64)
1334+
let epoch = UInt64(sInt64 - Int64(base))
1335+
let nanoseconds = min(UInt32((s - floor(s)) * 1_000_000_000), 999_999_999)
1336+
return UInt64(UInt32(bitPattern: base)) | epoch | (UInt64(nanoseconds) << 34)
13331337
}
13341338
}

Sources/ContainerizationEXT4/EXT4Reader+Export.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ extension EXT4.EXT4Reader {
7373
entry.size = Int64(size)
7474
entry.group = gid_t(inode.gid)
7575
entry.owner = uid_t(inode.uid)
76-
entry.creationDate = Date(fsTimestamp: UInt64((inode.ctimeExtra << 32) | inode.ctime))
77-
entry.modificationDate = Date(fsTimestamp: UInt64((inode.mtimeExtra << 32) | inode.mtime))
78-
entry.contentAccessDate = Date(fsTimestamp: UInt64((inode.atimeExtra << 32) | inode.atime))
76+
entry.creationDate = Date(fsTimestamp: UInt64(inode.crtimeExtra) << 32 | UInt64(inode.crtime))
77+
entry.modificationDate = Date(fsTimestamp: UInt64(inode.mtimeExtra) << 32 | UInt64(inode.mtime))
78+
entry.contentAccessDate = Date(fsTimestamp: UInt64(inode.atimeExtra) << 32 | UInt64(inode.atime))
7979
entry.xattrs = xattrs
8080

8181
if mode.isDir() {
@@ -156,9 +156,9 @@ extension EXT4.EXT4Reader {
156156
entry.permissions = inode.mode
157157
entry.group = gid_t(inode.gid)
158158
entry.owner = uid_t(inode.uid)
159-
entry.creationDate = Date(fsTimestamp: UInt64((inode.ctimeExtra << 32) | inode.ctime))
160-
entry.modificationDate = Date(fsTimestamp: UInt64((inode.mtimeExtra << 32) | inode.mtime))
161-
entry.contentAccessDate = Date(fsTimestamp: UInt64((inode.atimeExtra << 32) | inode.atime))
159+
entry.creationDate = Date(fsTimestamp: UInt64(inode.crtimeExtra) << 32 | UInt64(inode.crtime))
160+
entry.modificationDate = Date(fsTimestamp: UInt64(inode.mtimeExtra) << 32 | UInt64(inode.mtime))
161+
entry.contentAccessDate = Date(fsTimestamp: UInt64(inode.atimeExtra) << 32 | UInt64(inode.atime))
162162
try writer.writeEntry(entry: entry, data: nil)
163163
}
164164
try writer.finishEncoding()
@@ -203,7 +203,12 @@ extension Date {
203203
return
204204
}
205205

206-
let seconds = Int64(fsTimestamp & 0x3_ffff_ffff)
206+
// 32 bits - base: seconds since January 1, 1970, signed (negative for pre-1970 dates)
207+
// 2 bits - epoch: overflow counter (0-3), how many times the 32-bit seconds field has wrapped
208+
// 30 bits - nanoseconds (0-999,999,999)
209+
let base = Int32(truncatingIfNeeded: fsTimestamp)
210+
let epoch = Int64(fsTimestamp & 0x3_0000_0000)
211+
let seconds = Int64(base) + epoch
207212
let nanoseconds = Double(fsTimestamp >> 34) / 1_000_000_000
208213

209214
self = Date(timeIntervalSince1970: Double(seconds) + nanoseconds)

Tests/ContainerizationEXT4Tests/TestEXT4Format+Create.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,33 @@ struct Ext4FormatCreateTests {
7878
} // should create /parent automatically
7979
}
8080
}
81+
82+
@Suite(.serialized)
83+
struct NegativeTimestampRoundtripTests {
84+
private let fsPath = FilePath(
85+
FileManager.default.temporaryDirectory
86+
.appendingPathComponent("ext4-pre1970-roundtrip.img", isDirectory: false))
87+
private let apollo11MoonLanding: Date = {
88+
let f = ISO8601DateFormatter()
89+
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
90+
return f.date(from: "1969-07-20T20:17:39.9Z")!
91+
}()
92+
93+
@Test func encodeNegativeTimestamp() throws {
94+
let formatter = try EXT4.Formatter(fsPath, minDiskSize: 32.kib())
95+
defer { try? formatter.close() }
96+
let ts = FileTimestamps(access: apollo11MoonLanding, modification: apollo11MoonLanding, creation: apollo11MoonLanding)
97+
try formatter.create(path: FilePath("/file"), mode: EXT4.Inode.Mode(.S_IFREG, 0o755), ts: ts, buf: nil)
98+
}
99+
100+
@Test func decodeNegativeTimestamp() throws {
101+
let reader = try EXT4.EXT4Reader(blockDevice: fsPath)
102+
let (_, inode) = try reader.stat(FilePath("/file"))
103+
let mtime = Date(fsTimestamp: UInt64(inode.mtime) | (UInt64(inode.mtimeExtra) << 32))
104+
let atime = Date(fsTimestamp: UInt64(inode.atime) | (UInt64(inode.atimeExtra) << 32))
105+
let crtime = Date(fsTimestamp: UInt64(inode.crtime) | (UInt64(inode.crtimeExtra) << 32))
106+
#expect(mtime == apollo11MoonLanding)
107+
#expect(atime == apollo11MoonLanding)
108+
#expect(crtime == apollo11MoonLanding)
109+
}
110+
}

0 commit comments

Comments
 (0)