From 9b4e36fe844cf5db5b39edb21e6546b81a79af2b Mon Sep 17 00:00:00 2001 From: ravindudanthanarayana Date: Sun, 1 Nov 2026 09:48:11 +0530 Subject: [PATCH 1/3] Don't crash when saving an attachment whose name is only periods. If an attachment's preferred name is empty or consists only of periods (e.g. "." or ".."), the first attempt to write it fails and the fallback path splits the name on "." to add a suffix. The resulting array is empty, so indexing its first element traps and crashes the test process. This change falls back to the default preferred name ("untitled") in that case, matching how other unusable names are already handled. --- Sources/Testing/Attachments/Attachment.swift | 5 ++++ Tests/TestingTests/AttachmentTests.swift | 24 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 56bc2c112..357d9cbee 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -546,6 +546,11 @@ extension Attachment where AttachableValue: ~Copyable { // Split the extension(s) off the preferred name. The first component in // the resulting array is our base name. var preferredNameComponents = preferredName.split(separator: ".") + if preferredNameComponents.isEmpty { + // The preferred name is empty or consists only of periods (e.g. "..") + // so there is no base name to add a suffix to. Use the default name. + preferredNameComponents = [Substring(Self.defaultPreferredName)] + } let firstPreferredNameComponent = preferredNameComponents[0] while true { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 8bd7bee88..c2c36f322 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -164,6 +164,9 @@ struct AttachmentTests { String(repeating: "a", count: maximumNameCount), String(repeating: "a", count: maximumNameCount + 1), String(repeating: "a", count: maximumNameCount + 2), + ".", + "..", + "...", ]) func writeAttachmentWithBadName(name: String) throws { let attachableValue = MySendableAttachable(string: "") let attachment = Attachment(attachableValue, named: name) @@ -176,6 +179,27 @@ struct AttachmentTests { try compare(attachableValue, toContentsOfFileAtPath: filePath) } + @Test func writeAttachmentWithEmptyPreferredName() throws { + struct MyAttachableWithEmptyName: Attachable, Sendable { + func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try Array("".utf8).withUnsafeBytes(body) + } + + func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + "" + } + } + let attachment = Attachment(MyAttachableWithEmptyName()) + + let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectory()) + defer { + remove(filePath) + } + let filePathComponents = filePath.split { $0 == "/" || $0 == #"\"# } + let fileName = try #require(filePathComponents.last) + #expect(fileName.hasPrefix("\(Attachment.defaultPreferredName)-")) + } + @Test func fileSystemPathIsSetAfterWritingViaEventHandler() async throws { let attachableValue = MySendableAttachable(string: "") try await confirmation("Attachment detected") { valueAttached in From f5b561c2af60f4ea4a219ea85c4ce074b7cf9237 Mon Sep 17 00:00:00 2001 From: ravindudanthanarayana Date: Sun, 1 Nov 2026 18:55:24 +0530 Subject: [PATCH 2/3] Keep empty subsequences when splitting an attachment's preferred name. Passing omittingEmptySubsequences: false means a name made up only of periods splits into empty components rather than an empty array, so it no longer needs special handling. An empty name splits into a single empty component, which would produce a file name with no base name, so continue to use the default preferred name in that case. --- Sources/Testing/Attachments/Attachment.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 357d9cbee..1abcbd81c 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -545,10 +545,9 @@ extension Attachment where AttachableValue: ~Copyable { } catch { // Split the extension(s) off the preferred name. The first component in // the resulting array is our base name. - var preferredNameComponents = preferredName.split(separator: ".") - if preferredNameComponents.isEmpty { - // The preferred name is empty or consists only of periods (e.g. "..") - // so there is no base name to add a suffix to. Use the default name. + var preferredNameComponents = preferredName.split(separator: ".", omittingEmptySubsequences: false) + if preferredName.isEmpty { + // There is no base name to add a suffix to, so use the default name. preferredNameComponents = [Substring(Self.defaultPreferredName)] } let firstPreferredNameComponent = preferredNameComponents[0] From c2edbde7952b0b9dc03c20bb90ae25186155eef0 Mon Sep 17 00:00:00 2001 From: ravindudanthanarayana Date: Sun, 1 Nov 2026 19:03:47 +0530 Subject: [PATCH 3/3] Don't special-case an empty attachment name. If the name splits into something invalid, opening the file will fail later and we'll fall back to the default name. If it splits into something valid, it's okay to save the attachment. All this code needs to do is avoid the crash, which omittingEmptySubsequences: false already does. Update the test for an empty name to check the saved file's contents rather than its name. --- Sources/Testing/Attachments/Attachment.swift | 4 ---- Tests/TestingTests/AttachmentTests.swift | 12 +++++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 1abcbd81c..d3906db35 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -546,10 +546,6 @@ extension Attachment where AttachableValue: ~Copyable { // Split the extension(s) off the preferred name. The first component in // the resulting array is our base name. var preferredNameComponents = preferredName.split(separator: ".", omittingEmptySubsequences: false) - if preferredName.isEmpty { - // There is no base name to add a suffix to, so use the default name. - preferredNameComponents = [Substring(Self.defaultPreferredName)] - } let firstPreferredNameComponent = preferredNameComponents[0] while true { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index c2c36f322..e34483a74 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -181,23 +181,25 @@ struct AttachmentTests { @Test func writeAttachmentWithEmptyPreferredName() throws { struct MyAttachableWithEmptyName: Attachable, Sendable { + var string: String + func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try Array("".utf8).withUnsafeBytes(body) + try Array(string.utf8).withUnsafeBytes(body) } func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { "" } } - let attachment = Attachment(MyAttachableWithEmptyName()) + let string = "" + let attachment = Attachment(MyAttachableWithEmptyName(string: string)) let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectory()) defer { remove(filePath) } - let filePathComponents = filePath.split { $0 == "/" || $0 == #"\"# } - let fileName = try #require(filePathComponents.last) - #expect(fileName.hasPrefix("\(Attachment.defaultPreferredName)-")) + let file = try Testing.FileHandle(forReadingAtPath: filePath) + #expect(try file.readToEnd() == Array(string.utf8)) } @Test func fileSystemPathIsSetAfterWritingViaEventHandler() async throws {