diff --git a/Sources/SwiftASN1/Basic ASN1 Types/ObjectIdentifier.swift b/Sources/SwiftASN1/Basic ASN1 Types/ObjectIdentifier.swift index 7d8a9ba..c431376 100644 --- a/Sources/SwiftASN1/Basic ASN1 Types/ObjectIdentifier.swift +++ b/Sources/SwiftASN1/Basic ASN1 Types/ObjectIdentifier.swift @@ -63,8 +63,9 @@ public struct ASN1ObjectIdentifier: DERImplicitlyTaggable, BERImplicitlyTaggable } } + /// An array representing the OID components @inlinable - var oidComponents: [UInt] { + public var oidComponents: [UInt] { var content = bytes // We have to parse the content. From the spec: // @@ -143,25 +144,72 @@ extension ASN1ObjectIdentifier: Hashable {} extension ASN1ObjectIdentifier: Sendable {} -extension ASN1ObjectIdentifier: ExpressibleByArrayLiteral { +extension ASN1ObjectIdentifier { + /// Initializes ``ASN1ObjectIdentifier`` from its OID components + /// - Parameter elements: The OID components @inlinable - public init(arrayLiteral elements: UInt...) { + public init(elements: some Collection) throws { var bytes = [UInt8]() - var components = elements[...] - guard let firstComponent = components.popFirst(), let secondComponent = components.popFirst() else { - preconditionFailure("Invalid number of OID components: must be at least two!") + var iterator = elements.makeIterator() + + guard let firstComponent = iterator.next(), let secondComponent = iterator.next() else { + throw ASN1Error.tooFewOIDComponents( + reason: "Invalid number of OID components: must be at least two!" + ) } let serializedFirstComponent = (firstComponent * 40) + secondComponent ASN1ObjectIdentifier._writeOIDSubidentifier(serializedFirstComponent, into: &bytes) - while let component = components.popFirst() { + while let component = iterator.next() { ASN1ObjectIdentifier._writeOIDSubidentifier(component, into: &bytes) } self.bytes = bytes[...] } } +extension ASN1ObjectIdentifier: ExpressibleByStringLiteral { + @inlinable + public init(stringLiteral dotRepresentation: String) { + // To allow for invalid strings to be tested, parsing is performed in a separate initializer that `throws` + // (this initializer conforms to ExpressibleByStringLiteral so cannot throw) + try! self.init(dotRepresentation: dotRepresentation) + } + + /// Initializes an instance from a `Substring` containing the dot represented OID + /// - Parameter dotRepresentation: The dot represented OID + @inlinable + public init(dotRepresentation: Substring) throws { + let octetArray = dotRepresentation.utf8.split( + separator: UInt8(ascii: "."), + omittingEmptySubsequences: false + ) + + try self.init( + elements: octetArray.lazy.map { octet in + guard let uintOctet = UInt(Substring(octet)) else { + throw ASN1Error.invalidStringRepresentation(reason: "Invalid octet in OID") + } + return uintOctet + } + ) + } + + /// Initializes an instance from a `String` containing the dot represented OID + /// - Parameter dotRepresentation: The dot represented OID + @inlinable + public init(dotRepresentation: String) throws { + try self.init(dotRepresentation: Substring(dotRepresentation)) + } +} + +extension ASN1ObjectIdentifier: ExpressibleByArrayLiteral { + @inlinable + public init(arrayLiteral elements: UInt...) { + try! self.init(elements: elements) + } +} + extension ASN1ObjectIdentifier: CustomStringConvertible { @inlinable public var description: String { diff --git a/Sources/SwiftASN1/Errors.swift b/Sources/SwiftASN1/Errors.swift index 4d5b759..6579983 100644 --- a/Sources/SwiftASN1/Errors.swift +++ b/Sources/SwiftASN1/Errors.swift @@ -164,6 +164,23 @@ public struct ASN1Error: Error, Hashable, CustomStringConvertible { ) ) } + + /// Too few OID components were provided. There must be at least two or more. + @inline(never) + public static func tooFewOIDComponents( + reason: String, + file: String = #fileID, + line: UInt = #line + ) -> ASN1Error { + return ASN1Error( + backing: .init( + code: .tooFewOIDComponents, + reason: reason, + file: file, + line: line + ) + ) + } } extension ASN1Error { @@ -181,6 +198,7 @@ extension ASN1Error { case unsupportedFieldLength case invalidPEMDocument case invalidStringRepresentation + case tooFewOIDComponents } fileprivate var backingCode: BackingCode @@ -211,6 +229,9 @@ extension ASN1Error { /// A string was invalid. public static let invalidStringRepresentation = ErrorCode(.invalidStringRepresentation) + /// Too few OID components were provided. There must be at least two or more. + public static let tooFewOIDComponents = ErrorCode(.tooFewOIDComponents) + public var description: String { return String(describing: self.backingCode) } diff --git a/Tests/SwiftASN1Tests/ASN1Tests.swift b/Tests/SwiftASN1Tests/ASN1Tests.swift index 747247e..155defd 100644 --- a/Tests/SwiftASN1Tests/ASN1Tests.swift +++ b/Tests/SwiftASN1Tests/ASN1Tests.swift @@ -1019,6 +1019,46 @@ class ASN1Tests: XCTestCase { XCTAssertEqual(s, "ASN1Any([5, 0])") } + func testOIDArrayInitializer() { + let oidArray = try! ASN1ObjectIdentifier(elements: [1, 2, 865, 11241, 3]) + XCTAssertEqual(oidArray.oidComponents, [1, 2, 865, 11241, 3]) + + let anotherOidArray = try! ASN1ObjectIdentifier(elements: [1, 2, 865]) + XCTAssertEqual(anotherOidArray.oidComponents, [1, 2, 865]) + } + + func testOIDArrayInitializerInvalid() { + XCTAssertThrowsError(try ASN1ObjectIdentifier(elements: [1])) { error in + XCTAssertEqual((error as? ASN1Error)?.code, .tooFewOIDComponents) + } + + XCTAssertThrowsError(try ASN1ObjectIdentifier(elements: [])) { error in + XCTAssertEqual((error as? ASN1Error)?.code, .tooFewOIDComponents) + } + } + + func testOIDStringInitializer() { + let oidFromString: ASN1ObjectIdentifier = "1.2.865.11241.3" + let oidFromArrayLiteral: ASN1ObjectIdentifier = [1, 2, 865, 11241, 3] + + XCTAssertEqual(oidFromString, oidFromArrayLiteral) + XCTAssertEqual(oidFromString.oidComponents, [1, 2, 865, 11241, 3]) + } + + func testOIDStringInitializerInvalid() { + XCTAssertThrowsError(try ASN1ObjectIdentifier(dotRepresentation: "1..2.865.11241.3")) { error in + XCTAssertEqual((error as? ASN1Error)?.code, .invalidStringRepresentation) + } + + XCTAssertThrowsError(try ASN1ObjectIdentifier(dotRepresentation: "1.2..11241.3")) { error in + XCTAssertEqual((error as? ASN1Error)?.code, .invalidStringRepresentation) + } + + XCTAssertThrowsError(try ASN1ObjectIdentifier(dotRepresentation: "25")) { error in + XCTAssertEqual((error as? ASN1Error)?.code, .tooFewOIDComponents) + } + } + func testSetOfSingleElement() throws { var serializer = DER.Serializer() try serializer.serializeSetOf([