Skip to content

Commit

Permalink
Check for required files
Browse files Browse the repository at this point in the history
  • Loading branch information
fpseverino committed Jan 1, 2025
1 parent c8c5fc6 commit 7e38f2e
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 40 deletions.
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
54 changes: 35 additions & 19 deletions Sources/WalletOrders/OrderBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,41 @@ 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)

guard paths.contains("order.json") else {
throw WalletOrdersError.noOrderJSONFile
}

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 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
SHA256.hash(data: data).map { "0\(String($0, radix: 16))".suffix(2) }.joined()
}

return try self.encoder.encode(manifest)
}

private func signature(for manifest: Data) throws -> Data {
Expand Down Expand Up @@ -94,7 +114,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 +142,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)
}
}
6 changes: 6 additions & 0 deletions Sources/WalletOrders/WalletOrdersError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public struct WalletOrdersError: Error, Sendable, Equatable {
public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable {
enum Base: String, Sendable, Equatable {
case noSourceFiles
case noOrderJSONFile
case noOpenSSLExecutable
}

Expand All @@ -15,6 +16,8 @@ public struct WalletOrdersError: Error, Sendable, Equatable {

/// The path for the source files is not a directory.
public static let noSourceFiles = Self(.noSourceFiles)
/// The `order.json` file is missing.
public static let noOrderJSONFile = Self(.noOrderJSONFile)
/// The `openssl` executable is missing.
public static let noOpenSSLExecutable = Self(.noOpenSSLExecutable)

Expand Down Expand Up @@ -48,6 +51,9 @@ public struct WalletOrdersError: Error, Sendable, Equatable {
/// The path for the source files is not a directory.
public static let noSourceFiles = Self(errorType: .noSourceFiles)

/// The `order.json` file is missing.
public static let noOrderJSONFile = Self(errorType: .noOrderJSONFile)

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

Expand Down
80 changes: 60 additions & 20 deletions Sources/WalletPasses/PassBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
}
24 changes: 24 additions & 0 deletions Sources/WalletPasses/WalletPassesError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 7e38f2e

Please sign in to comment.