├── .github ├── FUNDING.yml └── workflows │ └── ci.yaml ├── .swiftformat ├── .spi.yml ├── Examples └── PrintPDF │ ├── hi_mom.pdf │ └── app.swift ├── Sources ├── IppClient │ ├── Exports.swift │ ├── IppJob.swift │ ├── IppPrinter.swift │ └── HttpClient+Ipp.swift └── IppProtocol │ ├── RequestResponse+ByteBuffer.swift │ ├── IppAttributable.swift │ ├── Encoding │ ├── Internals.swift │ ├── ByteBuffer+writing.swift │ └── ByteBuffer+reading.swift │ ├── IppObjects.swift │ ├── Values.swift │ ├── RequestResponse.swift │ └── SemanticModel.swift ├── .gitignore ├── Package.swift ├── Tests └── IppTests │ ├── AttributesTests.swift │ └── EncodingTests.swift ├── Package.resolved ├── README.md └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [sliemeobn] 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --stripunusedargs unnamed-only -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [IppClient, IppProtocol] -------------------------------------------------------------------------------- /Examples/PrintPDF/hi_mom.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliemeobn/ipp-nio/HEAD/Examples/PrintPDF/hi_mom.pdf -------------------------------------------------------------------------------- /Sources/IppClient/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import IppProtocol 2 | 3 | //NOTE: is this a good idea? 4 | @_exported import class AsyncHTTPClient.HTTPClient 5 | @_exported import struct AsyncHTTPClient.HTTPClientRequest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | .vscode/launch.json -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | ci: 12 | name: Build & Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/cache@v3 18 | with: 19 | path: .build 20 | key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} 21 | restore-keys: | 22 | ${{ runner.os }}-spm- 23 | 24 | - name: Build 25 | run: swift build --build-tests 26 | 27 | - name: Run Tests 28 | run: swift test -------------------------------------------------------------------------------- /Sources/IppClient/IppJob.swift: -------------------------------------------------------------------------------- 1 | import IppProtocol 2 | 3 | public extension IppPrinter { 4 | /// Implements the ``IppJobObject`` using a ``IppPrinter``. 5 | struct Job { 6 | public let jobId: Int32 7 | public let printer: IppPrinter 8 | 9 | public init(printer: IppPrinter, jobId: Int32) { 10 | self.jobId = jobId 11 | self.printer = printer 12 | } 13 | } 14 | 15 | func job(_ jobId: Int32) -> Job { 16 | Job(printer: self, jobId: jobId) 17 | } 18 | } 19 | 20 | extension IppPrinter.Job: IppJobObject { 21 | public typealias DataFormat = IppPrinter.DataFormat 22 | 23 | public func makeNewRequest(operation: IppOperationId) -> IppRequest { 24 | IppRequest( 25 | printerUri: printer.uri, 26 | jobId: jobId, 27 | operation: operation, 28 | requestingUserName: printer.authentication?.requestingUserName, 29 | attributesNaturalLanguage: printer.language, 30 | version: printer.version 31 | ) 32 | } 33 | 34 | public func execute(request: IppRequest, data: DataFormat?) async throws -> IppResponse { 35 | try await printer.execute(request: request, data: data) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ipp-nio", 6 | platforms: [ 7 | .macOS(.v10_15), 8 | .iOS(.v13), 9 | .tvOS(.v13), 10 | .watchOS(.v6), 11 | ], 12 | products: [ 13 | .library(name: "IppProtocol", targets: ["IppProtocol"]), 14 | .library(name: "IppClient", targets: ["IppClient"]), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"), 18 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.6"), 19 | .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), 20 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.0"), 21 | ], 22 | targets: [ 23 | .target( 24 | name: "IppClient", 25 | dependencies: [ 26 | .target(name: "IppProtocol"), 27 | .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), 28 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 29 | ] 30 | ), 31 | .target( 32 | name: "IppProtocol", 33 | dependencies: [ 34 | .product(name: "NIOCore", package: "swift-nio"), 35 | .product(name: "OrderedCollections", package: "swift-collections"), 36 | ] 37 | ), 38 | .testTarget( 39 | name: "IppTests", 40 | dependencies: ["IppProtocol", "IppClient"] 41 | ), 42 | ] 43 | ) 44 | 45 | #if os(macOS) || os(Linux) 46 | package.targets.append( 47 | .executableTarget( 48 | name: "PrintPDF", 49 | dependencies: [ 50 | .target(name: "IppClient"), 51 | ], 52 | path: "Examples/PrintPDF", 53 | exclude: ["hi_mom.pdf"] 54 | ) 55 | ) 56 | #endif -------------------------------------------------------------------------------- /Sources/IppProtocol/RequestResponse+ByteBuffer.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | public extension IppRequest { 4 | /// Reads an IPP request from the given buffer. 5 | /// 6 | /// This method will throw an error if the buffer does not contain a valid IPP request. 7 | /// The reader index of the buffer will be advanced to the end of the IPP request. 8 | init(buffer: inout ByteBuffer) throws { 9 | do { 10 | self = try buffer.readIppCodable() 11 | } catch let error as ParsingError { 12 | throw IppParsingError(readerIndex: buffer.readerIndex, parsingError: error) 13 | } 14 | } 15 | 16 | /// Writes this IPP request to the given buffer. 17 | func write(to buffer: inout ByteBuffer) { 18 | buffer.writeIppCodable(self) 19 | } 20 | } 21 | 22 | public extension IppResponse { 23 | /// Reads an IPP response from the given buffer. 24 | /// 25 | /// This method will throw an error if the buffer does not contain a valid IPP response. 26 | /// The reader index of the buffer will be advanced to the end of the IPP response. 27 | init(buffer: inout ByteBuffer) throws { 28 | do { 29 | self = try buffer.readIppCodable() 30 | } catch let error as ParsingError { 31 | throw IppParsingError(readerIndex: buffer.readerIndex, parsingError: error) 32 | } 33 | } 34 | 35 | /// Writes this IPP response to the given buffer. 36 | func write(to buffer: inout ByteBuffer) { 37 | buffer.writeIppCodable(self) 38 | } 39 | } 40 | 41 | /// An error that occurred while parsing an IPP request or response. 42 | /// 43 | /// This error contains the reader index of the buffer at which the parsing error occurred. 44 | public struct IppParsingError: Error, CustomStringConvertible { 45 | public let readerIndex: Int 46 | internal let parsingError: ParsingError 47 | 48 | public var description: String { 49 | "Parsing error at index \(readerIndex): \(parsingError)" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Examples/PrintPDF/app.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import IppClient 3 | 4 | @main 5 | struct App { 6 | static func main() async throws { 7 | let httpClient = HTTPClient(configuration: .init(certificateVerification: .none)) 8 | 9 | do { 10 | let printer = IppPrinter( 11 | httpClient: httpClient, 12 | uri: "ipps://macmini.not.local/printers/EPSON_XP_7100_Series" 13 | ) 14 | 15 | let attributesResponse = try await printer.getPrinterAttributes() 16 | 17 | if let printerName = attributesResponse[printer: \.printerName], 18 | let printerState = attributesResponse[printer: \.printerState], 19 | let printerStateReasons = attributesResponse[printer: \.printerStateReasons] 20 | { 21 | print("Printing on \(printerName) in state \(printerState), state reasons \(printerStateReasons)") 22 | } else { 23 | print("Could not read printer attributes, status code \(attributesResponse.statusCode)") 24 | } 25 | 26 | let pdf = try Data(contentsOf: URL(fileURLWithPath: "Examples/PrintPDF/hi_mom.pdf")) 27 | 28 | let response = try await printer.printJob( 29 | documentFormat: "application/pdf", 30 | data: .bytes(pdf) 31 | ) 32 | 33 | guard response.statusCode == .successfulOk, let jobId = response[job: \.jobId] else { 34 | print("Print job failed with status \(response.statusCode)") 35 | exit(1) 36 | } 37 | 38 | let job = printer.job(jobId) 39 | 40 | while true { 41 | let response = try await job.getJobAttributes(requestedAttributes: [.jobState]) 42 | guard let jobState = response[job: \.jobState] else { 43 | print("Failed to get job state") 44 | exit(1) 45 | } 46 | 47 | switch jobState { 48 | case .aborted, .canceled, .completed: 49 | print("Job ended with state \(jobState)") 50 | exit(0) 51 | default: 52 | print("Job state is \(jobState)") 53 | } 54 | 55 | try await Task.sleep(nanoseconds: 3_000_000_000) 56 | } 57 | } catch { 58 | print("Error: \(error)") 59 | } 60 | 61 | try await httpClient.shutdown() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/IppTests/AttributesTests.swift: -------------------------------------------------------------------------------- 1 | import IppProtocol 2 | import XCTest 3 | 4 | final class AttributesTests: XCTestCase { 5 | func testInitalizesCorrectly() async throws { 6 | let request = IppRequest(printerUri: "ipp://some", operation: .cancelJob) 7 | XCTAssertEqual(request.version, .v1_1) 8 | XCTAssertEqual(request.operationId, .cancelJob) 9 | XCTAssertEqual(request[operation: \.attributesCharset], "utf-8") 10 | XCTAssertEqual(request[operation: \.attributesNaturalLanguage], "en") 11 | XCTAssertEqual(request[operation: \.printerUri], "ipp://some") 12 | 13 | XCTAssert(request[.operation].keys[0] == .attributesCharset) 14 | XCTAssert(request[.operation].keys[1] == .attributesNaturalLanguage) 15 | XCTAssert(request[.operation].keys[2] == .printerUri) 16 | } 17 | 18 | func testStringSemantics() { 19 | var attributes = IppAttributes() 20 | 21 | attributes[\.operation.attributesCharset] = "A" 22 | attributes[\.operation.attributesNaturalLanguage] = "B" 23 | attributes[\.operation.requestingUserName] = "C" 24 | attributes[\.operation.jobUri] = "D" 25 | 26 | XCTAssertEqual(attributes[.attributesCharset], .init(.charset("A"))) 27 | XCTAssertEqual(attributes[.attributesNaturalLanguage], .init(.naturalLanguage("B"))) 28 | XCTAssertEqual(attributes[.requestingUserName], .init(.name(.withoutLanguage("C")))) 29 | XCTAssertEqual(attributes[.jobUri], .init(.uri("D"))) 30 | XCTAssertEqual(attributes[\.operation.attributesCharset], "A") 31 | XCTAssertEqual(attributes[\.operation.attributesNaturalLanguage], "B") 32 | XCTAssertEqual(attributes[\.operation.requestingUserName], "C") 33 | XCTAssertEqual(attributes[\.operation.jobUri], "D") 34 | } 35 | 36 | func testSetOfKeywordsSemantics() { 37 | var attributes = IppAttributes() 38 | 39 | attributes[\.operation.requestedAttributes] = ["A", "B"] 40 | 41 | XCTAssertEqual(attributes[.requestedAttributes]?.values, [.keyword("A"), .keyword("B")]) 42 | XCTAssertEqual(attributes[\.operation.requestedAttributes], ["A", "B"]) 43 | } 44 | 45 | func testEnumSemantics() { 46 | var attributes = IppAttributes() 47 | 48 | attributes[\.jobTemplate.orientationRequested] = .portrait 49 | 50 | XCTAssertEqual(attributes[.orientationRequested], .init(.enumValue(3))) 51 | XCTAssertEqual(attributes[\.jobTemplate.orientationRequested], .portrait) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/IppClient/IppPrinter.swift: -------------------------------------------------------------------------------- 1 | import AsyncHTTPClient 2 | import IppProtocol 3 | 4 | /// Implements the ``IppPrinterObject`` using a HTTPClient. 5 | public struct IppPrinter: IppPrinterObject, Sendable { 6 | public var httpClient: HTTPClient 7 | public var uri: String 8 | public var authentication: IppAuthentication? 9 | public var language: String 10 | public var version: IppVersion 11 | 12 | public init( 13 | httpClient: HTTPClient, 14 | uri: String, 15 | authentication: IppAuthentication? = nil, 16 | language: String = "en", 17 | version: IppVersion = .v1_1 18 | ) { 19 | self.httpClient = httpClient 20 | self.uri = uri 21 | self.authentication = authentication 22 | self.language = language 23 | self.version = version 24 | } 25 | 26 | public func makeNewRequest(operation: IppOperationId) -> IppRequest { 27 | IppRequest( 28 | printerUri: uri, 29 | operation: operation, 30 | requestingUserName: authentication?.requestingUserName, 31 | attributesNaturalLanguage: language, 32 | version: version 33 | ) 34 | } 35 | 36 | public func execute(request: IppRequest, data: HTTPClientRequest.Body?) async throws -> IppResponse { 37 | try await httpClient.execute(request, authentication: authentication, data: data) 38 | } 39 | } 40 | 41 | public extension HTTPClient { 42 | func ippPrinter(uri: String, language: String = "en", version: IppVersion = .v1_1) -> IppPrinter { 43 | IppPrinter(httpClient: self, uri: uri, language: language, version: version) 44 | } 45 | } 46 | 47 | /// Represents the authentication mode for an IPP request. 48 | public struct IppAuthentication: Sendable { 49 | enum Mode { 50 | case requestingUser(username: String) 51 | case basic(username: String, password: String) 52 | } 53 | 54 | var mode: Mode 55 | 56 | /// Sets the "requesting-user` attribute on every request. 57 | public static func requestingUser(username: String) -> Self { 58 | Self(mode: .requestingUser(username: username)) 59 | } 60 | 61 | /// Uses HTTP basic authentication for every request. 62 | public static func basic(username: String, password: String) -> Self { 63 | Self(mode: .basic(username: username, password: password)) 64 | } 65 | 66 | var requestingUserName: String { 67 | switch mode { 68 | case let .requestingUser(username), let .basic(username, _): 69 | return username 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/IppClient/HttpClient+Ipp.swift: -------------------------------------------------------------------------------- 1 | import AsyncAlgorithms 2 | import AsyncHTTPClient 3 | import IppProtocol 4 | import NIOCore 5 | 6 | public extension HTTPClient { 7 | /// Executes an IPP request and returns the response. 8 | /// - Parameter request: The IPP request to execute. 9 | /// - Parameter data: The data to send with the request. 10 | /// 11 | /// - Returns: The IPP response. 12 | func execute( 13 | _ request: IppRequest, 14 | authentication: IppAuthentication? = nil, 15 | data: consuming HTTPClientRequest.Body? = nil, 16 | timeout: TimeAmount = .seconds(10), 17 | maxResponseBytes: Int = 1024 * 1024 18 | ) async throws -> IppResponse { 19 | let httpRequest = try HTTPClientRequest(ippRequest: request, authentication: authentication, data: data) 20 | let httpResponse = try await execute(httpRequest, timeout: timeout) 21 | 22 | if httpResponse.status != .ok { 23 | throw IppHttpResponseError(response: httpResponse) 24 | } 25 | 26 | var buffer = try await httpResponse.body.collect(upTo: maxResponseBytes) 27 | return try IppResponse(buffer: &buffer) 28 | } 29 | } 30 | 31 | public extension HTTPClientRequest { 32 | /// Creates a HTTP by encoding the IPP request and attaching the data if provided. 33 | init(ippRequest: IppRequest, authentication: IppAuthentication? = nil, data: consuming Body? = nil) throws { 34 | try self.init(url: ippRequest.validatedHttpTargetUrl) 35 | method = .POST 36 | headers.add(name: "content-type", value: "application/ipp") 37 | 38 | // set auth header if needed 39 | if let authenticationMode = authentication?.mode { 40 | switch authenticationMode { 41 | case .requestingUser(username: _): break // nothing to do 42 | case let .basic(username: username, password: password): 43 | let value = HTTPClient.Authorization.basic(username: username, password: password).headerValue 44 | headers.add(name: "authorization", value: value) 45 | } 46 | } 47 | 48 | // maybe pre-size this thing somehow? 49 | var buffer = ByteBuffer() 50 | ippRequest.write(to: &buffer) 51 | 52 | if let data { 53 | // TODO: check out if this is so great - it would be nice know the length 54 | body = .stream(chain([buffer].async, data), length: .unknown) 55 | } else { 56 | body = .bytes(buffer) 57 | } 58 | } 59 | } 60 | 61 | /// Represents the error when an IPP request fails with an HTTP response that is not 200 OK. 62 | public struct IppHttpResponseError: Error, CustomStringConvertible { 63 | public let response: HTTPClientResponse 64 | 65 | public var description: String { 66 | "IPP request failed with response status \(response.status): \(response)" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/IppProtocol/IppAttributable.swift: -------------------------------------------------------------------------------- 1 | /// A type that can be attributed with IPP attributes. 2 | public protocol IppAttributable { 3 | var attributeGroups: IppAttributeGroups { get set } 4 | } 5 | 6 | extension IppRequest: IppAttributable {} 7 | extension IppResponse: IppAttributable {} 8 | 9 | public extension IppAttributable { 10 | /// Accesses the first attribute group with the specified name. If the group does not exist, it is created. 11 | /// 12 | /// This mostly behaves like a dictionary, but the IPP specification allows for multiple groups with the same name. 13 | subscript(_ name: IppAttributeGroup.Name) -> IppAttributes { 14 | _read { 15 | if let index = attributeGroups.firstIndex(where: { $0.name == name }) { 16 | yield attributeGroups[index].attributes 17 | } else { 18 | yield[:] 19 | } 20 | } 21 | _modify { 22 | if let index = attributeGroups.firstIndex(where: { $0.name == name }) { 23 | yield &attributeGroups[index].attributes 24 | } else { 25 | var group = IppAttributeGroup(name: name, attributes: [:]) 26 | yield &group.attributes 27 | attributeGroups.append(group) 28 | } 29 | } 30 | } 31 | } 32 | 33 | public extension IppRequest { 34 | /// Access an attribute in the `operation` group. 35 | subscript(operation attribute: KeyPath>) -> V? { 36 | get { 37 | self[.operation][(\SemanticModel.Attributes.operation).appending(path: attribute)] 38 | } 39 | set { 40 | self[.operation][(\SemanticModel.Attributes.operation).appending(path: attribute)] = newValue 41 | } 42 | } 43 | 44 | /// Access an attribute in the `job` group. 45 | subscript(job attribute: KeyPath>) -> V? { 46 | get { 47 | self[.job][(\SemanticModel.Attributes.jobTemplate).appending(path: attribute)] 48 | } 49 | set { 50 | self[.job][(\SemanticModel.Attributes.jobTemplate).appending(path: attribute)] = newValue 51 | } 52 | } 53 | } 54 | 55 | public extension IppResponse { 56 | /// Access an attribute in the `operation` group. 57 | subscript(operation attribute: KeyPath>) -> V? { 58 | get { 59 | self[.operation][(\SemanticModel.Attributes.operationResponse).appending(path: attribute)] 60 | } 61 | set { 62 | self[.operation][(\SemanticModel.Attributes.operationResponse).appending(path: attribute)] = newValue 63 | } 64 | } 65 | 66 | /// Access an attribute in the `job` group. 67 | subscript(job attribute: KeyPath>) -> V? { 68 | get { 69 | self[.job][(\SemanticModel.Attributes.jobDescription).appending(path: attribute)] 70 | } 71 | set { 72 | self[.job][(\SemanticModel.Attributes.jobDescription).appending(path: attribute)] = newValue 73 | } 74 | } 75 | 76 | subscript(printer attribute: KeyPath>) -> V? { 77 | get { 78 | self[.printer][(\SemanticModel.Attributes.printerDescription).appending(path: attribute)] 79 | } 80 | set { 81 | self[.printer][(\SemanticModel.Attributes.printerDescription).appending(path: attribute)] = newValue 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/IppProtocol/Encoding/Internals.swift: -------------------------------------------------------------------------------- 1 | enum DelimiterTag: UInt8 { 2 | case operationAttributes = 0x01 3 | case jobAttributes = 0x02 4 | case endOfAttributes = 0x03 5 | case printerAttributes = 0x04 6 | case unsupportedAttributes = 0x05 7 | 8 | static var valueRange: ClosedRange { 0x00 ... 0x0F } 9 | } 10 | 11 | enum ValueTag: UInt8 { 12 | case unsupported = 0x10 13 | case unknown = 0x12 14 | case noValue = 0x13 15 | 16 | case integer = 0x21 17 | case boolean = 0x22 18 | case `enum` = 0x23 19 | case octetString = 0x30 20 | case dateTime = 0x31 21 | case resolution = 0x32 22 | case rangeOfInteger = 0x33 23 | case begCollection = 0x34 24 | case textWithLanguage = 0x35 25 | case nameWithLanguage = 0x36 26 | case endCollection = 0x37 27 | case textWithoutLanguage = 0x41 28 | case nameWithoutLanguage = 0x42 29 | case keyword = 0x44 30 | case uri = 0x45 31 | case uriScheme = 0x46 32 | case charset = 0x47 33 | case naturalLanguage = 0x48 34 | case mimeMediaType = 0x49 35 | case memberAttrName = 0x4A 36 | 37 | static var valueRange: ClosedRange { 0x10 ... 0xFF } 38 | } 39 | 40 | extension IppAttributeGroup.Name { 41 | var tag: UInt8 { 42 | return switch self { 43 | case .operation: 44 | DelimiterTag.operationAttributes.rawValue 45 | case .job: 46 | DelimiterTag.jobAttributes.rawValue 47 | case .printer: 48 | DelimiterTag.printerAttributes.rawValue 49 | case .unsupported: 50 | DelimiterTag.unsupportedAttributes.rawValue 51 | case let .unknownGroup(tag): 52 | tag 53 | } 54 | } 55 | 56 | init(tag: UInt8) { 57 | guard let delimiterTag = DelimiterTag(rawValue: tag) else { 58 | self = .unknownGroup(tag: tag) 59 | return 60 | } 61 | 62 | self = switch delimiterTag { 63 | case .operationAttributes: .operation 64 | case .jobAttributes: .job 65 | case .printerAttributes: .printer 66 | case .unsupportedAttributes: .unsupported 67 | case .endOfAttributes: preconditionFailure("endOfAttributes is not a valid group name") 68 | } 69 | } 70 | } 71 | 72 | extension FixedWidthInteger where Self == Int16 { 73 | static var zeroLength: Self { 0 } 74 | } 75 | 76 | /// Internal protocol for serializing and deserializing IPP requests and responses. 77 | protocol IppCodable { 78 | var version: IppVersion { get } 79 | var operationIdOrStatusCode: Int16 { get } 80 | var requestId: Int32 { get } 81 | var attributeGroups: IppAttributeGroups { get } 82 | 83 | init( 84 | version: IppVersion, 85 | operationIdOrStatusCode: Int16, 86 | requestId: Int32, 87 | attributeGroups: IppAttributeGroups 88 | ) 89 | } 90 | 91 | extension IppRequest: IppCodable { 92 | var operationIdOrStatusCode: Int16 { operationId.rawValue } 93 | init(version: IppVersion, operationIdOrStatusCode: Int16, requestId: Int32, attributeGroups: IppAttributeGroups) { 94 | self.init( 95 | version: version, 96 | operationId: IppOperationId(rawValue: operationIdOrStatusCode), 97 | requestId: requestId, 98 | attributeGroups: attributeGroups 99 | ) 100 | } 101 | } 102 | 103 | extension IppResponse: IppCodable { 104 | var operationIdOrStatusCode: Int16 { statusCode.rawValue } 105 | init(version: IppVersion, operationIdOrStatusCode: Int16, requestId: Int32, attributeGroups: IppAttributeGroups) { 106 | self.init( 107 | version: version, 108 | statusCode: IppStatusCode(rawValue: operationIdOrStatusCode), 109 | requestId: requestId, 110 | attributeGroups: attributeGroups 111 | ) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "async-http-client", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swift-server/async-http-client.git", 7 | "state" : { 8 | "revision" : "5ccda442f103792d67680aefc8d0a87392fbd66c", 9 | "version" : "1.20.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-algorithms", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-algorithms", 16 | "state" : { 17 | "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", 18 | "version" : "1.2.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-async-algorithms", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-async-algorithms", 25 | "state" : { 26 | "revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36", 27 | "version" : "1.0.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-atomics", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-atomics.git", 34 | "state" : { 35 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 36 | "version" : "1.2.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-collections", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-collections.git", 43 | "state" : { 44 | "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", 45 | "version" : "1.0.6" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-http-types", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-http-types", 52 | "state" : { 53 | "revision" : "1827dc94bdab2eb5f2fc804e9b0cb43574282566", 54 | "version" : "1.0.2" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-log", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-log.git", 61 | "state" : { 62 | "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", 63 | "version" : "1.5.3" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-nio", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-nio.git", 70 | "state" : { 71 | "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", 72 | "version" : "2.62.0" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-nio-extras", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/apple/swift-nio-extras.git", 79 | "state" : { 80 | "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", 81 | "version" : "1.20.0" 82 | } 83 | }, 84 | { 85 | "identity" : "swift-nio-http2", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/apple/swift-nio-http2.git", 88 | "state" : { 89 | "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806", 90 | "version" : "1.29.0" 91 | } 92 | }, 93 | { 94 | "identity" : "swift-nio-ssl", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/apple/swift-nio-ssl.git", 97 | "state" : { 98 | "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", 99 | "version" : "2.25.0" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-nio-transport-services", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 106 | "state" : { 107 | "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", 108 | "version" : "1.20.0" 109 | } 110 | }, 111 | { 112 | "identity" : "swift-numerics", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/apple/swift-numerics.git", 115 | "state" : { 116 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 117 | "version" : "1.0.2" 118 | } 119 | } 120 | ], 121 | "version" : 2 122 | } 123 | -------------------------------------------------------------------------------- /Tests/IppTests/EncodingTests.swift: -------------------------------------------------------------------------------- 1 | import IppProtocol 2 | import NIOCore 3 | import XCTest 4 | 5 | final class EncodingRoundTripTests: XCTestCase { 6 | func testMultipleAttributeGroups() throws { 7 | let request = IppRequest( 8 | version: .v1_1, 9 | operationId: .validateJob, 10 | requestId: 1, 11 | attributeGroups: [ 12 | .operation: [ 13 | "attributes-charset": .init(.charset("utf-8")), 14 | "attributes-natural-language": .init(.naturalLanguage("en-us")), 15 | ], 16 | .job: ["foo": .init(.boolean(false))], 17 | .printer: [:], 18 | ] 19 | ) 20 | 21 | XCTAssertEqual(request, try request.roundtrippedCopy()) 22 | } 23 | 24 | func testAttributesWithAdditionValues() throws { 25 | let request = IppRequest( 26 | version: .v1_1, 27 | operationId: .validateJob, 28 | requestId: 1, 29 | attributeGroups: [ 30 | .operation: [ 31 | "some-list": .init(.integer(1), .integer(2), .integer(3)), 32 | ], 33 | ] 34 | ) 35 | 36 | XCTAssertEqual(request, try request.roundtrippedCopy()) 37 | } 38 | 39 | func testSimpleTypes() throws { 40 | let request = IppRequest( 41 | version: .v1_1, 42 | operationId: .validateJob, 43 | requestId: 999, 44 | attributeGroups: [ 45 | .operation: [ 46 | "a": .init(.enumValue(1)), 47 | "b": .init(.keyword("foo")), 48 | "c": .init(.mimeMediaType("bar")), 49 | "d": .init(.uri("ipp://yolo")), 50 | "e": .init(.uriScheme("ipp")), 51 | "f": .init(.boolean(true), .boolean(false)), 52 | "oob": .init(.noValue, .unknown, .unsupported), 53 | ], 54 | ] 55 | ) 56 | 57 | XCTAssertEqual(request, try request.roundtrippedCopy()) 58 | } 59 | 60 | func testTextAndNameTypes() throws { 61 | let request = IppResponse( 62 | version: .v1_0, 63 | statusCode: .successfulOk, 64 | requestId: 999, 65 | attributeGroups: [ 66 | .operation: [ 67 | "a": .init(.name(.withoutLanguage("foo")), .name(.withLanguage(language: "en", "bar"))), 68 | "b": .init(.text(.withoutLanguage("baz")), .text(.withLanguage(language: "de", "baq"))), 69 | ], 70 | ] 71 | ) 72 | 73 | XCTAssertEqual(request, try request.roundtrippedCopy()) 74 | } 75 | 76 | func testComplexTypes() throws { 77 | let date = IppAttribute.Value.DateTime(year: 2024, month: 1, day: 2, hour: 3, minutes: 4, seconds: 5, deciSeconds: 6, directionFromUtc: UInt8(ascii: "-"), hoursFromUtc: 5, minutesFromUtc: 30) 78 | 79 | let request = IppResponse( 80 | version: .v1_0, 81 | statusCode: .successfulOk, 82 | requestId: 999, 83 | attributeGroups: [ 84 | .operation: [ 85 | "a": .init(.dateTime(date)), 86 | "b": .init(.resolution(.init(crossFeed: 10, feed: 20, units: 30))), 87 | "c": .init(.rangeOfInteger(1 ... 5)), 88 | "d": .init(.octetString([1, 2, 3, 4, 5])), 89 | ], 90 | ] 91 | ) 92 | 93 | XCTAssertEqual(request, try request.roundtrippedCopy()) 94 | } 95 | 96 | func testCollectionsWithLists() throws { 97 | let request = IppRequest( 98 | version: .v1_1, 99 | operationId: .validateJob, 100 | requestId: 1, 101 | attributeGroups: [ 102 | .operation: [ 103 | "some-collection": .init(.collection( 104 | [ 105 | "some-list": .init(.integer(1), .integer(2), .integer(3)), 106 | "some-text": .init(.text(.withLanguage(language: "fr", "foo"))), 107 | ] 108 | )), 109 | ], 110 | .job: ["foo": .init(.collection([:]))], 111 | ] 112 | ) 113 | 114 | XCTAssertEqual(request, try request.roundtrippedCopy()) 115 | } 116 | 117 | func testNestedCollections() throws { 118 | let request = IppRequest( 119 | version: .v1_1, 120 | operationId: .validateJob, 121 | requestId: 1, 122 | attributeGroups: [ 123 | .operation: [ 124 | "a": .init(.collection( 125 | [ 126 | "b": .init(.collection( 127 | [ 128 | "foo": .init(.integer(1), .integer(2), .integer(3)), 129 | ] 130 | )), 131 | "c": .init(.collection( 132 | [ 133 | "foo": .init(.boolean(true)), 134 | ] 135 | )), 136 | ] 137 | )), 138 | ] 139 | ] 140 | ) 141 | 142 | XCTAssertEqual(request, try request.roundtrippedCopy()) 143 | } 144 | 145 | func testUnsupportedGroupsAndValues() throws { 146 | let request = IppResponse( 147 | version: .v1_0, 148 | statusCode: .successfulOk, 149 | requestId: 999, 150 | attributeGroups: [ 151 | .unknownGroup(tag: 0x0F): [ 152 | "a": .init(.unknownValueTag(tag: 0xFF, value: [1,2,3])), 153 | ], 154 | ] 155 | ) 156 | 157 | XCTAssertEqual(request, try request.roundtrippedCopy()) 158 | } 159 | 160 | } 161 | 162 | private extension IppRequest { 163 | func roundtrippedCopy() throws -> Self { 164 | var buffer = ByteBuffer() 165 | write(to: &buffer) 166 | return try Self(buffer: &buffer) 167 | } 168 | } 169 | 170 | private extension IppResponse { 171 | func roundtrippedCopy() throws -> Self { 172 | var buffer = ByteBuffer() 173 | write(to: &buffer) 174 | return try Self(buffer: &buffer) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipp-nio: Internet Printing Protocol for Swift 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsliemeobn%2Fipp-nio%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/sliemeobn/ipp-nio) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsliemeobn%2Fipp-nio%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/sliemeobn/ipp-nio) 5 | 6 | An implementation of the [Internet Printing Protocol (IPP)](https://www.rfc-editor.org/rfc/rfc8011) in pure swift, based on [swift-nio](https://github.com/apple/swift-nio) and [async-http-client](https://github.com/swift-server/async-http-client). 7 | 8 | This library allows you to communicate with virtually any network printer directly without any drivers or OS dependencies. It provides an easy API for encoding and exchanging IPP 1.1 requests and responses and a flexible, swifty way to work with attributes in a stongly-typed manner. 9 | 10 | **WARNING:** This package is fresh out of oven, you'll be part of the battle-testing phase. See the [implementation status](#status-of-implementation) below. 11 | 12 | ## Add `ipp-nio` to your package 13 | ```swift 14 | // Add to package dependencies 15 | .package(url: "https://github.com/sliemeobn/ipp-nio.git", from: "0.1.0"), 16 | ``` 17 | ```swift 18 | // Add to your target depencies 19 | dependencies: [ 20 | .product(name: "IppClient", package: "ipp-nio"), 21 | ] 22 | ``` 23 | 24 | ## Features 25 | 26 | The library's feature set is roughly organized in four layers: 27 | 28 | ### IppProtocol Module 29 | - IppRequest/IppResponse encoding and decoding into the [IPP wire format](https://www.rfc-editor.org/rfc/rfc8010) 30 | - Flexibly typed attribute accessors based on the [semantic model](https://www.rfc-editor.org/rfc/rfc8011) 31 | - A simple high-level API to execute common operations directly (ie: print this file) 32 | 33 | ### IppClient Module 34 | - The actual implementation using HTTPClient as the transfer mechanism to execute IPP operations via HTTP 35 | 36 | ## Usage 37 | 38 | ### Print a PDF 39 | ```swift 40 | import struct Foundation.Data 41 | import IppClient 42 | 43 | let printer = IppPrinter( 44 | httpClient: HTTPClient(configuration: .init(certificateVerification: .none)), 45 | uri: "ipps://my-printer/ipp/print" 46 | ) 47 | 48 | let pdf = try Data(contentsOf: .init(fileURLWithPath: "myfile.pdf")) 49 | 50 | let response = try await printer.printJob( 51 | documentFormat: "application/pdf", 52 | data: .bytes(pdf) 53 | ) 54 | 55 | if response.statusCode.class == .successful { 56 | print("Print job submitted") 57 | } else { 58 | print("Print job failed with status \(response.statusCode) \(response[operation: \.statusMessage])") 59 | } 60 | ``` 61 | 62 | ### Setting job template attributes 63 | ```swift 64 | var jobAttributes = IppAttributes() 65 | jobAttributes[\.jobTemplate.copies] = 2 66 | jobAttributes[\.jobTemplate.orientationRequested] = .landscape 67 | jobAttributes["some-custom-thing"] = .init(.keyword("some-value")) 68 | 69 | let response = try await printer.printJob( 70 | documentName: "myfile.pdf", 71 | jobAttributes: jobAttributes, 72 | data: .stream(myFileAsStream) 73 | ) 74 | ``` 75 | 76 | ### Requesting a job's state 77 | ```swift 78 | let response = try await printer.printJob(data: .bytes(myData)) 79 | guard let jobId = response[job: \.jobId] else { exit(1) } 80 | 81 | let job = printer.job(jobId) 82 | 83 | while true { 84 | let response = try await job.getJobAttributes(requestedAttributes: [.jobState]) 85 | guard let jobState = response[job: \.jobState] else { 86 | print("Failed to get job state") 87 | exit(1) 88 | } 89 | 90 | switch jobState { 91 | case .aborted, .canceled, .completed: 92 | print("Job ended with state \(jobState)") 93 | exit(0) 94 | default: 95 | print("Job state is \(jobState)") 96 | } 97 | 98 | try await Task.sleep(for: .seconds(3)) 99 | } 100 | ``` 101 | 102 | ### Setting authentication 103 | ```swift 104 | // "basic" mode 105 | let printer = IppPrinter( 106 | httpClient: HTTPClient(configuration: .init(certificateVerification: .none)), 107 | uri: "ipps://my-printer/ipp/print", 108 | authentication: .basic(username: "user", password: "top-secret") 109 | ) 110 | 111 | // "requesting-user" mode 112 | let printer = IppPrinter( 113 | httpClient: HTTPClient(configuration: .init(certificateVerification: .none)), 114 | uri: "ipps://my-printer/ipp/print", 115 | authentication: .requestingUser(username: "user") 116 | ) 117 | 118 | ``` 119 | 120 | ### Working with raw payloads 121 | ```swift 122 | import IppProtocol 123 | import NIOCore 124 | 125 | var request = IppRequest( 126 | version: .v1_1, 127 | operationId: .holdJob, 128 | requestId: 1 129 | ) 130 | 131 | request[.operation][.attributesCharset] = .init(.charset("utf-8")) 132 | request[.operation][.attributesNaturalLanguage] = .init(.naturalLanguage("en-us")) 133 | request[.operation][.printerUri] = .init(.uri("ipp://localhost:631/printers/ipp-printer")) 134 | request[.job]["my-crazy-attribute"] = .init(.enumValue(420), .enumValue(69)) 135 | 136 | var bytes = ByteBuffer() 137 | request.write(to: &bytes) 138 | let read = try! IppRequest(buffer: &bytes) 139 | 140 | print(request == read) // true 141 | ``` 142 | 143 | ## What is my printer's IPP URL? 144 | 145 | Most printers are discoverable via DNS-SD/Bonjour, any DNS-SD browser should show their information. (eg: [Discovery](https://apps.apple.com/ca/app/discovery-dns-sd-browser/id1381004916?mt=12) for macOS). 146 | 147 | The `rp` value is the URL path (usually `/ipp/print`), the scheme is always `ipp://` or `ipps://`. 148 | 149 | On macOS, shared printers are also exposed via IPP. (ie: any printer can be a network printer with a server in the middle) 150 | 151 | ## Status of implementation 152 | 153 | The basic, low-level encoding and transfer is robust and should fulfill all needs. 154 | The *semantic model* only covers the most basic attributes for now, but can be extended quite easily as needed. 155 | 156 | Since the library is written with custom extensions in mind, it should be quite simple to extend to any use case even without direct support. 157 | 158 | Missing: 159 | - consistent documentation 160 | - top-level APIs for all operations 161 | - support for CUPS operations 162 | - support IPP 2.x features 163 | 164 | Anything you would like to have added? Just ping me, also "pull requests welcome" ^^ -------------------------------------------------------------------------------- /Sources/IppProtocol/Encoding/ByteBuffer+writing.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension ByteBuffer { 4 | mutating func writeIppCodable(_ codable: some IppCodable) { 5 | writeVersion(codable.version) 6 | writeMultipleIntegers(codable.operationIdOrStatusCode, codable.requestId) 7 | writeAttributeGroups(codable.attributeGroups) 8 | writeInteger(DelimiterTag.endOfAttributes.rawValue) 9 | } 10 | 11 | mutating func writeVersion(_ version: IppVersion) { 12 | writeMultipleIntegers(version.major, version.minor) 13 | } 14 | 15 | mutating func writeAttributeGroups(_ groups: IppAttributeGroups) { 16 | for (group) in groups { 17 | writeInteger(group.name.tag) 18 | 19 | for (name, attribute) in group.attributes.elements { 20 | writeAttribute(attribute, named: name.rawValue) 21 | } 22 | } 23 | } 24 | 25 | mutating func writeAttribute(_ attribute: IppAttribute, named name: String?) { 26 | writeAttributeValue(attribute.value, withName: name) 27 | if let additionalValues = attribute.additionalValues { 28 | for value in additionalValues { 29 | writeAttributeValue(value, withName: nil) 30 | } 31 | } 32 | } 33 | 34 | mutating func writeAttributeValue(_ value: IppAttribute.Value, withName name: String?) { 35 | func writeValuePrelude(_ tag: ValueTag) { 36 | writeValuePrelude(tag.rawValue) 37 | } 38 | 39 | func writeValuePrelude(_ tag: ValueTag.RawValue) { 40 | writeInteger(tag) 41 | 42 | if let name { 43 | writeStringWithSize(name) 44 | } else { 45 | writeInteger(.zeroLength) 46 | } 47 | } 48 | 49 | switch value { 50 | // numbers and bool 51 | case let .integer(value): 52 | writeValuePrelude(.integer) 53 | writeMultipleIntegers(Int16(4), value) 54 | case let .boolean(value): 55 | writeValuePrelude(.boolean) 56 | writeMultipleIntegers(Int16(1), Int8(value ? 1 : 0)) 57 | case let .enumValue(value): 58 | writeValuePrelude(.enum) 59 | writeMultipleIntegers(Int16(4), value) 60 | 61 | // all string values 62 | case let .charset(value): 63 | writeValuePrelude(.charset) 64 | writeStringWithSize(value) 65 | case let .keyword(value): 66 | writeValuePrelude(.keyword) 67 | writeStringWithSize(value) 68 | case let .mimeMediaType(value): 69 | writeValuePrelude(.mimeMediaType) 70 | writeStringWithSize(value) 71 | case let .name(.withoutLanguage(value)): 72 | writeValuePrelude(.nameWithoutLanguage) 73 | writeStringWithSize(value) 74 | case let .text(.withoutLanguage(value)): 75 | writeValuePrelude(.textWithoutLanguage) 76 | writeStringWithSize(value) 77 | case let .naturalLanguage(value): 78 | writeValuePrelude(.naturalLanguage) 79 | writeStringWithSize(value) 80 | case let .uri(value): 81 | writeValuePrelude(.uri) 82 | writeStringWithSize(value) 83 | case let .uriScheme(value): 84 | writeValuePrelude(.uriScheme) 85 | writeStringWithSize(value) 86 | 87 | // plain text versions are matched above, these are with language 88 | case let .name(withLanguage): 89 | writeValuePrelude(.nameWithLanguage) 90 | writeStringWithSize(withLanguage) 91 | case let .text(withLanguage): 92 | writeValuePrelude(.textWithLanguage) 93 | writeStringWithSize(withLanguage) 94 | 95 | // complex values 96 | case let .dateTime(value): 97 | writeValuePrelude(.dateTime) 98 | writeMultipleIntegers( 99 | Int16(11), 100 | value.year, value.month, value.day, 101 | value.hour, value.minutes, value.seconds, value.deciSeconds, 102 | value.directionFromUtc, value.hoursFromUtc, value.minutesFromUtc 103 | ) 104 | case let .resolution(value): 105 | writeValuePrelude(.resolution) 106 | writeMultipleIntegers(Int16(9), value.crossFeed, value.feed, value.units) 107 | case let .rangeOfInteger(value): 108 | writeValuePrelude(.rangeOfInteger) 109 | writeMultipleIntegers(Int16(8), value.lowerBound, value.upperBound) 110 | case let .octetString(value): 111 | writeValuePrelude(.octetString) 112 | writeInteger(Int16(value.count)) 113 | writeBytes(value) 114 | 115 | // out-of-band values 116 | case .unsupported: 117 | writeValuePrelude(.unsupported) 118 | writeInteger(.zeroLength) 119 | case .unknown: 120 | writeValuePrelude(.unknown) 121 | writeInteger(.zeroLength) 122 | case .noValue: 123 | writeValuePrelude(.noValue) 124 | writeInteger(.zeroLength) 125 | 126 | // collection 127 | case let .collection(collection): 128 | writeValuePrelude(.begCollection) 129 | writeInteger(.zeroLength) // value length 130 | 131 | for (name, member) in collection { 132 | writeValue(ValueTag.memberAttrName) 133 | writeInteger(.zeroLength) // name length 134 | writeStringWithSize(name.rawValue) // name as value 135 | // write all values without a leading name 136 | writeAttribute(member, named: nil) 137 | } 138 | 139 | writeMultipleIntegers( 140 | ValueTag.endCollection.rawValue, 141 | .zeroLength, // name length 142 | .zeroLength // value length 143 | ) 144 | case let .unknownValueTag(tag, value): 145 | writeValuePrelude(tag) 146 | writeInteger(Int16(value.count)) 147 | writeBytes(value) 148 | } 149 | } 150 | } 151 | 152 | // value helpers 153 | extension ByteBuffer { 154 | mutating func writeStringWithSize(_ value: IppAttribute.Value.TextOrName) { 155 | switch value { 156 | case let .withoutLanguage(text): 157 | writeStringWithSize(text) 158 | case let .withLanguage(language, text): 159 | writeInteger(Int16(4 + language.count + text.count)) 160 | writeStringWithSize(language) 161 | writeStringWithSize(text) 162 | } 163 | } 164 | 165 | mutating func writeStringWithSize(_ value: String) { 166 | writeInteger(Int16(value.utf8.count)) 167 | writeString(value) 168 | } 169 | 170 | mutating func writeValue(_ value: some RawRepresentable) where T: FixedWidthInteger { 171 | writeInteger(value.rawValue) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/IppProtocol/IppObjects.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.URLComponents // NOTE: the only use of Foundation... worth it? 2 | 3 | /// Defines an IPP object that can be used to create and execute IPP requests. 4 | /// 5 | /// This is to decouple the specific HTTP transport from the semantic mode. 6 | public protocol IppObjectProtocol { 7 | associatedtype DataFormat 8 | /// Creates a new IPP request with the given operation ID that is targeted at this object. 9 | /// 10 | /// The request will be initialized with the IPP version, operation ID, the the required attributes for its target. 11 | func makeNewRequest(operation: IppOperationId) -> IppRequest 12 | 13 | /// Executes the given request and returns the response. 14 | func execute(request: IppRequest, data: DataFormat?) async throws -> IppResponse 15 | } 16 | 17 | /// Defines a simplified API for the most common operations of IPP printers. 18 | public protocol IppPrinterObject: IppObjectProtocol {} 19 | 20 | /// Defines a simplified API for the most common operations of IPP jobs. 21 | public protocol IppJobObject: IppObjectProtocol {} 22 | 23 | public extension IppPrinterObject { 24 | /// Executes a Get-Printer-Attributes request. 25 | func getPrinterAttributes( 26 | requestedAttributes: [IppAttribute.Name]? = nil, 27 | documentFormat: String? = nil 28 | ) async throws -> IppResponse { 29 | var request = makeNewRequest(operation: .getPrinterAttributes) 30 | 31 | request[.operation].with { 32 | $0[\.operation.requestedAttributes] = requestedAttributes 33 | $0[\.operation.documentFormat] = documentFormat 34 | } 35 | 36 | return try await execute(request: request, data: nil) 37 | } 38 | 39 | /// Executes a Print-Job request. 40 | func printJob( 41 | jobName: String? = nil, 42 | attributeFidelity: Bool? = nil, 43 | documentName: String? = nil, 44 | documentFormat: String? = nil, 45 | jobAttributes: IppAttributes? = nil, 46 | data: DataFormat 47 | ) async throws -> IppResponse { 48 | var request = makeNewRequest(operation: .printJob) 49 | 50 | request[.operation].with { 51 | $0[\.operation.jobName] = jobName 52 | $0[\.operation.ippAttributeFidelity] = attributeFidelity 53 | $0[\.operation.documentName] = documentName 54 | $0[\.operation.documentFormat] = documentFormat 55 | } 56 | 57 | if let jobAttributes = jobAttributes { 58 | request[.job] = jobAttributes 59 | } 60 | 61 | return try await execute(request: request, data: data) 62 | } 63 | } 64 | 65 | public extension IppJobObject { 66 | /// Executes a Get-Job-Attributes request. 67 | func getJobAttributes( 68 | requestedAttributes: [IppAttribute.Name]? = nil 69 | ) async throws -> IppResponse { 70 | var request = makeNewRequest(operation: .getJobAttributes) 71 | 72 | request[operation: \.requestedAttributes] = requestedAttributes 73 | 74 | return try await execute(request: request, data: nil) 75 | } 76 | 77 | /// Execute a Cancel-Job request. 78 | func cancelJob(message: String? = nil) async throws -> IppResponse { 79 | var request = makeNewRequest(operation: .cancelJob) 80 | 81 | request[operation: \.message] = message 82 | 83 | return try await execute(request: request, data: nil) 84 | } 85 | } 86 | 87 | public extension IppRequest { 88 | /// Creates a new IPP request targetet at a printer object. 89 | init( 90 | printerUri: String, 91 | operation: IppOperationId, 92 | requestingUserName: String? = nil, 93 | attributesNaturalLanguage: String = "en", 94 | version: IppVersion = .v1_1 95 | ) { 96 | // NOTE: maybe use a running number for requestId? not really worth the complexity though (nobody cares about it) 97 | self.init(version: .v1_1, operationId: operation, requestId: 1) 98 | 99 | // the order of these is important 100 | self[.operation].with { 101 | $0[\.operation.attributesCharset] = "utf-8" 102 | $0[\.operation.attributesNaturalLanguage] = attributesNaturalLanguage 103 | $0[\.operation.printerUri] = printerUri 104 | $0[\.operation.requestingUserName] = requestingUserName 105 | } 106 | } 107 | 108 | /// Creates a new IPP request targeted at a job object. 109 | init( 110 | printerUri: String, 111 | jobId: Int32, 112 | operation: IppOperationId, 113 | requestingUserName: String? = nil, 114 | attributesNaturalLanguage: String = "en", 115 | version: IppVersion = .v1_1 116 | ) { 117 | self.init(version: .v1_1, operationId: operation, requestId: 1) 118 | 119 | // the order of these is important 120 | self[.operation].with { 121 | $0[\.operation.attributesCharset] = "utf-8" 122 | $0[\.operation.attributesNaturalLanguage] = attributesNaturalLanguage 123 | $0[\.operation.printerUri] = printerUri 124 | $0[\.operation.jobId] = jobId 125 | $0[\.operation.requestingUserName] = requestingUserName 126 | } 127 | } 128 | 129 | /// Returns the target URL for this request or throws if the request is invalid. 130 | /// 131 | /// Accessing this propery will throw if the request does not contain the required attributes for sending 132 | /// according to the IPP specification. 133 | var validatedHttpTargetUrl: String { 134 | get throws { 135 | guard let firstGroup = attributeGroups.first, 136 | firstGroup.name == .operation, 137 | firstGroup.attributes.count >= 3 138 | else { 139 | throw InvalidRequestError.invalidOperationAttributes 140 | } 141 | 142 | guard firstGroup.attributes.keys[0] == .attributesCharset, 143 | firstGroup.attributes.keys[1] == .attributesNaturalLanguage, 144 | firstGroup.attributes.keys[2] == .printerUri || firstGroup.attributes.keys[2] == .jobUri, 145 | case let .uri(uri) = firstGroup.attributes.values[2].value 146 | else { 147 | throw InvalidRequestError.invalidOperationAttributes 148 | } 149 | 150 | guard var targetURL = URLComponents(string: uri) else { 151 | throw InvalidRequestError.invalidTargetUri(uri) 152 | } 153 | 154 | switch targetURL.scheme { 155 | case "ipp": 156 | targetURL.scheme = "http" 157 | case "ipps": 158 | targetURL.scheme = "https" 159 | default: 160 | throw InvalidRequestError.invalidScheme(targetURL.scheme ?? "") 161 | } 162 | 163 | targetURL.port = targetURL.port ?? 631 164 | return targetURL.string! 165 | } 166 | } 167 | } 168 | 169 | enum InvalidRequestError: Error, CustomStringConvertible { 170 | case invalidTargetUri(String) 171 | case invalidScheme(String) 172 | case invalidOperationAttributes 173 | 174 | var description: String { 175 | switch self { 176 | case let .invalidTargetUri(uri): 177 | return "Invalid target URI: \(uri)" 178 | case let .invalidScheme(scheme): 179 | return "Invalid scheme: \(scheme)" 180 | case .invalidOperationAttributes: 181 | return "Operation attributes must contain attributesCharset, attributesNaturalLanguage, and a targetUri as the first three attributes." 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Sources/IppProtocol/Values.swift: -------------------------------------------------------------------------------- 1 | public extension IppAttribute.Name { 2 | // operation attributes 3 | static var attributesCharset: Self { .init(rawValue: "attributes-charset") } 4 | static var attributesNaturalLanguage: Self { .init(rawValue: "attributes-natural-language") } 5 | static var printerUri: Self { .init(rawValue: "printer-uri") } 6 | static var jobUri: Self { .init(rawValue: "job-uri") } 7 | static var documentUri: Self { .init(rawValue: "document-uri") } 8 | static var requestingUserName: Self { .init(rawValue: "requesting-user-name") } 9 | static var jobName: Self { .init(rawValue: "job-name") } 10 | static var jobId: Self { .init(rawValue: "job-id") } 11 | static var documentName: Self { .init(rawValue: "document-name") } 12 | static var documentFormat: Self { .init(rawValue: "document-format") } 13 | static var requestedAttributes: Self { .init(rawValue: "requested-attributes") } 14 | static var message: Self { .init(rawValue: "message") } 15 | static var ippAttributeFidelity: Self { .init(rawValue: "ipp-attribute-fidelity") } 16 | static var compression: Self { .init(rawValue: "compression") } 17 | 18 | // job template attributes 19 | static var jobPriority: Self { .init(rawValue: "job-priority") } 20 | static var jobHoldUntil: Self { .init(rawValue: "job-hold-until") } 21 | static var jobSheets: Self { .init(rawValue: "job-sheets") } 22 | static var multipleDocumentHandling: Self { .init(rawValue: "multiple-document-handling") } 23 | static var copies: Self { .init(rawValue: "copies") } 24 | static var finishings: Self { .init(rawValue: "finishings") } 25 | static var pageRanges: Self { .init(rawValue: "page-ranges") } 26 | static var sides: Self { .init(rawValue: "sides") } 27 | static var numberUp: Self { .init(rawValue: "number-up") } 28 | static var orientationRequested: Self { .init(rawValue: "orientation-requested") } 29 | static var media: Self { .init(rawValue: "media") } 30 | static var printerResolution: Self { .init(rawValue: "printer-resolution") } 31 | static var printQuality: Self { .init(rawValue: "print-quality") } 32 | 33 | // job description attributes 34 | static var jobState: Self { .init(rawValue: "job-state") } 35 | static var jobStateMessage: Self { .init(rawValue: "job-state-message") } 36 | static var jobStateReasons: Self { .init(rawValue: "job-state-reasons") } 37 | 38 | // printer description attributes 39 | static var documentFormatSupported: Self { .init(rawValue: "document-format-supported") } 40 | static var printerName: Self { .init(rawValue: "printer-name") } 41 | static var printerState: Self { .init(rawValue: "printer-state") } 42 | static var printerStateMessage: Self { .init(rawValue: "printer-state-message") } 43 | static var printerStateReasons: Self { .init(rawValue: "printer-state-reasons") } 44 | static var printerIsAcceptingJobs: Self { .init(rawValue: "printer-is-accepting-jobs") } 45 | static var queuedJobCount: Self { .init(rawValue: "queued-job-count") } 46 | static var printerInfo: Self { .init(rawValue: "printer-info") } 47 | static var printerUriSupported: Self { .init(rawValue: "printer-uri-supported") } 48 | static var uriSecuritySupported: Self { .init(rawValue: "uri-security-supported") } 49 | static var uriAuthenticationSupported: Self { .init(rawValue: "uri-authentication-supported") } 50 | static var printerLocation: Self { .init(rawValue: "printer-location") } 51 | static var printerMoreInfo: Self { .init(rawValue: "printer-more-info") } 52 | static var printerMessageFromOperator: Self { .init(rawValue: "printer-message-from-operator") } 53 | static var colorSupported: Self { .init(rawValue: "color-supported") } 54 | 55 | // operation response attributes 56 | static var statusMessage: Self { .init(rawValue: "status-message") } 57 | static var detailedStatusMessage: Self { .init(rawValue: "detailed-status-message") } 58 | } 59 | 60 | public extension IppOperationId { 61 | static var printJob: Self { .init(rawValue: 0x0002) } 62 | static var printUri: Self { .init(rawValue: 0x0003) } 63 | static var validateJob: Self { .init(rawValue: 0x0004) } 64 | static var createJob: Self { .init(rawValue: 0x0005) } 65 | static var sendDocument: Self { .init(rawValue: 0x0006) } 66 | static var sendUri: Self { .init(rawValue: 0x0007) } 67 | static var cancelJob: Self { .init(rawValue: 0x0008) } 68 | static var getJobAttributes: Self { .init(rawValue: 0x0009) } 69 | static var getJobs: Self { .init(rawValue: 0x000A) } 70 | static var getPrinterAttributes: Self { .init(rawValue: 0x000B) } 71 | static var holdJob: Self { .init(rawValue: 0x000C) } 72 | static var releaseJob: Self { .init(rawValue: 0x000D) } 73 | static var restartJob: Self { .init(rawValue: 0x000E) } 74 | static var pausePrinter: Self { .init(rawValue: 0x0010) } 75 | static var resumePrinter: Self { .init(rawValue: 0x0011) } 76 | static var purgeJobs: Self { .init(rawValue: 0x0012) } 77 | 78 | // static var setPrinterAttributes: Self { .init(rawValue: 0x0013) } 79 | // static var setJobAttributes: Self { .init(rawValue: 0x0014) } 80 | // static var getPrinterSupportedValues: Self { .init(rawValue: 0x0015) } 81 | } 82 | 83 | public extension IppStatusCode { 84 | static var successfulOk: Self { .init(rawValue: 0x0000) } 85 | static var successfulOkIgnoredOrSubstitutedAttributes: Self { .init(rawValue: 0x0001) } 86 | static var successfulOkConflictingAttributes: Self { .init(rawValue: 0x0002) } 87 | static var successfulOkIgnoredSubscriptions: Self { .init(rawValue: 0x0003) } 88 | static var successfulOkIgnoredNotifications: Self { .init(rawValue: 0x0004) } 89 | static var successfulOkTooManyEvents: Self { .init(rawValue: 0x0005) } 90 | static var successfulOkButCancelSubscription: Self { .init(rawValue: 0x0006) } 91 | static var successfulOkEventsComplete: Self { .init(rawValue: 0x0007) } 92 | static var clientErrorBadRequest: Self { .init(rawValue: 0x0400) } 93 | static var clientErrorForbidden: Self { .init(rawValue: 0x0401) } 94 | static var clientErrorNotAuthenticated: Self { .init(rawValue: 0x0402) } 95 | static var clientErrorNotAuthorized: Self { .init(rawValue: 0x0403) } 96 | static var clientErrorNotPossible: Self { .init(rawValue: 0x0404) } 97 | static var clientErrorTimeout: Self { .init(rawValue: 0x0405) } 98 | static var clientErrorNotFound: Self { .init(rawValue: 0x0406) } 99 | static var clientErrorGone: Self { .init(rawValue: 0x0407) } 100 | static var clientErrorRequestEntityTooLarge: Self { .init(rawValue: 0x0408) } 101 | static var clientErrorRequestValueTooLong: Self { .init(rawValue: 0x0409) } 102 | static var clientErrorDocumentFormatNotSupported: Self { .init(rawValue: 0x040A) } 103 | static var clientErrorAttributesOrValuesNotSupported: Self { .init(rawValue: 0x040B) } 104 | static var clientErrorUriSchemeNotSupported: Self { .init(rawValue: 0x040C) } 105 | static var clientErrorCharsetNotSupported: Self { .init(rawValue: 0x040D) } 106 | static var clientErrorConflictingAttributes: Self { .init(rawValue: 0x040E) } 107 | } 108 | 109 | public extension IppStatusCode { 110 | enum Class: String { 111 | case successful 112 | case informational 113 | case redirection 114 | case clientError 115 | case serverError 116 | case invalid 117 | } 118 | 119 | /// The class of the status code based on the IPP value ranges. 120 | var `class`: Class { 121 | switch rawValue { 122 | case 0x0000...0x00ff: .successful 123 | case 0x0100...0x01ff: .informational 124 | case 0x0300...0x03ff: .redirection 125 | case 0x0400...0x04ff: .clientError 126 | case 0x0500...0x05ff: .serverError 127 | default: .invalid 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /Sources/IppProtocol/RequestResponse.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import OrderedCollections 3 | 4 | /// Represents a request message that can be sent to an IPP object. 5 | public struct IppRequest: Equatable, Sendable { 6 | public var version: IppVersion 7 | public var operationId: IppOperationId 8 | public var requestId: Int32 9 | public var attributeGroups: IppAttributeGroups 10 | 11 | public init(version: IppVersion, operationId: IppOperationId, requestId: Int32, attributeGroups: IppAttributeGroups = []) { 12 | self.version = version 13 | self.operationId = operationId 14 | self.requestId = requestId 15 | self.attributeGroups = attributeGroups 16 | } 17 | } 18 | 19 | /// Represents a response message that is returned by an IPP object. 20 | public struct IppResponse: Equatable, Sendable { 21 | public var version: IppVersion 22 | public var statusCode: IppStatusCode 23 | public var requestId: Int32 24 | public var attributeGroups: IppAttributeGroups 25 | 26 | public init(version: IppVersion, statusCode: IppStatusCode, requestId: Int32, attributeGroups: IppAttributeGroups = []) { 27 | self.version = version 28 | self.statusCode = statusCode 29 | self.requestId = requestId 30 | self.attributeGroups = attributeGroups 31 | } 32 | } 33 | 34 | public struct IppVersion: Equatable, Sendable, CustomStringConvertible { 35 | public let major: Int8 36 | public let minor: Int8 37 | 38 | public init(major: Int8, minor: Int8) { 39 | self.major = major 40 | self.minor = minor 41 | } 42 | 43 | public var description: String { 44 | "\(major).\(minor)" 45 | } 46 | } 47 | 48 | public extension IppVersion { 49 | static var v1_0: Self { .init(major: 1, minor: 0) } 50 | static var v1_1: Self { .init(major: 1, minor: 1) } 51 | } 52 | 53 | public struct IppOperationId: RawRepresentable, Equatable, Sendable { 54 | public let rawValue: Int16 55 | 56 | public init(rawValue: Int16) { 57 | self.rawValue = rawValue 58 | } 59 | 60 | public var description: String { 61 | "0x\(String(rawValue, radix: 16, uppercase: true))" 62 | } 63 | } 64 | 65 | public struct IppStatusCode: RawRepresentable, Equatable, Sendable, CustomStringConvertible { 66 | public let rawValue: Int16 67 | 68 | public init(rawValue: Int16) { 69 | self.rawValue = rawValue 70 | } 71 | 72 | public var description: String { 73 | "0x\(String(rawValue, radix: 16, uppercase: true))" 74 | } 75 | } 76 | 77 | public struct IppAttributeGroup: Equatable, Sendable { 78 | public enum Name: Hashable, Sendable { 79 | case operation 80 | case job 81 | case printer 82 | case unsupported 83 | case unknownGroup(tag: UInt8) 84 | } 85 | 86 | public let name: Name 87 | public var attributes: IppAttributes 88 | 89 | public init(name: Name, attributes: IppAttributes) { 90 | self.name = name 91 | self.attributes = attributes 92 | } 93 | } 94 | 95 | public typealias IppAttributeGroups = [IppAttributeGroup] 96 | public typealias IppAttributes = OrderedDictionary 97 | 98 | public struct IppAttribute: Equatable, Sendable, CustomStringConvertible { 99 | internal var additionalValues: [Value]? 100 | 101 | /// The value of this attribute. 102 | /// If the attribute has multiple values, this will be the first value. 103 | public let value: Value 104 | 105 | public init(_ value: Value, _ additionalValues: Value...) { 106 | self.value = value 107 | 108 | if !additionalValues.isEmpty { 109 | self.additionalValues = additionalValues 110 | } 111 | } 112 | 113 | public init?(_ values: some Collection) { 114 | guard !values.isEmpty else { return nil } 115 | value = values.first! 116 | if values.count > 1 { 117 | additionalValues = Array(values.dropFirst()) 118 | } 119 | } 120 | 121 | public var description: String { 122 | if isSet { 123 | "[\(values.map { "\($0)" }.joined(separator: ","))]" 124 | } else { 125 | "\(value)" 126 | } 127 | } 128 | } 129 | 130 | public extension IppAttribute { 131 | /// Returns `true` if this attribute has a list of values. 132 | var isSet: Bool { 133 | additionalValues != nil && additionalValues!.count > 0 134 | } 135 | 136 | /// Returns the list of values for this attribute. 137 | /// 138 | /// The list will always contain at least one value (equal to `value`) 139 | var values: [Value] { 140 | if let additionalValues = additionalValues { 141 | return [value] + additionalValues 142 | } else { 143 | return [value] 144 | } 145 | } 146 | } 147 | 148 | public extension IppAttribute { 149 | struct Name: RawRepresentable, Hashable, Sendable, CustomStringConvertible, ExpressibleByStringInterpolation { 150 | public let rawValue: String 151 | 152 | public init(rawValue: String) { 153 | self.rawValue = rawValue 154 | } 155 | 156 | public init(stringLiteral value: String) { 157 | self.init(rawValue: value) 158 | } 159 | 160 | public var description: String { rawValue } 161 | } 162 | } 163 | 164 | public extension IppAttribute { 165 | enum Value: Equatable, Sendable { 166 | // out-of-band values 167 | case unknown 168 | case unsupported 169 | case noValue 170 | 171 | // possible values semantics 172 | case text(TextOrName) 173 | case name(TextOrName) 174 | 175 | case keyword(String) 176 | case enumValue(Int32) 177 | case uri(String) 178 | case uriScheme(String) 179 | case charset(String) 180 | case naturalLanguage(String) 181 | case mimeMediaType(String) 182 | 183 | case octetString([UInt8]) 184 | case boolean(Bool) 185 | case integer(Int32) 186 | case rangeOfInteger(ClosedRange) 187 | case dateTime(DateTime) 188 | case resolution(Resolution) 189 | 190 | case collection(IppAttributes) 191 | case unknownValueTag(tag: UInt8, value: [UInt8]) 192 | } 193 | } 194 | 195 | public extension IppAttribute.Value { 196 | enum TextOrName: Equatable, Sendable, CustomStringConvertible { 197 | case withoutLanguage(String) 198 | case withLanguage(language: String, String) 199 | 200 | public var description: String { 201 | switch self { 202 | case let .withoutLanguage(value): 203 | return value 204 | case let .withLanguage(language, value): 205 | return "\(language):\(value)" 206 | } 207 | } 208 | } 209 | 210 | struct DateTime: Equatable, Sendable { 211 | public var year: Int16 212 | public var month: Int8 213 | public var day: Int8 214 | public var hour: Int8 215 | public var minutes: Int8 216 | public var seconds: Int8 217 | public var deciSeconds: Int8 218 | public var directionFromUtc: UInt8 // TODO: make this an enum 219 | public var hoursFromUtc: Int8 220 | public var minutesFromUtc: Int8 221 | 222 | public init( 223 | year: Int16, 224 | month: Int8, 225 | day: Int8, 226 | hour: Int8, 227 | minutes: Int8, 228 | seconds: Int8, 229 | deciSeconds: Int8, 230 | directionFromUtc: UInt8, 231 | hoursFromUtc: Int8, 232 | minutesFromUtc: Int8 233 | ) { 234 | self.year = year 235 | self.month = month 236 | self.day = day 237 | self.hour = hour 238 | self.minutes = minutes 239 | self.seconds = seconds 240 | self.deciSeconds = deciSeconds 241 | self.directionFromUtc = directionFromUtc 242 | self.hoursFromUtc = hoursFromUtc 243 | self.minutesFromUtc = minutesFromUtc 244 | } 245 | } 246 | 247 | struct Resolution: Equatable, Sendable { 248 | public var crossFeed: Int32 249 | public var feed: Int32 250 | public var units: Int8 251 | 252 | public init(crossFeed: Int32, feed: Int32, units: Int8) { 253 | self.crossFeed = crossFeed 254 | self.feed = feed 255 | self.units = units 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Sources/IppProtocol/Encoding/ByteBuffer+reading.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension ByteBuffer { 4 | mutating func readIppCodable(as: S.Type = S.self) throws -> S { 5 | let version = try readVersion() 6 | 7 | guard let (operationIdOrStatusCode, requestId) = readMultipleIntegers(as: (Int16, Int32).self) else { 8 | throw ParsingError.malformedHeader 9 | } 10 | 11 | let attributeGroups = try readAttributeGroups() 12 | 13 | return S( 14 | version: version, 15 | operationIdOrStatusCode: operationIdOrStatusCode, 16 | requestId: requestId, 17 | attributeGroups: attributeGroups 18 | ) 19 | } 20 | 21 | mutating func readVersion() throws -> IppVersion { 22 | guard let (major, minor) = readMultipleIntegers(as: (Int8, Int8).self) else { 23 | throw ParsingError.malformedHeader 24 | } 25 | 26 | return IppVersion(major: major, minor: minor) 27 | } 28 | 29 | mutating func readAttributeGroups() throws -> IppAttributeGroups { 30 | var groups: IppAttributeGroups = [] 31 | 32 | while let tag = readInteger(as: UInt8.self) { 33 | guard DelimiterTag.valueRange.contains(tag) else { 34 | throw ParsingError.unexpectedValueTag(tag) 35 | } 36 | 37 | if tag == DelimiterTag.endOfAttributes.rawValue { 38 | return groups 39 | } 40 | 41 | let name = IppAttributeGroup.Name(tag: tag) 42 | let attributes = try readGroupAttributes() 43 | groups.append(.init(name: name, attributes: attributes)) 44 | } 45 | 46 | throw ParsingError.missingEndOfAttributes 47 | } 48 | 49 | mutating func readGroupAttributes() throws -> IppAttributes { 50 | var group: IppAttributes = [:] 51 | 52 | while let (tag, name, valueSlice) = try readNextValueTriple() { 53 | var value: IppAttribute.Value 54 | 55 | switch tag { 56 | case ValueTag.begCollection.rawValue: 57 | value = try readAttributeCollection() 58 | case ValueTag.endCollection.rawValue, ValueTag.memberAttrName.rawValue: 59 | throw ParsingError.unexpectedValueTag(tag) 60 | default: 61 | value = try IppAttribute.Value(tag: tag, valueSlice: valueSlice) 62 | } 63 | 64 | try group.pushValue(value, withName: name) 65 | } 66 | 67 | return group 68 | } 69 | 70 | mutating func readAttributeCollection() throws -> IppAttribute.Value { 71 | var collection: IppAttributes = [:] 72 | 73 | var currentName: String? = nil 74 | 75 | while let (tag, name, valueSlice) = try readNextValueTriple() { 76 | guard name == nil else { 77 | throw ParsingError.invalidCollectionSyntax(name!) 78 | } 79 | 80 | var value: IppAttribute.Value 81 | 82 | switch tag { 83 | case ValueTag.memberAttrName.rawValue: 84 | let newName = String(buffer: valueSlice) 85 | 86 | guard currentName == nil else { 87 | throw ParsingError.invalidCollectionSyntax(newName) 88 | } 89 | 90 | currentName = newName 91 | continue 92 | case ValueTag.begCollection.rawValue: 93 | value = try readAttributeCollection() 94 | case ValueTag.endCollection.rawValue: 95 | guard currentName == nil else { 96 | throw ParsingError.invalidCollectionSyntax(currentName!) 97 | } 98 | return .collection(collection) 99 | default: 100 | value = try IppAttribute.Value(tag: tag, valueSlice: valueSlice) 101 | } 102 | 103 | try collection.pushValue(value, withName: currentName) 104 | currentName = nil 105 | } 106 | 107 | throw ParsingError.missingEndOfCollection 108 | } 109 | 110 | mutating func readNextValueTriple() throws -> (UInt8, String?, ByteBuffer)? { 111 | guard let tag = getInteger(at: readerIndex, as: UInt8.self) else { 112 | throw ParsingError.malformedValue 113 | } 114 | 115 | guard ValueTag.valueRange.contains(tag) else { 116 | // next tag is delimiter do not read further 117 | return nil 118 | } 119 | 120 | moveReaderIndex(forwardBy: 1) 121 | 122 | let name = try readSizedString() 123 | let valueSlice = try readValueSlice() 124 | return (tag, name, valueSlice) 125 | } 126 | 127 | mutating func readSizedString() throws -> String? { 128 | guard let length = readInteger(as: Int16.self) else { throw ParsingError.malformedValue } 129 | guard length != .zeroLength else { return nil } 130 | guard let string = readString(length: Int(length)) else { throw ParsingError.malformedValue } 131 | return string 132 | } 133 | 134 | mutating func readStringWithLanguage() throws -> IppAttribute.Value.TextOrName { 135 | let language = try readSizedString() 136 | let string = try readSizedString() 137 | return .withLanguage(language: language ?? "", string ?? "") 138 | } 139 | 140 | mutating func readValueSlice() throws -> ByteBuffer { 141 | guard let length = readInteger(as: Int16.self) else { throw ParsingError.malformedValue } 142 | guard let slice = readSlice(length: Int(length)) else { throw ParsingError.malformedValue } 143 | return slice 144 | } 145 | } 146 | 147 | extension ByteBuffer { 148 | mutating func readValue(as type: R.Type) -> R? 149 | where R: RawRepresentable, I: FixedWidthInteger 150 | { 151 | guard let rawValue = readInteger(as: I.self) else { return nil } 152 | return R(rawValue: rawValue) 153 | } 154 | 155 | consuming func readToEndOrFail(_ readFn: (inout Self) throws -> T?) throws -> T { 156 | guard let value = try readFn(&self) else { 157 | throw ParsingError.malformedValue 158 | } 159 | 160 | guard readableBytes == 0 else { 161 | throw ParsingError.malformedValue 162 | } 163 | 164 | return value 165 | } 166 | } 167 | 168 | extension IppAttribute.Value { 169 | init(tag: UInt8, valueSlice: consuming ByteBuffer) throws { 170 | guard let valueTag = ValueTag(rawValue: tag) else { 171 | self = .unknownValueTag( 172 | tag: tag, 173 | value: Array(buffer: valueSlice) 174 | ) 175 | return 176 | } 177 | 178 | switch valueTag { 179 | // numbers and bool 180 | case .integer: 181 | let value = try valueSlice.readToEndOrFail { 182 | $0.readInteger(as: Int32.self) 183 | } 184 | 185 | self = .integer(value) 186 | case .boolean: 187 | let value = try valueSlice.readToEndOrFail { 188 | $0.readInteger(as: UInt8.self) 189 | } 190 | 191 | self = .boolean(value != 0) 192 | case .enum: 193 | let value = try valueSlice.readToEndOrFail { 194 | $0.readInteger(as: Int32.self) 195 | } 196 | 197 | self = .enumValue(value) 198 | 199 | // all string values 200 | case .charset: 201 | self = .charset(String(buffer: valueSlice)) 202 | case .keyword: 203 | self = .keyword(String(buffer: valueSlice)) 204 | case .mimeMediaType: 205 | self = .mimeMediaType(String(buffer: valueSlice)) 206 | case .nameWithoutLanguage: 207 | self = .name(.withoutLanguage(String(buffer: valueSlice))) 208 | case .textWithoutLanguage: 209 | self = .text(.withoutLanguage(String(buffer: valueSlice))) 210 | case .naturalLanguage: 211 | self = .naturalLanguage(String(buffer: valueSlice)) 212 | case .uri: 213 | self = .uri(String(buffer: valueSlice)) 214 | case .uriScheme: 215 | self = .uriScheme(String(buffer: valueSlice)) 216 | 217 | // strings with language 218 | case .nameWithLanguage: 219 | let value = try valueSlice.readToEndOrFail { 220 | try $0.readStringWithLanguage() 221 | } 222 | self = .name(value) 223 | case .textWithLanguage: 224 | let value = try valueSlice.readToEndOrFail { 225 | try $0.readStringWithLanguage() 226 | } 227 | self = .text(value) 228 | 229 | // complex values 230 | case .dateTime: 231 | let (year, month, day, hour, minutes, seconds, deciSeconds, directionFromUtc, hoursFromUtc, minutesFromUtc) = try valueSlice.readToEndOrFail { 232 | $0.readMultipleIntegers(as: (Int16, Int8, Int8, Int8, Int8, Int8, Int8, UInt8, Int8, Int8).self) 233 | } 234 | 235 | self = .dateTime(DateTime( 236 | year: year, 237 | month: month, 238 | day: day, 239 | hour: hour, 240 | minutes: minutes, 241 | seconds: seconds, 242 | deciSeconds: deciSeconds, 243 | directionFromUtc: directionFromUtc, 244 | hoursFromUtc: hoursFromUtc, 245 | minutesFromUtc: minutesFromUtc 246 | )) 247 | case .resolution: 248 | let (crossFeed, feed, units) = try valueSlice.readToEndOrFail { 249 | $0.readMultipleIntegers(as: (Int32, Int32, Int8).self) 250 | } 251 | 252 | self = .resolution(Resolution( 253 | crossFeed: crossFeed, 254 | feed: feed, 255 | units: units 256 | )) 257 | case .rangeOfInteger: 258 | let (lower, upper) = try valueSlice.readToEndOrFail { 259 | $0.readMultipleIntegers(as: (Int32, Int32).self) 260 | } 261 | 262 | self = .rangeOfInteger(lower ... upper) 263 | case .octetString: 264 | self = .octetString(Array(buffer: valueSlice)) 265 | 266 | // out-of-band values 267 | case .unsupported: 268 | self = .unsupported 269 | case .unknown: 270 | self = .unknown 271 | case .noValue: 272 | self = .noValue 273 | 274 | case .begCollection, .endCollection, .memberAttrName: 275 | preconditionFailure("Collection tags cannot be turned into values") 276 | } 277 | } 278 | } 279 | 280 | private extension IppAttributes { 281 | mutating func pushValue(_ value: consuming IppAttribute.Value, withName name: consuming String?) throws { 282 | if let name { 283 | // push next attribute to group 284 | self[IppAttribute.Name(rawValue: name)] = IppAttribute(value) 285 | } else { 286 | // push value as additional to last value 287 | guard !isEmpty else { 288 | throw ParsingError.malformedValue 289 | } 290 | let lastIndex = values.endIndex - 1 291 | values[lastIndex].pushAdditionalValue(value) 292 | } 293 | } 294 | } 295 | 296 | private extension IppAttribute { 297 | mutating func pushAdditionalValue(_ value: Value) { 298 | if additionalValues == nil { 299 | additionalValues = [value] 300 | } else { 301 | additionalValues!.append(value) 302 | } 303 | } 304 | } 305 | 306 | internal enum ParsingError: Error, CustomStringConvertible { 307 | case malformedHeader 308 | case unexpectedValueTag(UInt8) 309 | case unexpectedDelimiterTag(UInt8) 310 | case missingEndOfAttributes 311 | case missingEndOfCollection 312 | case invalidCollectionSyntax(String) 313 | case malformedValue 314 | 315 | var description: String { 316 | switch self { 317 | case .malformedHeader: 318 | "Malformed header. Could not read mandatory IPP version, operation or status code, and request ID." 319 | case let .unexpectedValueTag(tag): 320 | "Unexpected value tag \(tag)." 321 | case let .unexpectedDelimiterTag(tag): 322 | "Unexpected delimiter tag \(tag)." 323 | case .missingEndOfAttributes: 324 | "Missing end of attributes delimiter." 325 | case .missingEndOfCollection: 326 | "Missing end of collection delimiter." 327 | case let .invalidCollectionSyntax(name): 328 | "Invalid collection syntax. Unexpected name \(name)." 329 | case .malformedValue: 330 | "Malformed value." 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /Sources/IppProtocol/SemanticModel.swift: -------------------------------------------------------------------------------- 1 | /// Namespace for IPP semantic model support. 2 | public enum SemanticModel { 3 | /// Defines a value type conversion of a semantic attribute. 4 | /// 5 | /// "Sytnax" is the word used in the IPP specification for the type of an attribute. 6 | public struct Syntax { 7 | public let get: (IppAttribute) -> Value? 8 | public let set: (Value) -> IppAttribute? 9 | 10 | public init(get: @escaping (IppAttribute) -> Value?, set: @escaping (Value) -> IppAttribute?) { 11 | self.get = get 12 | self.set = set 13 | } 14 | } 15 | 16 | /// Provides simplified typed access to attributes on IPP requests and responses. 17 | public struct Attribute { 18 | // The name of the attribute. 19 | public let name: IppAttribute.Name 20 | 21 | /// The syntax of the attribute as specified by the IPP model. 22 | public let syntax: Syntax 23 | } 24 | 25 | /// Provides key-paths for simplified typed access to attributes on IPP requests and responses. 26 | public struct Attributes { 27 | public let operation = Operation() 28 | public let operationResponse = OperationResponse() 29 | public let jobTemplate = JobTemplate() 30 | public let jobDescription = JobDescription() 31 | public let printerDescription = PrinterDescription() 32 | 33 | public struct Operation { 34 | public var attributesCharset: Attribute { .init(name: .attributesCharset, syntax: Syntaxes.charset) } 35 | public var attributesNaturalLanguage: Attribute { .init(name: .attributesNaturalLanguage, syntax: Syntaxes.naturalLanguage) } 36 | public var printerUri: Attribute { .init(name: .printerUri, syntax: Syntaxes.uri) } 37 | public var jobUri: Attribute { .init(name: .jobUri, syntax: Syntaxes.uri) } 38 | public var jobId: Attribute { .init(name: .jobId, syntax: Syntaxes.integer) } 39 | public var documentUri: Attribute { .init(name: .documentUri, syntax: Syntaxes.uri) } 40 | public var requestingUserName: Attribute { .init(name: .requestingUserName, syntax: Syntaxes.name) } 41 | public var jobName: Attribute { .init(name: .jobName, syntax: Syntaxes.name) } 42 | public var documentName: Attribute { .init(name: .documentName, syntax: Syntaxes.name) } 43 | public var requestedAttributes: Attribute<[IppAttribute.Name]> { .init(name: .requestedAttributes, syntax: Syntaxes.setOf(Syntaxes.keyword())) } 44 | public var documentFormat: Attribute { .init(name: .documentFormat, syntax: Syntaxes.mimeMediaType) } 45 | public var message: Attribute { .init(name: .message, syntax: Syntaxes.text) } 46 | public var ippAttributeFidelity: Attribute { .init(name: .ippAttributeFidelity, syntax: Syntaxes.boolean) } 47 | } 48 | 49 | public struct OperationResponse { 50 | public var attributesCharset: Attribute { .init(name: .attributesCharset, syntax: Syntaxes.charset) } 51 | public var attributesNaturalLanguage: Attribute { .init(name: .attributesNaturalLanguage, syntax: Syntaxes.naturalLanguage) } 52 | public var statusMessage: Attribute { .init(name: .statusMessage, syntax: Syntaxes.text) } 53 | public var detailedStatusMessage: Attribute { .init(name: .detailedStatusMessage, syntax: Syntaxes.text) } 54 | } 55 | 56 | public struct JobTemplate { 57 | public var copies: Attribute { .init(name: .copies, syntax: Syntaxes.integer) } 58 | public var orientationRequested: Attribute { .init(name: .orientationRequested, syntax: Syntaxes.enum()) } 59 | public var printQuality: Attribute { .init(name: .printQuality, syntax: Syntaxes.enum()) } 60 | public var sides: Attribute { .init(name: .sides, syntax: Syntaxes.keyword()) } 61 | } 62 | 63 | public struct JobDescription { 64 | public var jobUri: Attribute { .init(name: .jobUri, syntax: Syntaxes.uri) } 65 | public var jobId: Attribute { .init(name: .jobId, syntax: Syntaxes.integer) } 66 | public var jobState: Attribute { .init(name: .jobState, syntax: Syntaxes.enum()) } 67 | } 68 | 69 | public struct PrinterDescription { 70 | public var printerName: Attribute { .init(name: .printerName, syntax: Syntaxes.name) } 71 | public var printerLocation: Attribute { .init(name: .printerLocation, syntax: Syntaxes.text) } 72 | public var printerInfo: Attribute { .init(name: .printerInfo, syntax: Syntaxes.text) } 73 | public var printerState: Attribute { .init(name: .printerState, syntax: Syntaxes.enum()) } 74 | public var printerStateReasons: Attribute<[PrinterStateReason]> { .init(name: .printerStateReasons, syntax: Syntaxes.setOf(Syntaxes.keyword())) } 75 | public var printerIsAcceptingJobs: Attribute { .init(name: .printerIsAcceptingJobs, syntax: Syntaxes.boolean) } 76 | public var queuedJobCount: Attribute { .init(name: .queuedJobCount, syntax: Syntaxes.integer) } 77 | public var printerMessageFromOperator: Attribute { .init(name: .printerMessageFromOperator, syntax: Syntaxes.text) } 78 | public var colorSupported: Attribute { .init(name: .colorSupported, syntax: Syntaxes.boolean) } 79 | } 80 | } 81 | 82 | /// Collection of syntaxes for IPP attributes. 83 | public enum Syntaxes { 84 | public static var charset: Syntax { .init(get: { $0.value.asString }, set: { .init(.charset($0)) }) } 85 | public static var naturalLanguage: Syntax { .init(get: { $0.value.asString }, set: { .init(.naturalLanguage($0)) }) } 86 | public static var mimeMediaType: Syntax { .init(get: { $0.value.asString }, set: { .init(.mimeMediaType($0)) }) } 87 | public static var uri: Syntax { .init(get: { $0.value.asString }, set: { .init(.uri($0)) }) } 88 | public static var uriScheme: Syntax { .init(get: { $0.value.asString }, set: { .init(.uriScheme($0)) }) } 89 | public static var name: Syntax { .init(get: { $0.value.asString }, set: { .init(.name(.withoutLanguage($0))) }) } 90 | public static var text: Syntax { .init(get: { $0.value.asString }, set: { .init(.text(.withoutLanguage($0))) }) } 91 | public static var integer: Syntax { .init(get: { $0.value.asInteger }, set: { .init(.integer($0)) }) } 92 | public static var boolean: Syntax { .init(get: { $0.value.asBool }, set: { .init(.boolean($0)) }) } 93 | 94 | // keyword 95 | public static func keyword>(as: T.Type = T.self) -> Syntax { 96 | .init(get: { $0.value.asString.flatMap(T.init(rawValue:)) }, 97 | set: { .init(.keyword($0.rawValue)) }) 98 | } 99 | 100 | public static func `enum`>(as: T.Type = T.self) -> Syntax { 101 | .init(get: { $0.value.asInteger.flatMap(T.init(rawValue:)) }, 102 | set: { .init(.enumValue($0.rawValue)) }) 103 | } 104 | 105 | public static func setOf(_ syntax: Syntax) -> Syntax<[T]> { 106 | .init(get: { $0.values.map { IppAttribute($0) }.compactMap(syntax.get) }, 107 | set: { v in .init(v.compactMap(syntax.set).reduce(into: []) { $0.append($1.value) }) }) 108 | } 109 | } 110 | 111 | // this is just here for to map key-paths to attributes in accessors 112 | static let attributes = Attributes() 113 | } 114 | 115 | public extension SemanticModel { 116 | enum Orientation: Int32 { 117 | case portrait = 3 118 | case landscape = 4 119 | case reverseLandscape = 5 120 | case reversePortrait = 6 121 | } 122 | 123 | enum PrintQuality: Int32 { 124 | case draft = 3 125 | case normal = 4 126 | case high = 5 127 | } 128 | 129 | struct Sides: RawRepresentable, CustomStringConvertible { 130 | public let rawValue: String 131 | public init(rawValue: String) { self.rawValue = rawValue } 132 | 133 | public static var oneSided: Self { Self(rawValue: "one-sided") } 134 | public static var twoSidedLongEdge: Self { Self(rawValue: "two-sided-long-edge") } 135 | public static var twoSidedShortEdge: Self { Self(rawValue: "two-sided-short-edge") } 136 | 137 | public var description: String { rawValue } 138 | } 139 | 140 | enum JobState: Int32 { 141 | case pending = 3 142 | case pendingHeld = 4 143 | case processing = 5 144 | case processingStopped = 6 145 | case canceled = 7 146 | case aborted = 8 147 | case completed = 9 148 | } 149 | 150 | enum PrinterState: Int32 { 151 | case idle = 3 152 | case processing = 4 153 | case stopped = 5 154 | } 155 | 156 | struct PrinterStateReason: RawRepresentable, CustomStringConvertible { 157 | public let rawValue: String 158 | public init(rawValue: String) { self.rawValue = rawValue } 159 | 160 | public static var other: Self { Self(rawValue: "other") } 161 | public static var mediaNeeded: Self { Self(rawValue: "media-needed") } 162 | public static var mediaJam: Self { Self(rawValue: "media-jam") } 163 | public static var movingToPaused: Self { Self(rawValue: "moving-to-paused") } 164 | public static var paused: Self { Self(rawValue: "paused") } 165 | public static var shutdown: Self { Self(rawValue: "shutdown") } 166 | public static var connectingToDevice: Self { Self(rawValue: "connecting-to-device") } 167 | public static var timedOut: Self { Self(rawValue: "timed-out") } 168 | public static var stopping: Self { Self(rawValue: "stopping") } 169 | public static var stoppedPartly: Self { Self(rawValue: "stopped-partly") } 170 | public static var tonerLow: Self { Self(rawValue: "toner-low") } 171 | public static var tonerEmpty: Self { Self(rawValue: "toner-empty") } 172 | public static var spoolAreaFull: Self { Self(rawValue: "spool-area-full") } 173 | public static var coverOpen: Self { Self(rawValue: "cover-open") } 174 | public static var interlockOpen: Self { Self(rawValue: "interlock-open") } 175 | public static var doorOpen: Self { Self(rawValue: "door-open") } 176 | public static var inputTrayMissing: Self { Self(rawValue: "input-tray-missing") } 177 | public static var mediaLow: Self { Self(rawValue: "media-low") } 178 | public static var mediaEmpty: Self { Self(rawValue: "media-empty") } 179 | public static var outputTrayMissing: Self { Self(rawValue: "output-tray-missing") } 180 | public static var outputAreaAlmostFull: Self { Self(rawValue: "output-area-almost-full") } 181 | public static var outputAreaFull: Self { Self(rawValue: "output-area-full") } 182 | public static var markerSupplyLow: Self { Self(rawValue: "marker-supply-low") } 183 | public static var markerSupplyEmpty: Self { Self(rawValue: "marker-supply-empty") } 184 | public static var markerWasteAlmostFull: Self { Self(rawValue: "marker-waste-almost-full") } 185 | 186 | public var description: String { rawValue } 187 | } 188 | } 189 | 190 | public extension IppAttributes { 191 | /// Accesses the semantic attribute specified by the key path. 192 | /// 193 | /// If the IPP value cannot be converted to the specified type, `nil` is returned. 194 | /// Likewise, if the provided value cannot be converted to an IPP value, is removed from the attributes set. 195 | subscript(_ attribute: KeyPath>) -> V? { 196 | get { 197 | let key = SemanticModel.attributes[keyPath: attribute] 198 | return self[key.name].flatMap(key.syntax.get) 199 | } 200 | set { 201 | let key = SemanticModel.attributes[keyPath: attribute] 202 | self[key.name] = newValue.flatMap(key.syntax.set) 203 | } 204 | } 205 | 206 | /// Mutates this attribute dictionary in-place using the provided closure. 207 | mutating func with(_ mutation: (inout IppAttributes) -> Void) { 208 | mutation(&self) 209 | } 210 | } 211 | 212 | public extension IppAttribute.Value { 213 | /// Returns the value of this attribute as a string, if possible. 214 | var asString: String? { 215 | switch self { 216 | case let .charset(value), 217 | let .keyword(value), 218 | let .mimeMediaType(value), 219 | let .naturalLanguage(value), 220 | let .uri(value), 221 | let .uriScheme(value): 222 | value 223 | case let .text(value), let .name(value): 224 | value.string 225 | default: 226 | nil 227 | } 228 | } 229 | 230 | var asInteger: Int32? { 231 | switch self { 232 | case let .integer(value): value 233 | case let .enumValue(value): value 234 | default: 235 | nil 236 | } 237 | } 238 | 239 | var asBool: Bool? { 240 | switch self { 241 | case let .boolean(value): value 242 | default: 243 | nil 244 | } 245 | } 246 | } 247 | 248 | extension IppAttribute.Value.TextOrName { 249 | /// Returns the text value of this attribute as a string (ie: ignoring the language if there is one) 250 | var string: String { 251 | switch self { 252 | case let .withoutLanguage(value): 253 | value 254 | case let .withLanguage(language: _, value): 255 | value 256 | } 257 | } 258 | } 259 | 260 | extension IppAttributeGroups: ExpressibleByDictionaryLiteral { 261 | public init(dictionaryLiteral elements: (IppAttributeGroup.Name, IppAttributes)...) { 262 | self = elements.map { .init(name: $0.0, attributes: $0.1) } 263 | } 264 | } 265 | --------------------------------------------------------------------------------