Skip to content
Open
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
4 changes: 2 additions & 2 deletions Sources/ContainerizationEXT4/EXT4+Formatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,13 @@ extension EXT4 {
// the file we are deleting is a hardlink, decrement the link count
let linkedInodePtr = self.inodes[Int(hardlink - 1)]
var linkedInode = linkedInodePtr.pointee
if linkedInode.linksCount > 2 {
if linkedInode.linksCount > 1 {
linkedInode.linksCount -= 1
linkedInodePtr.initialize(to: linkedInode)
}
}

guard inodeNumber > FirstInode else {
guard inodeNumber >= FirstInode else {
// Free the inodes and the blocks related to the inode only if its valid
return
}
Expand Down
30 changes: 0 additions & 30 deletions Tests/ContainerizationEXT4Tests/TestEXT4Format+Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,33 +78,3 @@ 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)
}
}
60 changes: 60 additions & 0 deletions Tests/ContainerizationEXT4Tests/TestEXT4Format+Link.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project 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
//
// https://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.
//===----------------------------------------------------------------------===//

import Foundation
import SystemPackage
import Testing

@testable import ContainerizationEXT4

struct Ext4FormatLinkTests {
@Test func hardlinkLinksCount() throws {
func makeFile(unlink: Bool) throws -> FilePath {
let path = FilePath(
FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: false))
let fmt = try EXT4.Formatter(path, minDiskSize: 32.kib())
try fmt.create(path: "/original", mode: EXT4.Inode.Mode(.S_IFREG, 0o755), buf: nil)
try fmt.link(link: "/hardlink", target: "/original")
if unlink {
try fmt.unlink(path: "/hardlink")
}
try fmt.close()
return path
}

let afterLink = try makeFile(unlink: false)
#expect(try EXT4.EXT4Reader(blockDevice: afterLink).stat("/original").inode.linksCount == 2)

let afterUnlink = try makeFile(unlink: true)
#expect(try EXT4.EXT4Reader(blockDevice: afterUnlink).stat("/original").inode.linksCount == 1)
}

@Test func unlinkFirstInodeFreesInode() throws {
let emptyPath = FilePath(FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: false))
defer { try? FileManager.default.removeItem(at: emptyPath.url) }
try EXT4.Formatter(emptyPath, minDiskSize: 32.kib()).close()

let path = FilePath(FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: false))
defer { try? FileManager.default.removeItem(at: path.url) }
let fmt = try EXT4.Formatter(path, minDiskSize: 32.kib())
try fmt.create(path: FilePath("/file"), mode: EXT4.Inode.Mode(.S_IFREG, 0o755), buf: nil)
try fmt.unlink(path: FilePath("/file"))
try fmt.close()

#expect(try EXT4.EXT4Reader(blockDevice: path).superBlock.freeInodesCount == EXT4.EXT4Reader(blockDevice: emptyPath).superBlock.freeInodesCount)
}
}
30 changes: 30 additions & 0 deletions Tests/ContainerizationEXT4Tests/TestEXT4Format.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,33 @@ struct Ext4FormatTests: ~Copyable {
#expect(regFile.sizeLow == 4)
}
}

@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