From 53c56090168ddd9794094bbef72c96d6fe222f30 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sat, 25 Mar 2023 16:52:25 +0100 Subject: [PATCH] Generate keys with GYB (#22) --- .swiftformat | 2 +- Package.swift | 3 + README.md | 24 + Sources/K1/K1/ECDH/KeyAgreement.swift | 13 +- .../ECDSA/ECDSASignatureNonRecoverable.swift | 12 +- .../K1/ECDSA/ECDSASignatureRecoverable.swift | 12 +- Sources/K1/K1/Keys/Keys.swift | 435 ++++++ Sources/K1/K1/Keys/Keys.swift.gyb | 128 ++ .../PrivateKey/PrivateKeyOf+Feature.swift | 55 - .../PrivateKeyImplementation.swift | 0 .../Keys/PublicKey/PublicKeyOf+Feature.swift | 50 - .../PublicKeyImplementation.swift | 0 Sources/K1/K1/Schnorr/Schnorr+Signature.swift | 3 + Sources/K1/K1/Schnorr/Schnorr.swift | 10 +- .../generate_boilerplate_files_with_gyb.sh | 20 + scripts/gyb | 3 + scripts/gyb.py | 1260 +++++++++++++++++ scripts/gyb.pyc | Bin 0 -> 36305 bytes 18 files changed, 1889 insertions(+), 141 deletions(-) create mode 100644 Sources/K1/K1/Keys/Keys.swift create mode 100644 Sources/K1/K1/Keys/Keys.swift.gyb delete mode 100644 Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift rename Sources/K1/K1/Keys/{PrivateKey => }/PrivateKeyImplementation.swift (100%) delete mode 100644 Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift rename Sources/K1/K1/Keys/{PublicKey => }/PublicKeyImplementation.swift (100%) create mode 100755 scripts/generate_boilerplate_files_with_gyb.sh create mode 100755 scripts/gyb create mode 100644 scripts/gyb.py create mode 100644 scripts/gyb.pyc diff --git a/.swiftformat b/.swiftformat index 51c8c4e..1296771 100644 --- a/.swiftformat +++ b/.swiftformat @@ -13,4 +13,4 @@ --header strip # file options ---exclude .build,Sources/secp256k1,Sources/K1/Support/ThirdyParty +--exclude .build,Sources/secp256k1,Sources/K1/Support/ThirdyParty,**/*.swift.gyb diff --git a/Package.swift b/Package.swift index b93ef75..1c547d1 100644 --- a/Package.swift +++ b/Package.swift @@ -71,6 +71,9 @@ let package = Package( dependencies: [ "secp256k1", ], + exclude: [ + "K1/Keys/Keys.swift.gyb", + ], swiftSettings: [ .define("CRYPTO_IN_SWIFTPM_FORCE_BUILD_API"), ] diff --git a/README.md b/README.md index a6f59f0..c0a91cd 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,30 @@ Stand in root and run To clone the dependency [libsecp256k1][lib], using commit [427bc3cdcfbc74778070494daab1ae5108c71368](https://github.com/bitcoin-core/secp256k1/commit/427bc3cdcfbc74778070494daab1ae5108c71368) (semver 0.3.0) + +## `gyb` + +Some of the files in this project are autogenerated (metaprogramming) using the Swift Utils tools called [gyb](https://github.com/apple/swift/blob/main/utils/gyb.py) (_"generate your boilerplate"_). `gyb` is included in [`./scripts/gyb`](scripts/gyb). + +`gyb` will generate some `Foobar.swift` Swift file from some `Foobar.swift.gyb` _template_ file. **You should not edit `Foobar.swift` directly**, since all manual edits in that generated file will be overwritten the next time `gyb` is run. + +You run `gyb` for a single file like so: + +```bash +./scripts/gyb --line-directive "" Sources/Foobar.swift.gyb -o Sources/Foobar.swift +``` + +More conveniently you can run the bash script `./scripts/generate_boilerplate_files_with_gyb.sh` to generate all Swift files from their corresponding gyb template. + +**If you add a new `.gyb` file, you should append a `// MARK: - Generated file, do NOT edit` warning** inside it, e.g. + +```swift +// MARK: - Generated file, do NOT edit +// any edits of this file WILL be overwritten and thus discarded +// see section `gyb` in `README` for details. +``` + + # Alternatives - [GigaBitcoin/secp256k1.swift](https://github.com/GigaBitcoin/secp256k1.swift) (also using `libsecp256k1`, ⚠️ possibly unsafe, ✅ Schnorr support) diff --git a/Sources/K1/K1/ECDH/KeyAgreement.swift b/Sources/K1/K1/ECDH/KeyAgreement.swift index 5d5c1ae..7869ac0 100644 --- a/Sources/K1/K1/ECDH/KeyAgreement.swift +++ b/Sources/K1/K1/ECDH/KeyAgreement.swift @@ -1,20 +1,11 @@ import struct CryptoKit.SharedSecret import Foundation -// MARK: - K1Feature -public protocol K1Feature { - associatedtype PublicKey: K1PublicKeyProtocol -} - // MARK: - K1.KeyAgreement extension K1 { /// A mechanism used to create a shared secret between two users by performing `secp256k1` elliptic curve Diffie Hellman (ECDH) key exchange. - public enum KeyAgreement: K1Feature { - /// A `secp256k1` private key used for key agreement. - public typealias PrivateKey = PrivateKeyOf - - /// A `secp256k1` public key used for key agreement. - public typealias PublicKey = PublicKeyOf + public enum KeyAgreement { + // Just a namespace } } diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift index 0ce6d1d..65db552 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift @@ -5,19 +5,15 @@ import Foundation // MARK: - K1.ECDSA.NonRecoverable extension K1.ECDSA { /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA), signatures that do not offer recovery of the public key. - public enum NonRecoverable: K1Feature { - /// A `secp256k1` private key used to create cryptographic signatures, - /// more specifically ECDSA signatures, that do not offer recovery of the public key. - public typealias PrivateKey = PrivateKeyOf - - /// A `secp256k1` public key used to verify cryptographic signatures, - /// more specifically ECDSA signatures, that do not offer recovery of the public key. - public typealias PublicKey = PublicKeyOf + public enum NonRecoverable { + // Just a namespace } } // MARK: - K1.ECDSA.NonRecoverable.Signature extension K1.ECDSA.NonRecoverable { + /// A `secp256k1` elliptic curve digital signature algorithm (ECDSA) signature, + /// from which users can recover a public key with the message that was signed. public struct Signature: Sendable, Hashable, ContiguousBytes { typealias Wrapped = FFI.ECDSA.NonRecovery.Wrapped internal let wrapped: Wrapped diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift index 4a616f8..6df07c4 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift @@ -5,14 +5,8 @@ import Foundation // MARK: - K1.ECDSA.Recoverable extension K1.ECDSA { /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA), signatures that do offers recovery of the public key. - public enum Recoverable: K1Feature { - /// A `secp256k1` private key used to create cryptographic signatures, - /// more specifically ECDSA signatures that offers recovery of the public key. - public typealias PrivateKey = PrivateKeyOf - - /// A `secp256k1` public key used to verify cryptographic signatures. - /// more specifically ECDSA signatures that offers recovery of the public key. - public typealias PublicKey = PublicKeyOf + public enum Recoverable { + // Just a namespace } } @@ -137,6 +131,8 @@ extension K1.ECDSA.Recoverable.PublicKey { // MARK: - K1.ECDSA.Recoverable.Signature extension K1.ECDSA.Recoverable { + /// A `secp256k1` elliptic curve digital signature algorithm (ECDSA) signature, + /// from which users **cannot** recover the public key, not without the `RecoveryID`. public struct Signature: Sendable, Hashable, ContiguousBytes { typealias Wrapped = FFI.ECDSA.Recovery.Wrapped private let wrapped: Wrapped diff --git a/Sources/K1/K1/Keys/Keys.swift b/Sources/K1/K1/Keys/Keys.swift new file mode 100644 index 0000000..8e34b53 --- /dev/null +++ b/Sources/K1/K1/Keys/Keys.swift @@ -0,0 +1,435 @@ +// MARK: - Generated file, do NOT edit +// any edits of this file WILL be overwritten and thus discarded +// see section `gyb` in `README` for details. + +import Foundation + +extension K1.Schnorr { + // MARK: Schnorr + PrivateKey + /// A `secp256k1` private key used to create cryptographic signatures, + /// more specifically Schnorr signatures. + public struct PrivateKey: Sendable, Hashable, K1PrivateKeyProtocol { + public init() { + self.init(impl: .init()) + } + + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PrivateKeyImplementation + internal let impl: Impl + internal let publicKeyImpl: K1._PublicKeyImplementation + + public typealias PublicKey = K1.Schnorr.PublicKey + public var publicKey: PublicKey { + try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) + } + + init(impl: Impl) { + self.impl = impl + self.publicKeyImpl = impl.publicKey + } + } + + // MARK: Schnorr + PublicKey + /// A `secp256k1` public key used to verify cryptographic signatures, + /// more specifically Schnorr signatures + public struct PublicKey: Sendable, Hashable, K1PublicKeyProtocol { + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(compressedRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(compressedRepresentation: compressedRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var compressedRepresentation: Data { + impl.compressedRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PublicKeyImplementation + internal let impl: Impl + internal init(impl: Impl) { + self.impl = impl + } + } +} + +extension K1.KeyAgreement { + // MARK: KeyAgreement + PrivateKey + /// A `secp256k1` private key used for key agreement. + public struct PrivateKey: Sendable, Hashable, K1PrivateKeyProtocol { + public init() { + self.init(impl: .init()) + } + + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PrivateKeyImplementation + internal let impl: Impl + internal let publicKeyImpl: K1._PublicKeyImplementation + + public typealias PublicKey = K1.KeyAgreement.PublicKey + public var publicKey: PublicKey { + try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) + } + + init(impl: Impl) { + self.impl = impl + self.publicKeyImpl = impl.publicKey + } + } + + // MARK: KeyAgreement + PublicKey + /// A `secp256k1` public key used for key agreement. + public struct PublicKey: Sendable, Hashable, K1PublicKeyProtocol { + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(compressedRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(compressedRepresentation: compressedRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var compressedRepresentation: Data { + impl.compressedRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PublicKeyImplementation + internal let impl: Impl + internal init(impl: Impl) { + self.impl = impl + } + } +} + +extension K1.ECDSA.NonRecoverable { + // MARK: ECDSA.NonRecoverable + PrivateKey + /// A `secp256k1` private key used to create cryptographic signatures, + /// more specifically ECDSA signatures, that do not offer recovery of the public key. + public struct PrivateKey: Sendable, Hashable, K1PrivateKeyProtocol { + public init() { + self.init(impl: .init()) + } + + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PrivateKeyImplementation + internal let impl: Impl + internal let publicKeyImpl: K1._PublicKeyImplementation + + public typealias PublicKey = K1.ECDSA.NonRecoverable.PublicKey + public var publicKey: PublicKey { + try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) + } + + init(impl: Impl) { + self.impl = impl + self.publicKeyImpl = impl.publicKey + } + } + + // MARK: ECDSA.NonRecoverable + PublicKey + /// A `secp256k1` public key used to verify cryptographic signatures, + /// more specifically ECDSA signatures, that do not offer recovery of the public key. + public struct PublicKey: Sendable, Hashable, K1PublicKeyProtocol { + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(compressedRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(compressedRepresentation: compressedRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var compressedRepresentation: Data { + impl.compressedRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PublicKeyImplementation + internal let impl: Impl + internal init(impl: Impl) { + self.impl = impl + } + } +} + +extension K1.ECDSA.Recoverable { + // MARK: ECDSA.Recoverable + PrivateKey + /// A `secp256k1` private key used to create cryptographic signatures, + /// more specifically ECDSA signatures that offers recovery of the public key. + public struct PrivateKey: Sendable, Hashable, K1PrivateKeyProtocol { + public init() { + self.init(impl: .init()) + } + + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PrivateKeyImplementation + internal let impl: Impl + internal let publicKeyImpl: K1._PublicKeyImplementation + + public typealias PublicKey = K1.ECDSA.Recoverable.PublicKey + public var publicKey: PublicKey { + try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) + } + + init(impl: Impl) { + self.impl = impl + self.publicKeyImpl = impl.publicKey + } + } + + // MARK: ECDSA.Recoverable + PublicKey + /// A `secp256k1` public key used to verify cryptographic signatures. + /// more specifically ECDSA signatures that offers recovery of the public key. + public struct PublicKey: Sendable, Hashable, K1PublicKeyProtocol { + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(compressedRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(compressedRepresentation: compressedRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var compressedRepresentation: Data { + impl.compressedRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PublicKeyImplementation + internal let impl: Impl + internal init(impl: Impl) { + self.impl = impl + } + } +} diff --git a/Sources/K1/K1/Keys/Keys.swift.gyb b/Sources/K1/K1/Keys/Keys.swift.gyb new file mode 100644 index 0000000..7dc790d --- /dev/null +++ b/Sources/K1/K1/Keys/Keys.swift.gyb @@ -0,0 +1,128 @@ +// MARK: - Generated file, do NOT edit +// any edits of this file WILL be overwritten and thus discarded +// see section `gyb` in `README` for details. + +import Foundation + +%{ + LIST_KEY_FOR_FEATURE_WITH_DOCS = [ + { "feature": "Schnorr", "docSK": "/// A `secp256k1` private key used to create cryptographic signatures,\n\t/// more specifically Schnorr signatures.", "docPK": "/// A `secp256k1` public key used to verify cryptographic signatures,\n\t/// more specifically Schnorr signatures" }, + { "feature": "KeyAgreement", "docSK": "/// A `secp256k1` private key used for key agreement.", "docPK": "/// A `secp256k1` public key used for key agreement." }, + { "feature": "ECDSA.NonRecoverable", "docSK": "/// A `secp256k1` private key used to create cryptographic signatures,\n\t/// more specifically ECDSA signatures, that do not offer recovery of the public key.", "docPK": "/// A `secp256k1` public key used to verify cryptographic signatures,\n\t/// more specifically ECDSA signatures, that do not offer recovery of the public key." }, + { "feature": "ECDSA.Recoverable", "docSK": "/// A `secp256k1` private key used to create cryptographic signatures,\n\t/// more specifically ECDSA signatures that offers recovery of the public key.", "docPK": "/// A `secp256k1` public key used to verify cryptographic signatures.\n\t/// more specifically ECDSA signatures that offers recovery of the public key." }, + ] +}% +% for KEY_FOR_FEATURE_WITH_DOCS in LIST_KEY_FOR_FEATURE_WITH_DOCS: +%{ + FEATURE = KEY_FOR_FEATURE_WITH_DOCS["feature"] + DocPrivateKey = KEY_FOR_FEATURE_WITH_DOCS["docSK"] + DocPublicKey = KEY_FOR_FEATURE_WITH_DOCS["docPK"] +}% +extension K1.${FEATURE} { + // MARK: ${FEATURE} + PrivateKey + ${DocPrivateKey} + public struct PrivateKey: Sendable, Hashable, K1PrivateKeyProtocol { + public init() { + self.init(impl: .init()) + } + + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PrivateKeyImplementation + internal let impl: Impl + internal let publicKeyImpl: K1._PublicKeyImplementation + + public typealias PublicKey = K1.${FEATURE}.PublicKey + public var publicKey: PublicKey { + try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) + } + + init(impl: Impl) { + self.impl = impl + self.publicKeyImpl = impl.publicKey + } + } + + // MARK: ${FEATURE} + PublicKey + ${DocPublicKey} + public struct PublicKey: Sendable, Hashable, K1PublicKeyProtocol { + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(compressedRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(compressedRepresentation: compressedRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var compressedRepresentation: Data { + impl.compressedRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1._PublicKeyImplementation + internal let impl: Impl + internal init(impl: Impl) { + self.impl = impl + } + } +} + + % end + % end diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift b/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift deleted file mode 100644 index 8436038..0000000 --- a/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation - -// TODO: Replace typealiases and this single existential to use existentials generated with GYB. -public struct PrivateKeyOf: Sendable, Hashable, K1PrivateKeyProtocol { - public init() { - self.init(impl: .init()) - } - - public init(rawRepresentation: some ContiguousBytes) throws { - try self.init(impl: .init(rawRepresentation: rawRepresentation)) - } - - public init(x963Representation: some ContiguousBytes) throws { - try self.init(impl: .init(x963Representation: x963Representation)) - } - - public init(derRepresentation: some RandomAccessCollection) throws { - try self.init(impl: .init(derRepresentation: derRepresentation)) - } - - public init(pemRepresentation: String) throws { - try self.init(impl: .init(pemRepresentation: pemRepresentation)) - } - - public var rawRepresentation: Data { - impl.rawRepresentation - } - - public var x963Representation: Data { - impl.x963Representation - } - - public var derRepresentation: Data { - impl.derRepresentation - } - - public var pemRepresentation: String { - impl.pemRepresentation - } - - typealias Impl = K1._PrivateKeyImplementation - internal let impl: Impl - internal let publicKeyImpl: K1._PublicKeyImplementation - - public typealias PublicKey = Feature.PublicKey - - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } - - init(impl: Impl) { - self.impl = impl - self.publicKeyImpl = impl.publicKey - } -} diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKeyImplementation.swift b/Sources/K1/K1/Keys/PrivateKeyImplementation.swift similarity index 100% rename from Sources/K1/K1/Keys/PrivateKey/PrivateKeyImplementation.swift rename to Sources/K1/K1/Keys/PrivateKeyImplementation.swift diff --git a/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift b/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift deleted file mode 100644 index 2a12e9e..0000000 --- a/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation - -// TODO: Replace typealiases and this single existential to use existentials generated with GYB. -public struct PublicKeyOf: Sendable, Hashable, K1PublicKeyProtocol { - public init(rawRepresentation: some ContiguousBytes) throws { - try self.init(impl: .init(rawRepresentation: rawRepresentation)) - } - - public init(compressedRepresentation: some ContiguousBytes) throws { - try self.init(impl: .init(compressedRepresentation: compressedRepresentation)) - } - - public init(x963Representation: some ContiguousBytes) throws { - try self.init(impl: .init(x963Representation: x963Representation)) - } - - public init(derRepresentation: some RandomAccessCollection) throws { - try self.init(impl: .init(derRepresentation: derRepresentation)) - } - - public init(pemRepresentation: String) throws { - try self.init(impl: .init(pemRepresentation: pemRepresentation)) - } - - public var rawRepresentation: Data { - impl.rawRepresentation - } - - public var x963Representation: Data { - impl.x963Representation - } - - public var derRepresentation: Data { - impl.derRepresentation - } - - public var compressedRepresentation: Data { - impl.compressedRepresentation - } - - public var pemRepresentation: String { - impl.pemRepresentation - } - - typealias Impl = K1._PublicKeyImplementation - internal let impl: Impl - internal init(impl: Impl) { - self.impl = impl - } -} diff --git a/Sources/K1/K1/Keys/PublicKey/PublicKeyImplementation.swift b/Sources/K1/K1/Keys/PublicKeyImplementation.swift similarity index 100% rename from Sources/K1/K1/Keys/PublicKey/PublicKeyImplementation.swift rename to Sources/K1/K1/Keys/PublicKeyImplementation.swift diff --git a/Sources/K1/K1/Schnorr/Schnorr+Signature.swift b/Sources/K1/K1/Schnorr/Schnorr+Signature.swift index dec60db..160b571 100644 --- a/Sources/K1/K1/Schnorr/Schnorr+Signature.swift +++ b/Sources/K1/K1/Schnorr/Schnorr+Signature.swift @@ -2,6 +2,9 @@ import Foundation // MARK: - K1.Schnorr.Signature extension K1.Schnorr { + /// A `secp256k1` elliptic curve signature using the [Schnorr signature scheme][scheme]. + /// + /// [scheme]: https://doi.org/10.1007%2F0-387-34805-0_22 public struct Signature: Sendable, Hashable { typealias Wrapped = FFI.Schnorr.Wrapped internal let wrapped: Wrapped diff --git a/Sources/K1/K1/Schnorr/Schnorr.swift b/Sources/K1/K1/Schnorr/Schnorr.swift index b7d42d6..3835b7b 100644 --- a/Sources/K1/K1/Schnorr/Schnorr.swift +++ b/Sources/K1/K1/Schnorr/Schnorr.swift @@ -5,14 +5,8 @@ import Foundation // MARK: - K1.Schnorr extension K1 { /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` and Schnorr signature scheme. - public enum Schnorr: K1Feature { - /// A `secp256k1` private key used to create cryptographic signatures, - /// more specifically Schnorr signatures. - public typealias PrivateKey = PrivateKeyOf - - /// A `secp256k1` public key used to verify cryptographic signatures, - /// more specifically Schnorr signatures. - public typealias PublicKey = PublicKeyOf + public enum Schnorr { + // Just a namespace } } diff --git a/scripts/generate_boilerplate_files_with_gyb.sh b/scripts/generate_boilerplate_files_with_gyb.sh new file mode 100755 index 0000000..5ce6dd7 --- /dev/null +++ b/scripts/generate_boilerplate_files_with_gyb.sh @@ -0,0 +1,20 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftCrypto open source project +## +## Copyright (c) 2020 Apple Inc. and the SwiftCrypto project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.md for the list of SwiftCrypto project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +set -eu +find . -name '*.gyb' | \ + while read file; do \ + ./scripts/gyb --line-directive '' -o "${file%.gyb}" "$file"; \ + done \ No newline at end of file diff --git a/scripts/gyb b/scripts/gyb new file mode 100755 index 0000000..dece788 --- /dev/null +++ b/scripts/gyb @@ -0,0 +1,3 @@ +#!/usr/bin/env python2.7 +import gyb +gyb.main() diff --git a/scripts/gyb.py b/scripts/gyb.py new file mode 100644 index 0000000..2df9a03 --- /dev/null +++ b/scripts/gyb.py @@ -0,0 +1,1260 @@ +#!/usr/bin/env python +# GYB: Generate Your Boilerplate (improved names welcome; at least +# this one's short). See -h output for instructions + +from __future__ import print_function + +import os +import re +import sys +import textwrap +import tokenize +from bisect import bisect + + +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO + + +try: + basestring +except NameError: + basestring = str + + +def get_line_starts(s): + """Return a list containing the start index of each line in s. + + The list also contains a sentinel index for the end of the string, + so there will be one more element in the list than there are lines + in the string + """ + starts = [0] + + for line in s.split('\n'): + starts.append(starts[-1] + len(line) + 1) + + starts[-1] -= 1 + return starts + + +def strip_trailing_nl(s): + """If s ends with a newline, drop it; else return s intact""" + return s[:-1] if s.endswith('\n') else s + + +def split_lines(s): + """Split s into a list of lines, each of which has a trailing newline + + If the lines are later concatenated, the result is s, possibly + with a single appended newline. + """ + return [l + '\n' for l in s.split('\n')] + + +# text on a line up to the first '$$', '${', or '%%' +literalText = r'(?: [^$\n%] | \$(?![${]) | %(?!%) )*' + +# The part of an '%end' line that follows the '%' sign +linesClose = r'[\ \t]* end [\ \t]* (?: \# .* )? $' + +# Note: Where "# Absorb" appears below, the regexp attempts to eat up +# through the end of ${...} and %{...}% constructs. In reality we +# handle this with the Python tokenizer, which avoids mis-detections +# due to nesting, comments and strings. This extra absorption in the +# regexp facilitates testing the regexp on its own, by preventing the +# interior of some of these constructs from being treated as literal +# text. +tokenize_re = re.compile( + r''' +# %-lines and %{...}-blocks + # \n? # absorb one preceding newline + ^ + (?: + (?P + (?P<_indent> [\ \t]* % (?! [{%] ) [\ \t]* ) (?! [\ \t] | ''' + + linesClose + r''' ) .* + ( \n (?P=_indent) (?! ''' + linesClose + r''' ) .* ) * + ) + | (?P [\ \t]* % [ \t]* ''' + linesClose + r''' ) + | [\ \t]* (?P %\{ ) + (?: [^}]| \} (?!%) )* \}% # Absorb + ) + \n? # absorb one trailing newline + +# Substitutions +| (?P \$\{ ) + [^}]* \} # Absorb + +# %% and $$ are literal % and $ respectively +| (?P[$%]) (?P=symbol) + +# Literal text +| (?P ''' + literalText + r''' + (?: + # newline that doesn't precede space+% + (?: \n (?! [\ \t]* %[^%] ) ) + ''' + literalText + r''' + )* + \n? + ) +''', re.VERBOSE | re.MULTILINE) + +gyb_block_close = re.compile(r'\}%[ \t]*\n?') + + +def token_pos_to_index(token_pos, start, line_starts): + """Translate a tokenize (line, column) pair into an absolute + position in source text given the position where we started + tokenizing and a list that maps lines onto their starting + character indexes. + """ + relative_token_line_plus1, token_col = token_pos + + # line number where we started tokenizing + start_line_num = bisect(line_starts, start) - 1 + + # line number of the token in the whole text + abs_token_line = relative_token_line_plus1 - 1 + start_line_num + + # if found in the first line, adjust the end column to account + # for the extra text + if relative_token_line_plus1 == 1: + token_col += start - line_starts[start_line_num] + + # Sometimes tokenizer errors report a line beyond the last one + if abs_token_line >= len(line_starts): + return line_starts[-1] + + return line_starts[abs_token_line] + token_col + + +def tokenize_python_to_unmatched_close_curly(source_text, start, line_starts): + """Apply Python's tokenize to source_text starting at index start + while matching open and close curly braces. When an unmatched + close curly brace is found, return its index. If not found, + return len(source_text). If there's a tokenization error, return + the position of the error. + """ + stream = StringIO(source_text) + stream.seek(start) + nesting = 0 + + try: + for kind, text, token_start, token_end, line_text \ + in tokenize.generate_tokens(stream.readline): + + if text == '{': + nesting += 1 + elif text == '}': + nesting -= 1 + if nesting < 0: + return token_pos_to_index(token_start, start, line_starts) + + except tokenize.TokenError as error: + (message, error_pos) = error.args + return token_pos_to_index(error_pos, start, line_starts) + + return len(source_text) + + +def tokenize_template(template_text): + r"""Given the text of a template, returns an iterator over + (tokenType, token, match) tuples. + + **Note**: this is template syntax tokenization, not Python + tokenization. + + When a non-literal token is matched, a client may call + iter.send(pos) on the iterator to reset the position in + template_text at which scanning will resume. + + This function provides a base level of tokenization which is + then refined by ParseContext.token_generator. + + >>> from pprint import * + >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( + ... '%for x in range(10):\n% print x\n%end\njuicebox'))) + [('gybLines', '%for x in range(10):\n% print x'), + ('gybLinesClose', '%end'), + ('literal', 'juicebox')] + + >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... '''))) + [('literal', 'Nothing\n'), + ('gybLines', '% if x:\n% for i in range(3):'), + ('substitutionOpen', '${'), + ('literal', '\n'), + ('gybLinesClose', '% end'), + ('gybLines', '% else:'), + ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n')] + + >>> for kind, text, _ in tokenize_template(''' + ... This is $some$ literal stuff containing a ${substitution} + ... followed by a %{...} block: + ... %{ + ... # Python code + ... }% + ... and here $${are} some %-lines: + ... % x = 1 + ... % y = 2 + ... % if z == 3: + ... % print '${hello}' + ... % end + ... % for x in zz: + ... % print x + ... % # different indentation + ... % twice + ... and some lines that literally start with a %% token + ... %% first line + ... %% second line + ... '''): + ... print((kind, text.strip().split('\n',1)[0])) + ('literal', 'This is $some$ literal stuff containing a') + ('substitutionOpen', '${') + ('literal', 'followed by a %{...} block:') + ('gybBlockOpen', '%{') + ('literal', 'and here ${are} some %-lines:') + ('gybLines', '% x = 1') + ('gybLinesClose', '% end') + ('gybLines', '% for x in zz:') + ('gybLines', '% # different indentation') + ('gybLines', '% twice') + ('literal', 'and some lines that literally start with a % token') + """ + pos = 0 + end = len(template_text) + + saved_literal = [] + literal_first_match = None + + while pos < end: + m = tokenize_re.match(template_text, pos, end) + + # pull out the one matched key (ignoring internal patterns starting + # with _) + ((kind, text), ) = ( + (kind, text) for (kind, text) in m.groupdict().items() + if text is not None and kind[0] != '_') + + if kind in ('literal', 'symbol'): + if len(saved_literal) == 0: + literal_first_match = m + # literals and symbols get batched together + saved_literal.append(text) + pos = None + else: + # found a non-literal. First yield any literal we've accumulated + if saved_literal != []: + yield 'literal', ''.join(saved_literal), literal_first_match + saved_literal = [] + + # Then yield the thing we found. If we get a reply, it's + # the place to resume tokenizing + pos = yield kind, text, m + + # If we were not sent a new position by our client, resume + # tokenizing at the end of this match. + if pos is None: + pos = m.end(0) + else: + # Client is not yet ready to process next token + yield + + if saved_literal != []: + yield 'literal', ''.join(saved_literal), literal_first_match + + +def split_gyb_lines(source_lines): + r"""Return a list of lines at which to split the incoming source + + These positions represent the beginnings of python line groups that + will require a matching %end construct if they are to be closed. + + >>> src = split_lines('''\ + ... if x: + ... print x + ... if y: # trailing comment + ... print z + ... if z: # another comment\ + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 2 + >>> src[s[0]] + ' print z\n' + >>> s[1] - len(src) + 0 + + >>> src = split_lines('''\ + ... if x: + ... if y: print 1 + ... if z: + ... print 2 + ... pass\ + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 1 + >>> src[s[0]] + ' if y: print 1\n' + + >>> src = split_lines('''\ + ... if x: + ... if y: + ... print 1 + ... print 2 + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 2 + >>> src[s[0]] + ' if y:\n' + >>> src[s[1]] + ' print 1\n' + """ + last_token_text, last_token_kind = None, None + unmatched_indents = [] + + dedents = 0 + try: + for token_kind, token_text, token_start, \ + (token_end_line, token_end_col), line_text \ + in tokenize.generate_tokens(lambda i=iter(source_lines): + next(i)): + + if token_kind in (tokenize.COMMENT, tokenize.ENDMARKER): + continue + + if token_text == '\n' and last_token_text == ':': + unmatched_indents.append(token_end_line) + + # The tokenizer appends dedents at EOF; don't consider + # those as matching indentations. Instead just save them + # up... + if last_token_kind == tokenize.DEDENT: + dedents += 1 + # And count them later, when we see something real. + if token_kind != tokenize.DEDENT and dedents > 0: + unmatched_indents = unmatched_indents[:-dedents] + dedents = 0 + + last_token_text, last_token_kind = token_text, token_kind + + except tokenize.TokenError: + # Let the later compile() call report the error + return [] + + if last_token_text == ':': + unmatched_indents.append(len(source_lines)) + + return unmatched_indents + + +def code_starts_with_dedent_keyword(source_lines): + r"""Return True iff the incoming Python source_lines begin with "else", + "elif", "except", or "finally". + + Initial comments and whitespace are ignored. + + >>> code_starts_with_dedent_keyword(split_lines('if x in y: pass')) + False + >>> code_starts_with_dedent_keyword(split_lines('except ifSomethingElse:')) + True + >>> code_starts_with_dedent_keyword( + ... split_lines('\n# comment\nelse: # yes')) + True + """ + token_text = None + for token_kind, token_text, _, _, _ \ + in tokenize.generate_tokens(lambda i=iter(source_lines): next(i)): + + if token_kind != tokenize.COMMENT and token_text.strip() != '': + break + + return token_text in ('else', 'elif', 'except', 'finally') + + +class ParseContext(object): + + """State carried through a parse of a template""" + + filename = '' + template = '' + line_starts = [] + code_start_line = -1 + code_text = None + tokens = None # The rest of the tokens + close_lines = False + + def __init__(self, filename, template=None): + self.filename = os.path.abspath(filename) + if sys.platform == 'win32': + self.filename = self.filename.replace('\\', '/') + if template is None: + with open(filename) as f: + self.template = f.read() + else: + self.template = template + self.line_starts = get_line_starts(self.template) + self.tokens = self.token_generator(tokenize_template(self.template)) + self.next_token() + + def pos_to_line(self, pos): + return bisect(self.line_starts, pos) - 1 + + def token_generator(self, base_tokens): + r"""Given an iterator over (kind, text, match) triples (see + tokenize_template above), return a refined iterator over + token_kinds. + + Among other adjustments to the elements found by base_tokens, + this refined iterator tokenizes python code embedded in + template text to help determine its true extent. The + expression "base_tokens.send(pos)" is used to reset the index at + which base_tokens resumes scanning the underlying text. + + >>> ctx = ParseContext('dummy', ''' + ... %for x in y: + ... % print x + ... % end + ... literally + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('literal', '\n') + ('gybLinesOpen', 'for x in y:\n') + ('gybLines', ' print x\n') + ('gybLinesClose', '% end') + ('literal', 'literally\n') + + >>> ctx = ParseContext('dummy', + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('literal', 'Nothing\n') + ('gybLinesOpen', 'if x:\n') + ('gybLinesOpen', ' for i in range(3):\n') + ('substitutionOpen', 'i') + ('literal', '\n') + ('gybLinesClose', '% end') + ('gybLinesOpen', 'else:\n') + ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n') + + >>> ctx = ParseContext('dummy', + ... '''% for x in [1, 2, 3]: + ... % if x == 1: + ... literal1 + ... % elif x > 1: # add output line here to fix bug + ... % if x == 2: + ... literal2 + ... % end + ... % end + ... % end + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('gybLinesOpen', 'for x in [1, 2, 3]:\n') + ('gybLinesOpen', ' if x == 1:\n') + ('literal', 'literal1\n') + ('gybLinesOpen', 'elif x > 1: # add output line here to fix bug\n') + ('gybLinesOpen', ' if x == 2:\n') + ('literal', 'literal2\n') + ('gybLinesClose', '% end') + ('gybLinesClose', '% end') + ('gybLinesClose', '% end') + """ + for self.token_kind, self.token_text, self.token_match in base_tokens: + kind = self.token_kind + self.code_text = None + + # Do we need to close the current lines? + self.close_lines = kind == 'gybLinesClose' + + # %{...}% and ${...} constructs + if kind.endswith('Open'): + + # Tokenize text that follows as Python up to an unmatched '}' + code_start = self.token_match.end(kind) + self.code_start_line = self.pos_to_line(code_start) + + close_pos = tokenize_python_to_unmatched_close_curly( + self.template, code_start, self.line_starts) + self.code_text = self.template[code_start:close_pos] + yield kind + + if (kind == 'gybBlockOpen'): + # Absorb any '}% \n' + m2 = gyb_block_close.match(self.template, close_pos) + if not m2: + raise ValueError("Invalid block closure") + next_pos = m2.end(0) + else: + assert kind == 'substitutionOpen' + # skip past the closing '}' + next_pos = close_pos + 1 + + # Resume tokenizing after the end of the code. + base_tokens.send(next_pos) + + elif kind == 'gybLines': + + self.code_start_line = self.pos_to_line( + self.token_match.start('gybLines')) + indentation = self.token_match.group('_indent') + + # Strip off the leading indentation and %-sign + source_lines = re.split( + '^' + re.escape(indentation), + self.token_match.group('gybLines') + '\n', + flags=re.MULTILINE)[1:] + + if code_starts_with_dedent_keyword(source_lines): + self.close_lines = True + + last_split = 0 + for line in split_gyb_lines(source_lines): + self.token_kind = 'gybLinesOpen' + self.code_text = ''.join(source_lines[last_split:line]) + yield self.token_kind + last_split = line + self.code_start_line += line - last_split + self.close_lines = False + + self.code_text = ''.join(source_lines[last_split:]) + if self.code_text: + self.token_kind = 'gybLines' + yield self.token_kind + else: + yield self.token_kind + + def next_token(self): + """Move to the next token""" + for kind in self.tokens: + return self.token_kind + + self.token_kind = None + + +_default_line_directive = \ + '// ###sourceLocation(file: "%(file)s", line: %(line)d)' + + +class ExecutionContext(object): + + """State we pass around during execution of a template""" + + def __init__(self, line_directive=_default_line_directive, + **local_bindings): + self.local_bindings = local_bindings + self.line_directive = line_directive + self.local_bindings['__context__'] = self + self.result_text = [] + self.last_file_line = None + + def append_text(self, text, file, line): + # see if we need to inject a line marker + if self.line_directive: + if (file, line) != self.last_file_line: + # We can only insert the line directive at a line break + if len(self.result_text) == 0 \ + or self.result_text[-1].endswith('\n'): + substitutions = {'file': file, 'line': line + 1} + format_str = self.line_directive + '\n' + self.result_text.append(format_str % substitutions) + # But if the new text contains any line breaks, we can create + # one + elif '\n' in text: + i = text.find('\n') + self.result_text.append(text[:i + 1]) + # and try again + self.append_text(text[i + 1:], file, line + 1) + return + + self.result_text.append(text) + self.last_file_line = (file, line + text.count('\n')) + + +class ASTNode(object): + + """Abstract base class for template AST nodes""" + + def __init__(self): + raise NotImplementedError("ASTNode.__init__ is not implemented.") + + def execute(self, context): + raise NotImplementedError("ASTNode.execute is not implemented.") + + def __str__(self, indent=''): + raise NotImplementedError("ASTNode.__str__ is not implemented.") + + def format_children(self, indent): + if not self.children: + return ' []' + + return '\n'.join( + ['', indent + '['] + + [x.__str__(indent + 4 * ' ') for x in self.children] + + [indent + ']']) + + +class Block(ASTNode): + + """A sequence of other AST nodes, to be executed in order""" + + children = [] + + def __init__(self, context): + self.children = [] + + while context.token_kind and not context.close_lines: + if context.token_kind == 'literal': + node = Literal + else: + node = Code + self.children.append(node(context)) + + def execute(self, context): + for x in self.children: + x.execute(context) + + def __str__(self, indent=''): + return indent + 'Block:' + self.format_children(indent) + + +class Literal(ASTNode): + + """An AST node that generates literal text""" + + def __init__(self, context): + self.text = context.token_text + start_position = context.token_match.start(context.token_kind) + self.start_line_number = context.pos_to_line(start_position) + self.filename = context.filename + context.next_token() + + def execute(self, context): + context.append_text(self.text, self.filename, self.start_line_number) + + def __str__(self, indent=''): + return '\n'.join( + [indent + x for x in ['Literal:'] + + strip_trailing_nl(self.text).split('\n')]) + + +class Code(ASTNode): + + """An AST node that is evaluated as Python""" + + code = None + children = () + kind = None + + def __init__(self, context): + + source = '' + source_line_count = 0 + + def accumulate_code(): + s = source + (context.code_start_line - source_line_count) * '\n' \ + + textwrap.dedent(context.code_text) + line_count = context.code_start_line + \ + context.code_text.count('\n') + context.next_token() + return s, line_count + + eval_exec = 'exec' + if context.token_kind.startswith('substitution'): + eval_exec = 'eval' + source, source_line_count = accumulate_code() + source = '(' + source.strip() + ')' + + else: + while context.token_kind == 'gybLinesOpen': + source, source_line_count = accumulate_code() + source += ' __children__[%d].execute(__context__)\n' % len( + self.children) + source_line_count += 1 + + self.children += (Block(context),) + + if context.token_kind == 'gybLinesClose': + context.next_token() + + if context.token_kind == 'gybLines': + source, source_line_count = accumulate_code() + + # Only handle a substitution as part of this code block if + # we don't already have some %-lines. + elif context.token_kind == 'gybBlockOpen': + + # Opening ${...} and %{...}% constructs + source, source_line_count = accumulate_code() + + self.filename = context.filename + self.start_line_number = context.code_start_line + self.code = compile(source, context.filename, eval_exec) + self.source = source + + def execute(self, context): + # Save __children__ from the local bindings + save_children = context.local_bindings.get('__children__') + # Execute the code with our __children__ in scope + context.local_bindings['__children__'] = self.children + context.local_bindings['__file__'] = self.filename + result = eval(self.code, context.local_bindings) + + if context.local_bindings['__children__'] is not self.children: + raise ValueError("The code is not allowed to mutate __children__") + # Restore the bindings + context.local_bindings['__children__'] = save_children + + # If we got a result, the code was an expression, so append + # its value + if result is not None \ + or (isinstance(result, basestring) and result != ''): + from numbers import Number, Integral + result_string = None + if isinstance(result, Number) and not isinstance(result, Integral): + result_string = repr(result) + else: + result_string = str(result) + context.append_text( + result_string, self.filename, self.start_line_number) + + def __str__(self, indent=''): + source_lines = re.sub(r'^\n', '', strip_trailing_nl( + self.source), flags=re.MULTILINE).split('\n') + if len(source_lines) == 1: + s = indent + 'Code: {' + source_lines[0] + '}' + else: + s = indent + 'Code:\n' + indent + '{\n' + '\n'.join( + indent + 4 * ' ' + l for l in source_lines + ) + '\n' + indent + '}' + return s + self.format_children(indent) + + +def expand(filename, line_directive=_default_line_directive, **local_bindings): + r"""Return the contents of the given template file, executed with the given + local bindings. + + >>> from tempfile import NamedTemporaryFile + >>> # On Windows, the name of a NamedTemporaryFile cannot be used to open + >>> # the file for a second time if delete=True. Therefore, we have to + >>> # manually handle closing and deleting this file to allow us to open + >>> # the file by its name across all platforms. + >>> f = NamedTemporaryFile(delete=False) + >>> f.write( + ... r'''--- + ... % for i in range(int(x)): + ... a pox on ${i} for epoxy + ... % end + ... ${120 + + ... + ... 3} + ... abc + ... ${"w\nx\nX\ny"} + ... z + ... ''') + >>> f.flush() + >>> result = expand( + ... f.name, + ... line_directive='//#sourceLocation(file: "%(file)s", ' + \ + ... 'line: %(line)d)', + ... x=2 + ... ).replace( + ... '"%s"' % f.name.replace('\\', '/'), '"dummy.file"') + >>> print(result, end='') + //#sourceLocation(file: "dummy.file", line: 1) + --- + //#sourceLocation(file: "dummy.file", line: 3) + a pox on 0 for epoxy + //#sourceLocation(file: "dummy.file", line: 3) + a pox on 1 for epoxy + //#sourceLocation(file: "dummy.file", line: 5) + 123 + //#sourceLocation(file: "dummy.file", line: 8) + abc + w + x + X + y + //#sourceLocation(file: "dummy.file", line: 10) + z + >>> f.close() + >>> os.remove(f.name) + """ + with open(filename) as f: + t = parse_template(filename, f.read()) + d = os.getcwd() + os.chdir(os.path.dirname(os.path.abspath(filename))) + try: + return execute_template( + t, line_directive=line_directive, **local_bindings) + finally: + os.chdir(d) + + +def parse_template(filename, text=None): + r"""Return an AST corresponding to the given template file. + + If text is supplied, it is assumed to be the contents of the file, + as a string. + + >>> print(parse_template('dummy.file', text= + ... '''% for x in [1, 2, 3]: + ... % if x == 1: + ... literal1 + ... % elif x > 1: # add output line after this line to fix bug + ... % if x == 2: + ... literal2 + ... % end + ... % end + ... % end + ... ''')) + Block: + [ + Code: + { + for x in [1, 2, 3]: + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: + { + if x == 1: + __children__[0].execute(__context__) + elif x > 1: # add output line after this line to fix bug + __children__[1].execute(__context__) + } + [ + Block: + [ + Literal: + literal1 + ] + Block: + [ + Code: + { + if x == 2: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + literal2 + ] + ] + ] + ] + ] + ] + ] + + >>> print(parse_template( + ... 'dummy.file', + ... text='%for x in range(10):\n% print(x)\n%end\njuicebox')) + Block: + [ + Code: + { + for x in range(10): + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {print(x)} [] + ] + ] + Literal: + juicebox + ] + + >>> print(parse_template('/dummy.file', text= + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''')) + Block: + [ + Literal: + Nothing + Code: + { + if x: + __children__[0].execute(__context__) + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: + { + for i in range(3): + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {(i)} [] + Literal: + + ] + ] + ] + Block: + [ + Literal: + THIS SHOULD NOT APPEAR IN THE OUTPUT + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %for x in y: + ... % print(y) + ... ''')) + Block: + [ + Code: + { + for x in y: + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %if x: + ... % print(y) + ... AAAA + ... %else: + ... BBBB + ... ''')) + Block: + [ + Code: + { + if x: + __children__[0].execute(__context__) + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + Literal: + AAAA + ] + Block: + [ + Literal: + BBBB + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %if x: + ... % print(y) + ... AAAA + ... %# This is a comment + ... %else: + ... BBBB + ... ''')) + Block: + [ + Code: + { + if x: + __children__[0].execute(__context__) + # This is a comment + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + Literal: + AAAA + ] + Block: + [ + Literal: + BBBB + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''\ + ... %for x in y: + ... AAAA + ... %if x: + ... BBBB + ... %end + ... CCCC + ... ''')) + Block: + [ + Code: + { + for x in y: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + AAAA + Code: + { + if x: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + BBBB + ] + ] + Literal: + CCCC + ] + ] + ] + """ + return Block(ParseContext(filename, text)) + + +def execute_template( + ast, line_directive=_default_line_directive, **local_bindings): + r"""Return the text generated by executing the given template AST. + + Keyword arguments become local variable bindings in the execution context + + >>> root_directory = os.path.abspath('/') + >>> file_name = (root_directory + 'dummy.file').replace('\\', '/') + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''') + >>> out = execute_template(ast, + ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', + ... x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + //#sourceLocation(file: "DUMMY-FILE", line: 1) + Nothing + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 0 + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 1 + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 2 + + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % a = [] + ... % for x in range(3): + ... % a.append(x) + ... % end + ... ${a} + ... ''') + >>> out = execute_template(ast, + ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', + ... x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + //#sourceLocation(file: "DUMMY-FILE", line: 1) + Nothing + //#sourceLocation(file: "DUMMY-FILE", line: 6) + [0, 1, 2] + + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % a = [] + ... % for x in range(3): + ... % a.append(x) + ... % end + ... ${a} + ... ''') + >>> out = execute_template(ast, + ... line_directive='#line %(line)d "%(file)s"', x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + #line 1 "DUMMY-FILE" + Nothing + #line 6 "DUMMY-FILE" + [0, 1, 2] + """ + execution_context = ExecutionContext( + line_directive=line_directive, **local_bindings) + ast.execute(execution_context) + return ''.join(execution_context.result_text) + + +def main(): + import argparse + import sys + + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description='Generate Your Boilerplate!', epilog=''' + A GYB template consists of the following elements: + + - Literal text which is inserted directly into the output + + - %% or $$ in literal text, which insert literal '%' and '$' + symbols respectively. + + - Substitutions of the form ${}. The Python + expression is converted to a string and the result is inserted + into the output. + + - Python code delimited by %{...}%. Typically used to inject + definitions (functions, classes, variable bindings) into the + evaluation context of the template. Common indentation is + stripped, so you can add as much indentation to the beginning + of this code as you like + + - Lines beginning with optional whitespace followed by a single + '%' and Python code. %-lines allow you to nest other + constructs inside them. To close a level of nesting, use the + "%end" construct. + + - Lines beginning with optional whitespace and followed by a + single '%' and the token "end", which close open constructs in + %-lines. + + Example template: + + - Hello - + %{ + x = 42 + def succ(a): + return a+1 + }% + + I can assure you that ${x} < ${succ(x)} + + % if int(y) > 7: + % for i in range(3): + y is greater than seven! + % end + % else: + y is less than or equal to seven + % end + + - The End. - + + When run with "gyb -Dy=9", the output is + + - Hello - + + I can assure you that 42 < 43 + + y is greater than seven! + y is greater than seven! + y is greater than seven! + + - The End. - +''' + ) + parser.add_argument( + '-D', action='append', dest='defines', metavar='NAME=VALUE', + default=[], + help='''Bindings to be set in the template's execution context''') + + parser.add_argument( + 'file', type=argparse.FileType(), + help='Path to GYB template file (defaults to stdin)', nargs='?', + default=sys.stdin) + parser.add_argument( + '-o', dest='target', type=argparse.FileType('w'), + help='Output file (defaults to stdout)', default=sys.stdout) + parser.add_argument( + '--test', action='store_true', + default=False, help='Run a self-test') + parser.add_argument( + '--verbose-test', action='store_true', + default=False, help='Run a verbose self-test') + parser.add_argument( + '--dump', action='store_true', + default=False, help='Dump the parsed template to stdout') + parser.add_argument( + '--line-directive', + default=_default_line_directive, + help=''' + Line directive format string, which will be + provided 2 substitutions, `%%(line)d` and `%%(file)s`. + + Example: `#sourceLocation(file: "%%(file)s", line: %%(line)d)` + + The default works automatically with the `line-directive` tool, + which see for more information. + ''') + + args = parser.parse_args(sys.argv[1:]) + + if args.test or args.verbose_test: + import doctest + selfmod = sys.modules[__name__] + if doctest.testmod(selfmod, verbose=args.verbose_test or None).failed: + sys.exit(1) + + bindings = dict(x.split('=', 1) for x in args.defines) + ast = parse_template(args.file.name, args.file.read()) + if args.dump: + print(ast) + # Allow the template to open files and import .py files relative to its own + # directory + os.chdir(os.path.dirname(os.path.abspath(args.file.name))) + sys.path = ['.'] + sys.path + + args.target.write(execute_template(ast, args.line_directive, **bindings)) + + +if __name__ == '__main__': + main() diff --git a/scripts/gyb.pyc b/scripts/gyb.pyc new file mode 100644 index 0000000000000000000000000000000000000000..193bda937078df25470b1fd79920ae9147a4fd09 GIT binary patch literal 36305 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