-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c8c5fc6
commit 7e38f2e
Showing
5 changed files
with
126 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,21 +35,65 @@ public struct PassBuilder: Sendable { | |
self.openSSLURL = URL(fileURLWithPath: openSSLPath) | ||
} | ||
|
||
private func manifest(for directory: URL) throws -> Data { | ||
var manifest: [String: String] = [:] | ||
private static func sourceFiles(in directory: URL, isPersonalized: Bool = false) throws -> [String: Data] { | ||
var files: [String: Data] = [:] | ||
|
||
let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path) | ||
|
||
guard paths.contains("pass.json") else { | ||
throw WalletPassesError.noPassJSONFile | ||
} | ||
|
||
guard | ||
paths.contains("icon.png") | ||
|| paths.contains("[email protected]") | ||
|| paths.contains("[email protected]") | ||
|| paths.contains("[email protected]") | ||
else { | ||
throw WalletPassesError.noIconFile | ||
} | ||
|
||
if isPersonalized { | ||
guard paths.contains("personalization.json") else { | ||
throw WalletPassesError.noPersonalizationJSONFile | ||
} | ||
|
||
guard | ||
paths.contains("personalizationLogo.png") | ||
|| paths.contains("[email protected]") | ||
|| paths.contains("[email protected]") | ||
|| paths.contains("[email protected]") | ||
else { | ||
throw WalletPassesError.noPersonalizationLogoFile | ||
} | ||
} | ||
|
||
for relativePath in paths { | ||
let file = URL(fileURLWithPath: relativePath, relativeTo: directory) | ||
guard !file.hasDirectoryPath else { | ||
continue | ||
} | ||
|
||
let hash = try Insecure.SHA1.hash(data: Data(contentsOf: file)) | ||
manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() | ||
guard FileManager.default.fileExists(atPath: file.path) else { | ||
continue | ||
} | ||
|
||
guard !(file.lastPathComponent == ".gitkeep" || file.lastPathComponent == ".DS_Store") else { | ||
continue | ||
} | ||
|
||
files[relativePath] = try Data(contentsOf: file) | ||
} | ||
|
||
return try encoder.encode(manifest) | ||
return files | ||
} | ||
|
||
private func manifest(for sourceFiles: [String: Data]) throws -> Data { | ||
let manifest = sourceFiles.mapValues { data in | ||
Insecure.SHA1.hash(data: data).map { "0\(String($0, radix: 16))".suffix(2) }.joined() | ||
} | ||
|
||
return try self.encoder.encode(manifest) | ||
} | ||
|
||
/// Generates a signature for a given manifest or personalization token. | ||
|
@@ -99,7 +143,7 @@ public struct PassBuilder: Sendable { | |
], | ||
certificate: Certificate(pemEncoded: self.pemCertificate), | ||
privateKey: .init(pemEncoded: self.pemPrivateKey), | ||
signingTime: Date() | ||
signingTime: Date.now | ||
) | ||
return Data(signature) | ||
} | ||
|
@@ -129,35 +173,31 @@ public struct PassBuilder: Sendable { | |
try FileManager.default.copyItem(at: filesDirectory, to: tempDir) | ||
defer { try? FileManager.default.removeItem(at: tempDir) } | ||
|
||
var files: [ArchiveFile] = [] | ||
var archiveFiles: [ArchiveFile] = [] | ||
|
||
let passJSON = try self.encoder.encode(pass) | ||
try passJSON.write(to: tempDir.appendingPathComponent("pass.json")) | ||
files.append(ArchiveFile(filename: "pass.json", data: passJSON)) | ||
archiveFiles.append(ArchiveFile(filename: "pass.json", data: passJSON)) | ||
|
||
// Pass Personalization | ||
if let personalization { | ||
let personalizationJSONData = try self.encoder.encode(personalization) | ||
try personalizationJSONData.write(to: tempDir.appendingPathComponent("personalization.json")) | ||
files.append(ArchiveFile(filename: "personalization.json", data: personalizationJSONData)) | ||
archiveFiles.append(ArchiveFile(filename: "personalization.json", data: personalizationJSONData)) | ||
} | ||
|
||
let manifest = try self.manifest(for: tempDir) | ||
files.append(ArchiveFile(filename: "manifest.json", data: manifest)) | ||
try files.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest))) | ||
let sourceFiles = try Self.sourceFiles(in: tempDir, isPersonalized: personalization != nil) | ||
|
||
let paths = try FileManager.default.subpathsOfDirectory(atPath: filesDirectory.path) | ||
for relativePath in paths { | ||
let file = URL(fileURLWithPath: relativePath, relativeTo: tempDir) | ||
guard !file.hasDirectoryPath else { | ||
continue | ||
} | ||
let manifest = try self.manifest(for: sourceFiles) | ||
archiveFiles.append(ArchiveFile(filename: "manifest.json", data: manifest)) | ||
try archiveFiles.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest))) | ||
|
||
try files.append(ArchiveFile(filename: relativePath, data: Data(contentsOf: file))) | ||
for file in sourceFiles { | ||
archiveFiles.append(ArchiveFile(filename: file.key, data: file.value)) | ||
} | ||
|
||
let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).pkpass") | ||
try Zip.zipData(archiveFiles: files, zipFilePath: zipFile) | ||
try Zip.zipData(archiveFiles: archiveFiles, zipFilePath: zipFile) | ||
return try Data(contentsOf: zipFile) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,10 @@ public struct WalletPassesError: Error, Sendable, Equatable { | |
public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable { | ||
enum Base: String, Sendable, Equatable { | ||
case noSourceFiles | ||
case noPassJSONFile | ||
case noIconFile | ||
case noPersonalizationJSONFile | ||
case noPersonalizationLogoFile | ||
case noOpenSSLExecutable | ||
case invalidNumberOfPasses | ||
} | ||
|
@@ -16,6 +20,14 @@ public struct WalletPassesError: Error, Sendable, Equatable { | |
|
||
/// The path for the source files is not a directory. | ||
public static let noSourceFiles = Self(.noSourceFiles) | ||
/// The `pass.json` file is missing. | ||
public static let noPassJSONFile = Self(.noPassJSONFile) | ||
/// The `[email protected]` file is missing. | ||
public static let noIconFile = Self(.noIconFile) | ||
/// The `personalization.json` file is missing. | ||
public static let noPersonalizationJSONFile = Self(.noPersonalizationJSONFile) | ||
/// The `[email protected]` file is missing. | ||
public static let noPersonalizationLogoFile = Self(.noPersonalizationLogoFile) | ||
/// The `openssl` executable is missing. | ||
public static let noOpenSSLExecutable = Self(.noOpenSSLExecutable) | ||
/// The number of passes to bundle is invalid. | ||
|
@@ -51,6 +63,18 @@ public struct WalletPassesError: Error, Sendable, Equatable { | |
/// The path for the source files is not a directory. | ||
public static let noSourceFiles = Self(errorType: .noSourceFiles) | ||
|
||
/// The `pass.json` file is missing. | ||
public static let noPassJSONFile = Self(errorType: .noPassJSONFile) | ||
|
||
/// The `[email protected]` file is missing. | ||
public static let noIconFile = Self(errorType: .noIconFile) | ||
|
||
/// The `personalization.json` file is missing. | ||
public static let noPersonalizationJSONFile = Self(errorType: .noPersonalizationJSONFile) | ||
|
||
/// The `[email protected]` file is missing. | ||
public static let noPersonalizationLogoFile = Self(errorType: .noPersonalizationLogoFile) | ||
|
||
/// The `openssl` executable is missing. | ||
public static let noOpenSSLExecutable = Self(errorType: .noOpenSSLExecutable) | ||
|
||
|