Skip to content

Commit 69f0b6e

Browse files
authored
Check for required files (#3)
* Check for required files * Remove superfluous checks * Add testing
1 parent c8c5fc6 commit 69f0b6e

File tree

5 files changed

+129
-40
lines changed

5 files changed

+129
-40
lines changed

Diff for: Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import PackageDescription
44
let package = Package(
55
name: "swift-wallet",
66
platforms: [
7-
.macOS(.v11)
7+
.macOS(.v12)
88
],
99
products: [
1010
.library(name: "WalletPasses", targets: ["WalletPasses"]),

Diff for: Sources/WalletOrders/OrderBuilder.swift

+27-19
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,33 @@ public struct OrderBuilder: Sendable {
3535
self.openSSLURL = URL(fileURLWithPath: openSSLPath)
3636
}
3737

38-
private func manifest(for directory: URL) throws -> Data {
39-
var manifest: [String: String] = [:]
38+
private static func sourceFiles(in directory: URL) throws -> [String: Data] {
39+
var files: [String: Data] = [:]
4040

4141
let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path)
42+
4243
for relativePath in paths {
4344
let file = URL(fileURLWithPath: relativePath, relativeTo: directory)
4445
guard !file.hasDirectoryPath else {
4546
continue
4647
}
4748

48-
let hash = try SHA256.hash(data: Data(contentsOf: file))
49-
manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined()
49+
guard !(file.lastPathComponent == ".gitkeep" || file.lastPathComponent == ".DS_Store") else {
50+
continue
51+
}
52+
53+
files[relativePath] = try Data(contentsOf: file)
54+
}
55+
56+
return files
57+
}
58+
59+
private func manifest(for sourceFiles: [String: Data]) throws -> Data {
60+
let manifest = sourceFiles.mapValues { data in
61+
SHA256.hash(data: data).map { "0\(String($0, radix: 16))".suffix(2) }.joined()
5062
}
5163

52-
return try encoder.encode(manifest)
64+
return try self.encoder.encode(manifest)
5365
}
5466

5567
private func signature(for manifest: Data) throws -> Data {
@@ -94,7 +106,7 @@ public struct OrderBuilder: Sendable {
94106
],
95107
certificate: Certificate(pemEncoded: self.pemCertificate),
96108
privateKey: .init(pemEncoded: self.pemPrivateKey),
97-
signingTime: Date()
109+
signingTime: Date.now
98110
)
99111
return Data(signature)
100112
}
@@ -122,28 +134,24 @@ public struct OrderBuilder: Sendable {
122134
try FileManager.default.copyItem(at: filesDirectory, to: tempDir)
123135
defer { try? FileManager.default.removeItem(at: tempDir) }
124136

125-
var files: [ArchiveFile] = []
137+
var archiveFiles: [ArchiveFile] = []
126138

127139
let orderJSON = try self.encoder.encode(order)
128140
try orderJSON.write(to: tempDir.appendingPathComponent("order.json"))
129-
files.append(ArchiveFile(filename: "order.json", data: orderJSON))
141+
archiveFiles.append(ArchiveFile(filename: "order.json", data: orderJSON))
130142

131-
let manifest = try self.manifest(for: tempDir)
132-
files.append(ArchiveFile(filename: "manifest.json", data: manifest))
133-
try files.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest)))
143+
let sourceFiles = try Self.sourceFiles(in: tempDir)
134144

135-
let paths = try FileManager.default.subpathsOfDirectory(atPath: filesDirectory.path)
136-
for relativePath in paths {
137-
let file = URL(fileURLWithPath: relativePath, relativeTo: tempDir)
138-
guard !file.hasDirectoryPath else {
139-
continue
140-
}
145+
let manifest = try self.manifest(for: sourceFiles)
146+
archiveFiles.append(ArchiveFile(filename: "manifest.json", data: manifest))
147+
try archiveFiles.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest)))
141148

142-
try files.append(ArchiveFile(filename: relativePath, data: Data(contentsOf: file)))
149+
for file in sourceFiles {
150+
archiveFiles.append(ArchiveFile(filename: file.key, data: file.value))
143151
}
144152

145153
let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).order")
146-
try Zip.zipData(archiveFiles: files, zipFilePath: zipFile)
154+
try Zip.zipData(archiveFiles: archiveFiles, zipFilePath: zipFile)
147155
return try Data(contentsOf: zipFile)
148156
}
149157
}

Diff for: Sources/WalletPasses/PassBuilder.swift

+48-20
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,53 @@ public struct PassBuilder: Sendable {
3535
self.openSSLURL = URL(fileURLWithPath: openSSLPath)
3636
}
3737

38-
private func manifest(for directory: URL) throws -> Data {
39-
var manifest: [String: String] = [:]
38+
private static func sourceFiles(in directory: URL, isPersonalized: Bool = false) throws -> [String: Data] {
39+
var files: [String: Data] = [:]
4040

4141
let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path)
42+
43+
if isPersonalized {
44+
guard
45+
paths.contains("personalizationLogo.png")
46+
|| paths.contains("[email protected]")
47+
|| paths.contains("[email protected]")
48+
|| paths.contains("[email protected]")
49+
else {
50+
throw WalletPassesError.noPersonalizationLogo
51+
}
52+
}
53+
54+
guard
55+
paths.contains("icon.png")
56+
|| paths.contains("[email protected]")
57+
|| paths.contains("[email protected]")
58+
|| paths.contains("[email protected]")
59+
else {
60+
throw WalletPassesError.noIcon
61+
}
62+
4263
for relativePath in paths {
4364
let file = URL(fileURLWithPath: relativePath, relativeTo: directory)
4465
guard !file.hasDirectoryPath else {
4566
continue
4667
}
4768

48-
let hash = try Insecure.SHA1.hash(data: Data(contentsOf: file))
49-
manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined()
69+
guard !(file.lastPathComponent == ".gitkeep" || file.lastPathComponent == ".DS_Store") else {
70+
continue
71+
}
72+
73+
files[relativePath] = try Data(contentsOf: file)
74+
}
75+
76+
return files
77+
}
78+
79+
private func manifest(for sourceFiles: [String: Data]) throws -> Data {
80+
let manifest = sourceFiles.mapValues { data in
81+
Insecure.SHA1.hash(data: data).map { "0\(String($0, radix: 16))".suffix(2) }.joined()
5082
}
5183

52-
return try encoder.encode(manifest)
84+
return try self.encoder.encode(manifest)
5385
}
5486

5587
/// Generates a signature for a given manifest or personalization token.
@@ -99,7 +131,7 @@ public struct PassBuilder: Sendable {
99131
],
100132
certificate: Certificate(pemEncoded: self.pemCertificate),
101133
privateKey: .init(pemEncoded: self.pemPrivateKey),
102-
signingTime: Date()
134+
signingTime: Date.now
103135
)
104136
return Data(signature)
105137
}
@@ -129,35 +161,31 @@ public struct PassBuilder: Sendable {
129161
try FileManager.default.copyItem(at: filesDirectory, to: tempDir)
130162
defer { try? FileManager.default.removeItem(at: tempDir) }
131163

132-
var files: [ArchiveFile] = []
164+
var archiveFiles: [ArchiveFile] = []
133165

134166
let passJSON = try self.encoder.encode(pass)
135167
try passJSON.write(to: tempDir.appendingPathComponent("pass.json"))
136-
files.append(ArchiveFile(filename: "pass.json", data: passJSON))
168+
archiveFiles.append(ArchiveFile(filename: "pass.json", data: passJSON))
137169

138170
// Pass Personalization
139171
if let personalization {
140172
let personalizationJSONData = try self.encoder.encode(personalization)
141173
try personalizationJSONData.write(to: tempDir.appendingPathComponent("personalization.json"))
142-
files.append(ArchiveFile(filename: "personalization.json", data: personalizationJSONData))
174+
archiveFiles.append(ArchiveFile(filename: "personalization.json", data: personalizationJSONData))
143175
}
144176

145-
let manifest = try self.manifest(for: tempDir)
146-
files.append(ArchiveFile(filename: "manifest.json", data: manifest))
147-
try files.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest)))
177+
let sourceFiles = try Self.sourceFiles(in: tempDir, isPersonalized: personalization != nil)
148178

149-
let paths = try FileManager.default.subpathsOfDirectory(atPath: filesDirectory.path)
150-
for relativePath in paths {
151-
let file = URL(fileURLWithPath: relativePath, relativeTo: tempDir)
152-
guard !file.hasDirectoryPath else {
153-
continue
154-
}
179+
let manifest = try self.manifest(for: sourceFiles)
180+
archiveFiles.append(ArchiveFile(filename: "manifest.json", data: manifest))
181+
try archiveFiles.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest)))
155182

156-
try files.append(ArchiveFile(filename: relativePath, data: Data(contentsOf: file)))
183+
for file in sourceFiles {
184+
archiveFiles.append(ArchiveFile(filename: file.key, data: file.value))
157185
}
158186

159187
let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).pkpass")
160-
try Zip.zipData(archiveFiles: files, zipFilePath: zipFile)
188+
try Zip.zipData(archiveFiles: archiveFiles, zipFilePath: zipFile)
161189
return try Data(contentsOf: zipFile)
162190
}
163191
}

Diff for: Sources/WalletPasses/WalletPassesError.swift

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ public struct WalletPassesError: Error, Sendable, Equatable {
44
public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable {
55
enum Base: String, Sendable, Equatable {
66
case noSourceFiles
7+
case noIcon
8+
case noPersonalizationLogo
79
case noOpenSSLExecutable
810
case invalidNumberOfPasses
911
}
@@ -16,6 +18,10 @@ public struct WalletPassesError: Error, Sendable, Equatable {
1618

1719
/// The path for the source files is not a directory.
1820
public static let noSourceFiles = Self(.noSourceFiles)
21+
/// The `[email protected]` file is missing.
22+
public static let noIcon = Self(.noIcon)
23+
/// The `[email protected]` file is missing.
24+
public static let noPersonalizationLogo = Self(.noPersonalizationLogo)
1925
/// The `openssl` executable is missing.
2026
public static let noOpenSSLExecutable = Self(.noOpenSSLExecutable)
2127
/// The number of passes to bundle is invalid.
@@ -51,6 +57,12 @@ public struct WalletPassesError: Error, Sendable, Equatable {
5157
/// The path for the source files is not a directory.
5258
public static let noSourceFiles = Self(errorType: .noSourceFiles)
5359

60+
/// The `[email protected]` file is missing.
61+
public static let noIcon = Self(errorType: .noIcon)
62+
63+
/// The `[email protected]` file is missing.
64+
public static let noPersonalizationLogo = Self(errorType: .noPersonalizationLogo)
65+
5466
/// The `openssl` executable is missing.
5567
public static let noOpenSSLExecutable = Self(errorType: .noOpenSSLExecutable)
5668

Diff for: Tests/WalletPassesTests/WalletPassesTests.swift

+41
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,47 @@ struct WalletPassesTests {
105105
}
106106
}
107107

108+
@Test("Build Pass without Icon")
109+
func buildWithoutIcon() throws {
110+
let builder = PassBuilder(
111+
pemWWDRCertificate: TestCertificate.pemWWDRCertificate,
112+
pemCertificate: TestCertificate.pemCertificate,
113+
pemPrivateKey: TestCertificate.pemPrivateKey
114+
)
115+
116+
#expect(throws: WalletPassesError.noIcon) {
117+
try builder.build(
118+
pass: pass,
119+
sourceFilesDirectoryPath: "\(FileManager.default.currentDirectoryPath)/Tests/WalletPassesTests"
120+
)
121+
}
122+
}
123+
124+
@Test("Build Personalizable Pass without Personalization Logo")
125+
func buildPersonalizedWithoutLogo() throws {
126+
let builder = PassBuilder(
127+
pemWWDRCertificate: TestCertificate.pemWWDRCertificate,
128+
pemCertificate: TestCertificate.pemCertificate,
129+
pemPrivateKey: TestCertificate.pemPrivateKey
130+
)
131+
132+
let testPersonalization = PersonalizationJSON(
133+
requiredPersonalizationFields: [
134+
.name,
135+
.emailAddress,
136+
],
137+
description: "Test Personalization"
138+
)
139+
140+
#expect(throws: WalletPassesError.noPersonalizationLogo) {
141+
try builder.build(
142+
pass: pass,
143+
sourceFilesDirectoryPath: "\(FileManager.default.currentDirectoryPath)/Tests/WalletPassesTests",
144+
personalization: testPersonalization
145+
)
146+
}
147+
}
148+
108149
private func testRoundTripped(_ bundle: Data, with personalization: PersonalizationJSON? = nil) throws {
109150
let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass")
110151
try bundle.write(to: passURL)

0 commit comments

Comments
 (0)