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
56 changes: 39 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,49 @@ name: CI

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

jobs:
test:
runs-on: macos-latest

test-macos:
name: Swift ${{ matrix.swift }} on macOS ${{ matrix.macos }} with Xcode ${{ matrix.xcode }}
runs-on: macos-${{ matrix.macos }}
env:
DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer"
strategy:
fail-fast: false
matrix:
swift-version:
- ^6
include:
- swift: "6.0"
xcode: "16.0"
macos: "15"
- swift: "6.1"
xcode: "16.3"
macos: "15"
- swift: "6.2"
xcode: "26.0"
macos: "26"
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v6

name: Build and Test (Swift ${{ matrix.swift-version }})
- name: Cache Swift Package Manager dependencies
uses: actions/cache@v5
with:
path: |
~/.cache/org.swift.swiftpm
.build
key: ${{ runner.os }}-swift-${{ matrix.swift }}-spm-${{ hashFiles('Package.swift', 'Package.resolved') }}
restore-keys: |
${{ runner.os }}-swift-${{ matrix.swift }}-spm-

steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v2
with:
swift-version: ${{ matrix.swift-version }}
- name: Build
run: swift build -v
- name: Run tests
run: swift test -v
- name: Lint
run: swift format lint --strict --recursive .

- name: Build
run: swift build

- name: Test
run: swift test
15 changes: 15 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": 1,
"indentation": {
"spaces": 4
},
"lineLength": 160,
"maximumBlankLines": 1,
"respectsExistingLineBreaks": true,
"lineBreakBeforeEachArgument": true,
"multiElementCollectionTrailingCommas": true,
"spacesAroundRangeFormationOperators": true,
"rules": {
"AlwaysUseLowerCamelCase": false
}
}
12 changes: 8 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@ let package = Package(
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "TypedStream",
targets: ["TypedStream"]),
targets: ["TypedStream"]
),
.library(
name: "iMessage",
targets: ["iMessage"]),
targets: ["iMessage"]
),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "TypedStream",
dependencies: []),
dependencies: []
),
.target(
name: "iMessage",
dependencies: ["TypedStream"]),
dependencies: ["TypedStream"]
),
.testTarget(
name: "iMessageTests",
dependencies: ["iMessage"]
Expand Down
110 changes: 52 additions & 58 deletions Sources/TypedStream/Archivable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,23 @@ public enum Archivable: Hashable, Sendable {

// MARK: - Convenience Properties

/**
If this archivable represents an `NSString` or `NSMutableString` object,
returns its string value.

### Example
```swift
let nsstring = Archivable.object(
Class(name: "NSString", version: 1),
[.string("Hello world")]
)
print(nsstring.stringValue) // Optional("Hello world")

let notNSString = Archivable.object(
Class(name: "NSNumber", version: 1),
[.signedInteger(100)]
)
print(notNSString.stringValue) // nil
```
*/
/// If this archivable represents an `NSString` or `NSMutableString` object,
/// returns its string value.
///
/// ### Example
/// ```swift
/// let nsstring = Archivable.object(
/// Class(name: "NSString", version: 1),
/// [.string("Hello world")]
/// )
/// print(nsstring.stringValue) // Optional("Hello world")
///
/// let notNSString = Archivable.object(
/// Class(name: "NSNumber", version: 1),
/// [.signedInteger(100)]
/// )
/// print(notNSString.stringValue) // nil
/// ```
public var stringValue: String? {
if case let .object(classInfo, value) = self,
classInfo.name == "NSString" || classInfo.name == "NSMutableString",
Expand All @@ -47,31 +45,29 @@ public enum Archivable: Hashable, Sendable {
{
return nil
}

return text
}
return nil
}

/**
If this archivable represents an `NSNumber` object containing an integer,
returns its 64-bit integer value.

### Example
```swift
let nsnumber = Archivable.object(
Class(name: "NSNumber", version: 1),
[.signedInteger(100)]
)
print(nsnumber.integerValue) // Optional(100)

let notNSNumber = Archivable.object(
Class(name: "NSString", version: 1),
[.string("Hello world")]
)
print(notNSNumber.integerValue) // nil
```
*/
/// If this archivable represents an `NSNumber` object containing an integer,
/// returns its 64-bit integer value.
///
/// ### Example
/// ```swift
/// let nsnumber = Archivable.object(
/// Class(name: "NSNumber", version: 1),
/// [.signedInteger(100)]
/// )
/// print(nsnumber.integerValue) // Optional(100)
///
/// let notNSNumber = Archivable.object(
/// Class(name: "NSString", version: 1),
/// [.string("Hello world")]
/// )
/// print(notNSNumber.integerValue) // nil
/// ```
public var integerValue: Int64? {
if case let .object(classInfo, value) = self,
classInfo.name == "NSNumber",
Expand All @@ -83,25 +79,23 @@ public enum Archivable: Hashable, Sendable {
return nil
}

/**
If this archivable represents an `NSNumber` object containing a floating-point value,
returns its double-precision value.

### Example
```swift
let nsnumber = Archivable.object(
Class(name: "NSNumber", version: 1),
[.double(100.001)]
)
print(nsnumber.doubleValue) // Optional(100.001)

let notNSNumber = Archivable.object(
Class(name: "NSString", version: 1),
[.string("Hello world")]
)
print(notNSNumber.doubleValue) // nil
```
*/
/// If this archivable represents an `NSNumber` object containing a floating-point value,
/// returns its double-precision value.
///
/// ### Example
/// ```swift
/// let nsnumber = Archivable.object(
/// Class(name: "NSNumber", version: 1),
/// [.double(100.001)]
/// )
/// print(nsnumber.doubleValue) // Optional(100.001)
///
/// let notNSNumber = Archivable.object(
/// Class(name: "NSString", version: 1),
/// [.string("Hello world")]
/// )
/// print(notNSNumber.doubleValue) // nil
/// ```
public var doubleValue: Double? {
if case let .object(classInfo, value) = self,
classInfo.name == "NSNumber",
Expand Down
2 changes: 1 addition & 1 deletion Sources/TypedStream/Type.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public enum Type: Hashable, Sendable {
var index = 1
while index < types.count,
let digit = UInt8(exactly: types[index]),
(48...57).contains(digit)
(48 ... 57).contains(digit)
{
length = length * 10 + Int(digit - 48) // ASCII '0' is 48
index += 1
Expand Down
2 changes: 1 addition & 1 deletion Sources/TypedStream/TypedStreamDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ public final class TypedStreamDecoder {
guard idx + size <= stream.count else {
throw Error.outOfBounds(index: idx + size, length: stream.count)
}
let data = Data(stream[idx..<(idx + size)])
let data = Data(stream[idx ..< (idx + size)])
idx += size
return data
}
Expand Down
12 changes: 8 additions & 4 deletions Sources/iMessage/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ public final class Database {
GROUP BY chat_id
HAVING COUNT(DISTINCT handle_id) = ?
)
""")
"""
)

// Add each participant as a value
handles.forEach { handle in
Expand Down Expand Up @@ -207,7 +208,8 @@ public final class Database {

let displayName = sqlite3_column_text(statement, 1).map { String(cString: $0) }
let lastMessageDate = Date(
nanosecondsSinceReferenceDate: sqlite3_column_int64(statement, 3))
nanosecondsSinceReferenceDate: sqlite3_column_int64(statement, 3)
)

// Fetch participants for this chat
let participants = try fetchParticipants(for: chatId)
Expand Down Expand Up @@ -257,7 +259,8 @@ public final class Database {
JOIN handle h ON m.handle_id = h.ROWID
WHERE h.id IN (\(String(repeating: "?,", count: handles.count).dropLast()))
)
""")
"""
)

// Add each participant as a value
handles.forEach { handle in
Expand Down Expand Up @@ -325,7 +328,8 @@ public final class Database {
}

let date = Date(
nanosecondsSinceReferenceDate: sqlite3_column_int64(statement, 3))
nanosecondsSinceReferenceDate: sqlite3_column_int64(statement, 3)
)
let isFromMe = sqlite3_column_int(statement, 4) != 0
let rawReadAt = sqlite3_column_int64(statement, 7)
let readAt =
Expand Down
2 changes: 1 addition & 1 deletion Sources/iMessage/Extensions/Data+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ extension Data {
while index < string.endIndex {
let nextIndex = string.index(index, offsetBy: 2)
guard nextIndex <= string.endIndex,
let byte = UInt8(string[index..<nextIndex], radix: 16)
let byte = UInt8(string[index ..< nextIndex], radix: 16)
else {
return nil
}
Expand Down
8 changes: 4 additions & 4 deletions Tests/iMessageTests/DatabaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ struct DatabaseTests {
// Test with date filter - should only get chat with recent messages
let now = Date()
let yesterday = now.addingTimeInterval(-86400)
let filtered = try db.fetchChats(in: yesterday..<now)
let filtered = try db.fetchChats(in: yesterday ..< now)
#expect(filtered.count == 1)
#expect(filtered.first?.id.rawValue == "chat-guid-1")

// Test with older date range - should get the second chat
let threeDaysAgo = now.addingTimeInterval(-86400 * 3)
let twoDaysAgo = now.addingTimeInterval(-86400 * 2)
let oldFiltered = try db.fetchChats(in: threeDaysAgo..<twoDaysAgo)
let oldFiltered = try db.fetchChats(in: threeDaysAgo ..< twoDaysAgo)
#expect(oldFiltered.count == 1)
#expect(oldFiltered.first?.id.rawValue == "chat-guid-2")

Expand Down Expand Up @@ -100,7 +100,7 @@ struct DatabaseTests {
let today = Date()
let rangeMessages = try db.fetchMessages(
for: chatId,
in: yesterday..<today
in: yesterday ..< today
)
#expect(!rangeMessages.isEmpty)
}
Expand All @@ -124,7 +124,7 @@ struct DatabaseTests {
let today = Date()
let rangeMessages = try db.fetchMessages(
with: [handle],
in: yesterday..<today
in: yesterday ..< today
)
#expect(!rangeMessages.isEmpty)
}
Expand Down
Loading