Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for required files #3

Merged
merged 3 commits into from
Jan 1, 2025
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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "swift-wallet",
platforms: [
.macOS(.v11)
.macOS(.v12)
],
products: [
.library(name: "WalletPasses", targets: ["WalletPasses"]),
Expand Down
46 changes: 27 additions & 19 deletions Sources/WalletOrders/OrderBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,33 @@ public struct OrderBuilder: 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) throws -> [String: Data] {
var files: [String: Data] = [:]

let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path)

for relativePath in paths {
let file = URL(fileURLWithPath: relativePath, relativeTo: directory)
guard !file.hasDirectoryPath else {
continue
}

let hash = try SHA256.hash(data: Data(contentsOf: file))
manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined()
guard !(file.lastPathComponent == ".gitkeep" || file.lastPathComponent == ".DS_Store") else {
continue
}

files[relativePath] = try Data(contentsOf: file)
}

return files
}

private func manifest(for sourceFiles: [String: Data]) throws -> Data {
let manifest = sourceFiles.mapValues { data in
SHA256.hash(data: data).map { "0\(String($0, radix: 16))".suffix(2) }.joined()
}

return try encoder.encode(manifest)
return try self.encoder.encode(manifest)
}

private func signature(for manifest: Data) throws -> Data {
Expand Down Expand Up @@ -94,7 +106,7 @@ public struct OrderBuilder: Sendable {
],
certificate: Certificate(pemEncoded: self.pemCertificate),
privateKey: .init(pemEncoded: self.pemPrivateKey),
signingTime: Date()
signingTime: Date.now
)
return Data(signature)
}
Expand Down Expand Up @@ -122,28 +134,24 @@ public struct OrderBuilder: Sendable {
try FileManager.default.copyItem(at: filesDirectory, to: tempDir)
defer { try? FileManager.default.removeItem(at: tempDir) }

var files: [ArchiveFile] = []
var archiveFiles: [ArchiveFile] = []

let orderJSON = try self.encoder.encode(order)
try orderJSON.write(to: tempDir.appendingPathComponent("order.json"))
files.append(ArchiveFile(filename: "order.json", data: orderJSON))
archiveFiles.append(ArchiveFile(filename: "order.json", data: orderJSON))

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)

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).order")
try Zip.zipData(archiveFiles: files, zipFilePath: zipFile)
try Zip.zipData(archiveFiles: archiveFiles, zipFilePath: zipFile)
return try Data(contentsOf: zipFile)
}
}
68 changes: 48 additions & 20 deletions Sources/WalletPasses/PassBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,53 @@ 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)

if isPersonalized {
guard
paths.contains("personalizationLogo.png")
|| paths.contains("[email protected]")
|| paths.contains("[email protected]")
|| paths.contains("[email protected]")
else {
throw WalletPassesError.noPersonalizationLogo
}
}

guard
paths.contains("icon.png")
|| paths.contains("[email protected]")
|| paths.contains("[email protected]")
|| paths.contains("[email protected]")
else {
throw WalletPassesError.noIcon
}

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 !(file.lastPathComponent == ".gitkeep" || file.lastPathComponent == ".DS_Store") else {
continue
}

files[relativePath] = try Data(contentsOf: file)
}

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 encoder.encode(manifest)
return try self.encoder.encode(manifest)
}

/// Generates a signature for a given manifest or personalization token.
Expand Down Expand Up @@ -99,7 +131,7 @@ public struct PassBuilder: Sendable {
],
certificate: Certificate(pemEncoded: self.pemCertificate),
privateKey: .init(pemEncoded: self.pemPrivateKey),
signingTime: Date()
signingTime: Date.now
)
return Data(signature)
}
Expand Down Expand Up @@ -129,35 +161,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)
}
}
12 changes: 12 additions & 0 deletions Sources/WalletPasses/WalletPassesError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ public struct WalletPassesError: Error, Sendable, Equatable {
public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable {
enum Base: String, Sendable, Equatable {
case noSourceFiles
case noIcon
case noPersonalizationLogo
case noOpenSSLExecutable
case invalidNumberOfPasses
}
Expand All @@ -16,6 +18,10 @@ public struct WalletPassesError: Error, Sendable, Equatable {

/// The path for the source files is not a directory.
public static let noSourceFiles = Self(.noSourceFiles)
/// The `[email protected]` file is missing.
public static let noIcon = Self(.noIcon)
/// The `[email protected]` file is missing.
public static let noPersonalizationLogo = Self(.noPersonalizationLogo)
/// The `openssl` executable is missing.
public static let noOpenSSLExecutable = Self(.noOpenSSLExecutable)
/// The number of passes to bundle is invalid.
Expand Down Expand Up @@ -51,6 +57,12 @@ public struct WalletPassesError: Error, Sendable, Equatable {
/// The path for the source files is not a directory.
public static let noSourceFiles = Self(errorType: .noSourceFiles)

/// The `[email protected]` file is missing.
public static let noIcon = Self(errorType: .noIcon)

/// The `[email protected]` file is missing.
public static let noPersonalizationLogo = Self(errorType: .noPersonalizationLogo)

/// The `openssl` executable is missing.
public static let noOpenSSLExecutable = Self(errorType: .noOpenSSLExecutable)

Expand Down
41 changes: 41 additions & 0 deletions Tests/WalletPassesTests/WalletPassesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,47 @@ struct WalletPassesTests {
}
}

@Test("Build Pass without Icon")
func buildWithoutIcon() throws {
let builder = PassBuilder(
pemWWDRCertificate: TestCertificate.pemWWDRCertificate,
pemCertificate: TestCertificate.pemCertificate,
pemPrivateKey: TestCertificate.pemPrivateKey
)

#expect(throws: WalletPassesError.noIcon) {
try builder.build(
pass: pass,
sourceFilesDirectoryPath: "\(FileManager.default.currentDirectoryPath)/Tests/WalletPassesTests"
)
}
}

@Test("Build Personalizable Pass without Personalization Logo")
func buildPersonalizedWithoutLogo() throws {
let builder = PassBuilder(
pemWWDRCertificate: TestCertificate.pemWWDRCertificate,
pemCertificate: TestCertificate.pemCertificate,
pemPrivateKey: TestCertificate.pemPrivateKey
)

let testPersonalization = PersonalizationJSON(
requiredPersonalizationFields: [
.name,
.emailAddress,
],
description: "Test Personalization"
)

#expect(throws: WalletPassesError.noPersonalizationLogo) {
try builder.build(
pass: pass,
sourceFilesDirectoryPath: "\(FileManager.default.currentDirectoryPath)/Tests/WalletPassesTests",
personalization: testPersonalization
)
}
}

private func testRoundTripped(_ bundle: Data, with personalization: PersonalizationJSON? = nil) throws {
let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass")
try bundle.write(to: passURL)
Expand Down
Loading